import nodemailer from 'nodemailer'; import { getVerificationCopy, getResetCopy } from './emailTemplates'; let _transporter: nodemailer.Transporter | null = null; function getTransporter() { if (!_transporter) { const host = process.env.SMTP_HOST || 'smtp.gmail.com'; const port = parseInt(process.env.SMTP_PORT || '587'); const secure = port === 465; // Port 465 = TLS, Port 587 = STARTTLS const user = process.env.SMTP_USERNAME || process.env.SMTP_USER || ''; const pass = process.env.SMTP_PASSWORD || process.env.SMTP_PASS || ''; console.log(`[EMAIL] Creating SMTP transport: ${host}:${port} (secure=${secure}, user=${user})`); _transporter = nodemailer.createTransport({ host, port, secure, auth: { user, pass }, tls: { rejectUnauthorized: false }, }); } return _transporter; } export async function sendVerificationEmail( email: string, code: string, token: string, language?: string | null, ) { const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; // Point the button at the client verify page (not the API GET handler). // This stops corporate email scanners (M365 Safe Links, etc.) from // pre-fetching the URL and silently consuming the one-shot token before // the user ever sees the email — which is the bug behind point #2. const verifyLink = `${baseUrl}/auth/verify-email?token=${token}&email=${encodeURIComponent(email)}`; const t = getVerificationCopy(language); const html = `

My Weekly ToDo's

${t.welcome}

${t.yourCode}

${code}

${t.orClick}

${t.button}

${t.expiresNote}

${t.footer}

`; const from = process.env.SMTP_FROM || '"My Weekly ToDo\'s" '; console.log(`[EMAIL] Sending verification email to ${email} from ${from}`); if (process.env.NODE_ENV === 'development') { console.log(`\n======================================================`); console.log(`[DEV VERIFICATION LINK]:\n${verifyLink}`); console.log(`======================================================\n`); } try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('SMTP timeout')), 15000) ); const mailPromise = getTransporter().sendMail({ from, to: email, subject: t.subject(code), html, }); const result = await Promise.race([mailPromise, timeoutPromise]) as any; console.log(`[EMAIL] Email sent successfully: ${result.messageId}`); } catch (err: any) { console.error(`[EMAIL] Failed to send email (Error: ${err.message}). The verification link is printed above for manual use.`); } } export async function sendPasswordResetEmail(email: string, token: string, language?: string | null) { const baseUrl = process.env.NEXTAUTH_URL || 'http://localhost:3000'; const resetLink = `${baseUrl}/auth/reset-password?token=${token}`; const t = getResetCopy(language); const html = `

My Weekly ToDo's

${t.intro}

${t.button}

${t.expiresNote}

${t.footer}

`; const from = process.env.SMTP_FROM || '"My Weekly ToDo\'s" '; console.log(`[EMAIL] Sending password reset email to ${email} from ${from}`); if (process.env.NODE_ENV === 'development') { console.log(`\n======================================================`); console.log(`[DEV RESET LINK]:\n${resetLink}`); console.log(`======================================================\n`); } try { const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('SMTP timeout')), 15000) ); const mailPromise = getTransporter().sendMail({ from, to: email, subject: t.subject, html, }); const result = await Promise.race([mailPromise, timeoutPromise]) as any; console.log(`[EMAIL] Password reset email sent successfully: ${result.messageId}`); } catch (err: any) { console.error(`[EMAIL] Failed to send reset email (Error: ${err.message}). The reset link is printed above for manual use.`); } } export function generateVerificationCode(): string { return Math.floor(100000 + Math.random() * 900000).toString(); } export function generateVerificationToken(): string { return crypto.randomUUID(); }