import crypto from 'crypto'; import nodemailer from 'nodemailer'; import { Resend } from 'resend'; type EmailPayload = { to: string; subject: string; html: string; text?: string; }; const resend = new Resend(process.env.RESEND_API_KEY); function getBaseUrl(): string { return process.env.NEXTAUTH_URL || process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3001'; } function formatFromAddress(fromEmail: string, fromName?: string): string { if (!fromName) { return fromEmail; } return `${fromName} <${fromEmail}>`; } function getSmtpPort(): number { const port = Number(process.env.SMTP_PORT || '465'); return Number.isNaN(port) ? 465 : port; } function getSmtpSecure(): boolean { return (process.env.SMTP_SECURE || 'true').toLowerCase() === 'true'; } function hasSmtpConfig(): boolean { return Boolean( process.env.SMTP_HOST && process.env.SMTP_USER && process.env.SMTP_PASS ); } async function sendViaSmtp(payload: EmailPayload): Promise { const smtpHost = process.env.SMTP_HOST; const smtpUser = process.env.SMTP_USER; const smtpPass = process.env.SMTP_PASS; if (!smtpHost || !smtpUser || !smtpPass) { throw new Error('Missing SMTP configuration (SMTP_HOST, SMTP_USER, SMTP_PASS)'); } const smtpFromEmail = process.env.SMTP_FROM_EMAIL || smtpUser; const smtpFromName = process.env.SMTP_FROM_NAME; const smtpReplyTo = process.env.SMTP_REPLY_TO || smtpFromEmail; const transporter = nodemailer.createTransport({ host: smtpHost, port: getSmtpPort(), secure: getSmtpSecure(), auth: { user: smtpUser, pass: smtpPass, }, }); await transporter.sendMail({ from: formatFromAddress(smtpFromEmail, smtpFromName), to: payload.to, replyTo: smtpReplyTo, subject: payload.subject, text: payload.text, html: payload.html, }); } async function sendViaResend(payload: EmailPayload): Promise { const fromEmail = process.env.RESEND_FROM_EMAIL || process.env.SMTP_FROM_EMAIL || 'onboarding@resend.dev'; const fromName = process.env.RESEND_FROM_NAME || process.env.SMTP_FROM_NAME; const replyTo = process.env.RESEND_REPLY_TO || process.env.SMTP_REPLY_TO || fromEmail; const result = await resend.emails.send({ from: formatFromAddress(fromEmail, fromName), to: payload.to, replyTo, subject: payload.subject, text: payload.text, html: payload.html, }); if (result.error) { throw new Error(`Resend API error: ${result.error.message || 'Unknown error'}`); } } async function sendEmailWithFallback(payload: EmailPayload): Promise { const provider = (process.env.EMAIL_PROVIDER || 'smtp').toLowerCase(); if (provider == 'resend') { await sendViaResend(payload); return; } if (!hasSmtpConfig()) { await sendViaResend(payload); return; } try { await sendViaSmtp(payload); } catch (smtpError) { console.error('[EMAIL] SMTP send failed, trying Resend fallback:', smtpError); await sendViaResend(payload); } } export function generateEmailConfirmationToken(): string { return crypto.randomBytes(32).toString('hex'); } export async function sendEmailConfirmation( email: string, name: string, token: string ): Promise { const confirmationUrl = `${getBaseUrl()}/api/auth/verify-email?token=${token}`; const html = ` Confirm your email

Confirm your email address

Hi ${name},

Thank you for signing up! Please confirm your email address by clicking the button below:

Or copy and paste this link into your browser:

${confirmationUrl}

If you didn't create an account, you can safely ignore this email.

`; const text = `Hi ${name}, Thank you for signing up. Please confirm your email address: ${confirmationUrl} If you did not create this account, you can safely ignore this email.`; await sendEmailWithFallback({ to: email, subject: 'Confirm your email address', html, text, }); } export async function sendEmailConfirmationResend( email: string, name: string, token: string ): Promise { const confirmationUrl = `${getBaseUrl()}/api/auth/verify-email?token=${token}`; const html = ` Confirm your email

Confirm your email address

Hi ${name},

You requested a new confirmation email. Please confirm your email address by clicking the button below:

Or copy and paste this link into your browser:

${confirmationUrl}

This link will expire in 24 hours. If you didn't request this email, you can safely ignore it.

`; const text = `Hi ${name}, You requested a new confirmation email. Please confirm your email address: ${confirmationUrl} This link expires in 24 hours. If you did not request this, you can ignore this email.`; await sendEmailWithFallback({ to: email, subject: 'Confirm your email address', html, text, }); } export function generatePasswordResetToken(): string { return crypto.randomBytes(32).toString('hex'); } export async function sendPasswordResetEmail( email: string, name: string, token: string ): Promise { const resetUrl = `${getBaseUrl()}/reset-password?token=${token}`; const text = `Hi ${name}, You requested to reset your password. Click the link below to create a new password: ${resetUrl} This link will expire in 1 hour. If you didn't request a password reset, you can safely ignore this email. Best regards, PunimTag Viewer Team`; const html = ` Reset your password

Reset your password

Hi ${name},

You requested to reset your password for your PunimTag Viewer account. Click the button below to create a new password:

Or copy and paste this link into your browser:

${resetUrl}

Important: This link will expire in 1 hour for security reasons.

If you didn't request a password reset, you can safely ignore this email. Your password will remain unchanged.

This is an automated message from PunimTag Viewer. Please do not reply to this email.

`; await sendEmailWithFallback({ to: email, subject: 'Reset your password - PunimTag Viewer', text, html, }); }