206 行
4.7 KiB
JavaScript
206 行
4.7 KiB
JavaScript
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,
|
|
};
|