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.orClick}
${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.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();
}