134 lines
4.7 KiB
JavaScript
134 lines
4.7 KiB
JavaScript
/**
|
|
* Worker thread for sending email notifications asynchronously.
|
|
* Receives payloads from the main thread and performs Puppeteer render + nodemailer send.
|
|
*/
|
|
import { parentPort } from 'worker_threads';
|
|
import puppeteer from 'puppeteer';
|
|
import nodemailer from 'nodemailer';
|
|
import log4js from 'log4js';
|
|
import config from './config.js';
|
|
|
|
const baseUrl = (urlClient) => (urlClient || 'http://localhost:3000').replace(/\/$/, '');
|
|
|
|
async function fetchAndInlineStyles(html, urlClient) {
|
|
const base = baseUrl(urlClient);
|
|
const linkMatches = [...html.matchAll(/<link[^>]+>/g)];
|
|
const stylesheetLinks = linkMatches
|
|
.map((m) => {
|
|
const tag = m[0];
|
|
if (!/rel=["']stylesheet["']/i.test(tag)) return null;
|
|
const hrefMatch = tag.match(/href=["']([^"']+)["']/);
|
|
return hrefMatch ? { tag, href: hrefMatch[1] } : null;
|
|
})
|
|
.filter(Boolean);
|
|
let inlined = html;
|
|
for (const { tag, href } of stylesheetLinks) {
|
|
const url = href.startsWith('http') ? href : `${base}${href.startsWith('/') ? '' : '/'}${href}`;
|
|
try {
|
|
const res = await fetch(url);
|
|
if (res.ok) {
|
|
const css = await res.text();
|
|
inlined = inlined.replace(tag, `<style>${css}</style>`);
|
|
}
|
|
} catch (e) {
|
|
logger.trace('Could not fetch stylesheet:', url, e.message);
|
|
}
|
|
}
|
|
return inlined;
|
|
}
|
|
|
|
const logger = log4js.getLogger('MailWorker');
|
|
logger.level = config.server.logLevel;
|
|
|
|
async function sendEmail(payload) {
|
|
const { email, title, message, type, metadata, smtpConfig, urlClient, createdAt, updatedAt, authCode } =
|
|
payload;
|
|
|
|
if (!email || !smtpConfig?.host) {
|
|
logger.warn('Missing email or SMTP config, skipping...');
|
|
return;
|
|
}
|
|
|
|
const params = new URLSearchParams({
|
|
title: title || '',
|
|
message: message || '',
|
|
type: type || 'info',
|
|
email: email || '',
|
|
createdAt: createdAt || new Date(),
|
|
updatedAt: updatedAt || new Date(),
|
|
metadata: JSON.stringify(metadata || {}),
|
|
});
|
|
if (authCode) {
|
|
params.set('authCode', authCode);
|
|
}
|
|
const templateUrl = `${baseUrl(urlClient)}/email/notification?${params.toString()}`;
|
|
|
|
logger.debug('Rendering template...');
|
|
logger.trace('Template URL:', templateUrl);
|
|
|
|
let html = '';
|
|
let browser;
|
|
try {
|
|
browser = await puppeteer.launch({
|
|
headless: true,
|
|
args: ['--no-sandbox', '--disable-setuid-sandbox'],
|
|
});
|
|
const page = await browser.newPage();
|
|
await page.goto(templateUrl, { waitUntil: 'networkidle0', timeout: 15000 });
|
|
await page.waitForSelector('#email-notification-root[data-rendered="true"]', { timeout: 5000 });
|
|
// Wait for Ant Design CSS-in-JS to finish injecting styles
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
html = await page.evaluate(() => {
|
|
const root = document.getElementById('email-notification-root');
|
|
if (!root) return document.documentElement.outerHTML;
|
|
const origin = document.location.origin;
|
|
const styleTags = Array.from(document.querySelectorAll('style'))
|
|
.map((s) => s.outerHTML)
|
|
.join('\n');
|
|
const linkTags = Array.from(document.querySelectorAll('link[rel="stylesheet"]'))
|
|
.map((link) => {
|
|
const href = link.getAttribute('href');
|
|
const abs = href?.startsWith('http')
|
|
? href
|
|
: `${origin}${href?.startsWith('/') ? '' : '/'}${href || ''}`;
|
|
return `<link rel="stylesheet" href="${abs}">`;
|
|
})
|
|
.join('\n');
|
|
return `<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">${styleTags}${linkTags}</head><body>${root.outerHTML}</body></html>`;
|
|
});
|
|
html = await fetchAndInlineStyles(html, urlClient);
|
|
} catch (err) {
|
|
logger.error('MailWorker: Puppeteer error', err.message);
|
|
html = `<div style="font-family:sans-serif;padding:20px"><h2>${title || 'Notification'}</h2><p>${message || ''}</p></div>`;
|
|
} finally {
|
|
if (browser) await browser.close();
|
|
}
|
|
|
|
const transporter = nodemailer.createTransport({
|
|
host: smtpConfig.host,
|
|
port: smtpConfig.port || 587,
|
|
secure: smtpConfig.secure || false,
|
|
auth: smtpConfig.auth?.user ? smtpConfig.auth : undefined,
|
|
});
|
|
|
|
const subject = title ? `${title} - FarmControl` : 'FarmControl Notification';
|
|
|
|
const mailOptions = {
|
|
from: smtpConfig.from || 'FarmControl <noreply@tombutcher.work>',
|
|
to: email,
|
|
subject: subject,
|
|
html,
|
|
};
|
|
|
|
logger.debug('Sending email...');
|
|
logger.trace('Mail options:', mailOptions);
|
|
const info = await transporter.sendMail(mailOptions);
|
|
logger.debug('Email sent successfully.');
|
|
}
|
|
|
|
parentPort.on('message', (payload) => {
|
|
sendEmail(payload).catch((err) => {
|
|
logger.error('MailWorker: send failed', err.message);
|
|
});
|
|
});
|