文件
hyb-oa/server/src/services/email/index.js
T

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,
};