Files
apollo-backend/src/mailer.js
2026-04-07 10:25:28 +07:00

123 lines
3.2 KiB
JavaScript

import nodemailer from 'nodemailer';
import { buildLoginCodeEmailTemplate } from './notification-templates.js';
let cachedTransporter = null;
function getSmtpConfig() {
const host = process.env.SMTP_HOST;
const portRaw = process.env.SMTP_PORT;
const from = process.env.SMTP_FROM;
const secureRaw = process.env.SMTP_SECURE ?? 'false';
const user = process.env.SMTP_USER;
const pass = process.env.SMTP_PASS;
if (!host) {
throw new Error('SMTP_HOST is required for email login code delivery.');
}
if (!portRaw) {
throw new Error('SMTP_PORT is required for email login code delivery.');
}
if (!from) {
throw new Error('SMTP_FROM is required for email login code delivery.');
}
const port = Number(portRaw);
if (!Number.isInteger(port) || port <= 0) {
throw new Error('SMTP_PORT must be a valid integer port number.');
}
const secure = String(secureRaw).toLowerCase() === 'true';
const hasUser = Boolean(user);
const hasPass = Boolean(pass);
if (hasUser !== hasPass) {
throw new Error('SMTP_USER and SMTP_PASS must be set together.');
}
return {
from,
transport: {
host,
port,
secure,
...(hasUser && hasPass ? { auth: { user, pass } } : {}),
},
};
}
function getTransporter() {
if (cachedTransporter) {
return cachedTransporter;
}
const config = getSmtpConfig();
cachedTransporter = nodemailer.createTransport(config.transport);
return cachedTransporter;
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
function normalizeBody(body) {
if (Array.isArray(body)) {
return body
.map((line) => String(line ?? '').trim())
.filter(Boolean);
}
const text = String(body ?? '').trim();
return text ? [text] : [];
}
function buildNotificationEmailText(body, buttonText, buttonUrl) {
const lines = normalizeBody(body);
if (buttonUrl) {
lines.push(`${buttonText || 'Открыть'}: ${buttonUrl}`);
}
return lines.join('\n');
}
function buildNotificationEmailHtml(body, buttonText, buttonUrl) {
const paragraphs = normalizeBody(body)
.map((line) => `<p>${escapeHtml(line)}</p>`)
.join('');
if (!buttonUrl) {
return paragraphs;
}
return `${paragraphs}<p><a href="${escapeHtml(buttonUrl)}" style="display:inline-block;padding:12px 18px;border-radius:999px;background:#123824;color:#ffffff;text-decoration:none;font-weight:700;">${escapeHtml(buttonText || 'Открыть')}</a></p>`;
}
export async function sendLoginCodeEmail({ to, code, expiresAt }) {
const { from } = getSmtpConfig();
const transporter = getTransporter();
const template = buildLoginCodeEmailTemplate({ code, expiresAt });
await transporter.sendMail({
from,
to,
subject: template.subject,
text: template.text,
html: template.html,
});
}
export async function sendNotificationEmail({ to, subject, body, buttonText = null, buttonUrl = null }) {
const { from } = getSmtpConfig();
const transporter = getTransporter();
await transporter.sendMail({
from,
to,
subject,
text: buildNotificationEmailText(body, buttonText, buttonUrl),
html: buildNotificationEmailHtml(body, buttonText, buttonUrl),
});
}