/** * 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(/]+>/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, ``); } } 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(), authCode: authCode || '', metadata: JSON.stringify(metadata || {}), }); 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: 'new', args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-features=SameSiteByDefaultCookies', ], }); const page = await browser.newPage(); page.on('console', (msg) => { const text = msg.text(); const type = msg.type(); logger.trace(`Puppeteer [${type}]: ${text}`); }); await page.goto(templateUrl, { waitUntil: 'networkidle0', timeout: 30000 }); await page.waitForSelector('#email-notification-root[data-rendered="true"]', { timeout: 5000 }); // Wait for Ant Design CSS-in-JS to finish injecting styles logger.debug('Waiting for 1.5 seconds for page to render...'); await new Promise((r) => setTimeout(r, 1500)); 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 ``; }) .join('\n'); return `${styleTags}${linkTags}${root.outerHTML}`; }); html = await fetchAndInlineStyles(html, urlClient); } catch (err) { logger.error('MailWorker: Puppeteer error', err.message); html = `

${title || 'Notification'}

${message || ''}

`; } 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 ', 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); }); });