import nodemailer, { type Transporter } from 'nodemailer'; // The API sends exactly one kind of mail: the link that proves an address. The // relay is declared in the deployment's .env, because a mail server is // infrastructure, not a constant. // // With no SMTP_HOST there is nothing to connect to, so the link is written to // the log instead. That keeps a dev box — or this repo's own test suite — able // to finish a signup without a mail server, and whoever reads the log is // already the person running the database. const SMTP_HOST = (process.env.SMTP_HOST ?? '').trim(); const SMTP_PORT = Number(process.env.SMTP_PORT || 587); const SMTP_USER = (process.env.SMTP_USER ?? '').trim(); const SMTP_PASS = process.env.SMTP_PASS ?? ''; const SMTP_FROM = (process.env.SMTP_FROM ?? '').trim() || SMTP_USER || 'no-reply@recipescam.local'; // 465 is TLS from the first byte; 587 starts in the clear and upgrades. Only a // port the operator actually chose should be second-guessed. const SMTP_SECURE = process.env.SMTP_SECURE ? process.env.SMTP_SECURE === 'true' : SMTP_PORT === 465; export const mailConfigured = SMTP_HOST !== ''; const transporter: Transporter | null = mailConfigured ? nodemailer.createTransport({ host: SMTP_HOST, port: SMTP_PORT, secure: SMTP_SECURE, auth: SMTP_USER ? { user: SMTP_USER, pass: SMTP_PASS } : undefined, // A relay that never answers must not hold a request open. connectionTimeout: 10_000, greetingTimeout: 10_000, socketTimeout: 20_000, }) : null; // Both languages, because the account's language is not known before it exists. // The code comes first and the link second: the code is the one that works // without leaving the page the visitor signed up on, so it is what most of them // will use — but the link stays, for the phone that owns the address, and // because a letter that only carries digits is a letter a spam filter can // decide is a phishing attempt. const body = (url: string, code: string) => [ 'RecipesCam — xác thực địa chỉ email / verify your email address', '', `Mã xác thực của bạn / your verification code: ${code}`, '', 'Mã có hiệu lực 15 phút. Nhập mã vào trang đăng ký để bật các tính năng PRO.', 'The code is valid for 15 minutes. Type it into the signup page to unlock PRO.', '', 'Hoặc mở liên kết này / or open this link (hiệu lực 24 giờ / valid 24 hours):', url, '', 'Nếu bạn không đăng ký, hãy bỏ qua thư này. / If you did not sign up, ignore this mail.', ].join('\r\n'); // Fire and forget: the account already exists, so a relay that is slow, out of // quota or misconfigured may not fail the signup that asked for it. The owner // can ask for another link from the studio; the operator sees the error here. export function sendVerificationMail(to: string, url: string, code: string, log: (msg: string) => void): void { if (!transporter) { log(`[verify] SMTP not configured — code ${code} and verification link for ${to}: ${url}`); return; } transporter .sendMail({ from: SMTP_FROM, to, subject: `RecipesCam — ${code} is your verification code`, text: body(url, code) }) .then(() => log(`[verify] code and link sent to ${to}`)) .catch((err: unknown) => log(`[verify] could not mail ${to}: ${String(err)}`)); }