/** * 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(), 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 ``; }) .join('\n'); return `
${styleTags}${linkTags}${root.outerHTML}`; }); html = await fetchAndInlineStyles(html, urlClient); } catch (err) { logger.error('MailWorker: Puppeteer error', err.message); html = `${message || ''}