const nodemailer = require('nodemailer'); function resolveEmailBaseUrl(config) { const explicit = String(config.emailBaseUrl ?? '').trim(); if (explicit) { return explicit.replace(/\/+$/, ''); } const origin = String(config.appOrigin ?? '').split(',')[0]?.trim?.() ?? ''; if (origin) { return origin.replace(/\/+$/, ''); } return ''; } let cachedTransportKey = null; let cachedTransporter = null; function getTransporter(config) { if (!config.emailEnabled) { return null; } if (!config.smtpHost) { return null; } const authUser = String(config.smtpUser ?? '').trim(); const authPass = String(config.smtpPass ?? '').trim(); const key = JSON.stringify({ host: config.smtpHost, port: config.smtpPort, secure: config.smtpSecure, user: authUser, }); if (cachedTransporter && cachedTransportKey === key) { return cachedTransporter; } cachedTransportKey = key; cachedTransporter = nodemailer.createTransport({ host: config.smtpHost, port: config.smtpPort, secure: config.smtpSecure, auth: authUser ? { user: authUser, pass: authPass } : undefined, }); return cachedTransporter; } function resolveEmailDeliveryStatus(config) { const reasons = []; const authUser = String(config.smtpUser ?? '').trim(); const authPass = String(config.smtpPass ?? '').trim(); const smtpPort = Number(config.smtpPort); if (!config.emailEnabled) { reasons.push('email_disabled'); } if (!String(config.emailFrom ?? '').trim()) { reasons.push('email_from_missing'); } if (!String(config.smtpHost ?? '').trim()) { reasons.push('smtp_host_missing'); } if (!Number.isFinite(smtpPort) || smtpPort <= 0) { reasons.push('smtp_port_invalid'); } if (authUser && !authPass) { reasons.push('smtp_auth_incomplete'); } if (!authUser && authPass) { reasons.push('smtp_auth_incomplete'); } return { ready: reasons.length === 0, reasons, }; } function uniq(values) { return Array.from(new Set((values ?? []).filter(Boolean))); } async function sendNotificationEmailSafe(params) { try { const emailResult = await sendNotificationEmail(params); return { email: emailResult, }; } catch (error) { params.logger?.error?.( { err: error, requestId: params.requestId, notificationType: params.type, recipients: params.recipientEmployeeIds?.length ?? 0, }, 'Failed to send notification email', ); return { email: { skipped: false, sent: 0, failed: params.recipientEmployeeIds?.length ?? 0 }, }; } } async function sendNotificationEmail({ prisma, config, logger, requestId, recipientEmployeeIds, subject, body, type = 'notification', targetPath, }) { if (!config.emailEnabled) { return { skipped: true, sent: 0, failed: 0 }; } const transporter = getTransporter(config); if (!transporter) { logger?.warn?.({ requestId }, 'Email enabled but transporter not configured'); return { skipped: true, sent: 0, failed: 0 }; } const recipients = uniq(recipientEmployeeIds); if (!recipients.length) { return { skipped: true, sent: 0, failed: 0 }; } const employees = await prisma.employee.findMany({ where: { id: { in: recipients }, status: 'active', }, select: { id: true, email: true, name: true, }, }); const emailFrom = String(config.emailFrom ?? '').trim(); if (!emailFrom) { logger?.warn?.({ requestId }, 'Email enabled but EMAIL_FROM is empty'); return { skipped: true, sent: 0, failed: 0 }; } const baseUrl = resolveEmailBaseUrl(config); const link = targetPath && baseUrl ? `${baseUrl}${targetPath}` : null; const safeSubject = String(subject ?? '').trim().slice(0, 200) || '系统通知'; const safeBody = String(body ?? '').trim(); let sent = 0; let failed = 0; for (const employee of employees) { if (!employee.email) { failed += 1; continue; } const text = link ? `${safeBody}\n\n查看详情:${link}` : safeBody; try { await transporter.sendMail({ from: emailFrom, to: employee.email, subject: safeSubject, text, headers: { 'x-hyb-notification-type': type, 'x-request-id': requestId ?? '', }, }); sent += 1; } catch (error) { failed += 1; logger?.error?.( { err: error, requestId, employeeId: employee.id, email: employee.email, notificationType: type, }, 'Email send failed', ); } } return { skipped: false, sent, failed }; } module.exports = { resolveEmailDeliveryStatus, sendNotificationEmailSafe, sendNotificationEmail, };