import nodemailer from 'nodemailer' // Create reusable transporter const transporter = nodemailer.createTransport({ host: process.env.SMTP_HOST || 'localhost', port: parseInt(process.env.SMTP_PORT || '587'), secure: process.env.SMTP_PORT === '465', // true for 465, false for other ports auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }) // Default sender const defaultFrom = process.env.EMAIL_FROM || 'MOPC Platform ' // ============================================================================= // Brand Colors & Logo URLs // ============================================================================= const BRAND = { red: '#de0f1e', redHover: '#b91c1c', darkBlue: '#053d57', teal: '#557f8c', white: '#fefefe', lightGray: '#f5f5f5', textDark: '#1f2937', textMuted: '#6b7280', } const getSmallLogoUrl = () => `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/images/MOPC-blue-small.png` const getBigLogoUrl = () => `${process.env.NEXTAUTH_URL || 'http://localhost:3000'}/images/MOPC-blue-long.png` // ============================================================================= // Email Template Wrapper & Helpers // ============================================================================= /** * Wrap email content with consistent branding: * - Small logo at top of white content box * - Big logo in dark blue footer */ function getEmailWrapper(content: string): string { return ` MOPC
MOPC
${content}
Monaco Ocean Protection Challenge

Together for a healthier ocean

© ${new Date().getFullYear()} Monaco Ocean Protection Challenge

` } /** * Generate a styled CTA button */ function ctaButton(url: string, text: string): string { return `
${text}
` } /** * Generate styled section title */ function sectionTitle(text: string): string { return `

${text}

` } /** * Generate styled paragraph */ function paragraph(text: string): string { return `

${text}

` } /** * Generate styled info box */ function infoBox(content: string, variant: 'warning' | 'info' | 'success' = 'info'): string { const colors = { warning: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' }, info: { bg: '#e0f2fe', border: '#0ea5e9', text: '#0369a1' }, success: { bg: '#dcfce7', border: '#22c55e', text: '#166534' }, } const c = colors[variant] return `

${content}

` } /** * Generate styled stat card */ function statCard(label: string, value: string | number): string { return `

${label}

${value}

` } // ============================================================================= // Email Templates // ============================================================================= interface EmailTemplate { subject: string html: string text: string } /** * Generate magic link email template */ function getMagicLinkTemplate(url: string, expiryMinutes: number = 15): EmailTemplate { const content = ` ${sectionTitle('Sign in to your account')} ${paragraph('Click the button below to securely sign in to the MOPC Platform.')} ${infoBox(`This link expires in ${expiryMinutes} minutes`, 'warning')} ${ctaButton(url, 'Sign In to MOPC')}

If you didn't request this email, you can safely ignore it.

` return { subject: 'Sign in to MOPC Platform', html: getEmailWrapper(content), text: ` Sign in to MOPC Platform ========================= Click the link below to sign in to your account: ${url} This link will expire in ${expiryMinutes} minutes. If you didn't request this email, you can safely ignore it. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate generic invitation email template (not round-specific) */ function getGenericInvitationTemplate( name: string, url: string, role: string ): EmailTemplate { const roleLabel = role === 'JURY_MEMBER' ? 'jury member' : role.toLowerCase().replace('_', ' ') const greeting = name ? `Hello ${name},` : 'Hello,' const content = ` ${sectionTitle(greeting)} ${paragraph(`You've been invited to join the Monaco Ocean Protection Challenge platform as a ${roleLabel}.`)} ${paragraph('Click the button below to set up your account and get started.')} ${ctaButton(url, 'Accept Invitation')} ${infoBox('This link will expire in 24 hours.', 'info')} ` return { subject: "You're invited to join the MOPC Platform", html: getEmailWrapper(content), text: ` ${greeting} You've been invited to join the Monaco Ocean Protection Challenge platform as a ${roleLabel}. Click the link below to set up your account and get started: ${url} This link will expire in 24 hours. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate evaluation reminder email template */ function getEvaluationReminderTemplate( name: string, pendingCount: number, roundName: string, deadline: string, assignmentsUrl: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' // Deadline alert box (styled differently from info box) const deadlineBox = `

Deadline

${deadline}

` const content = ` ${sectionTitle(greeting)} ${paragraph(`This is a friendly reminder about your pending evaluations for ${roundName}.`)} ${statCard('Pending Evaluations', pendingCount)} ${deadlineBox} ${paragraph('Your expert evaluation helps identify the most promising ocean conservation projects. Please complete your reviews before the deadline.')} ${ctaButton(assignmentsUrl, 'View My Assignments')} ` return { subject: `Reminder: ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} awaiting your review`, html: getEmailWrapper(content), text: ` ${greeting} This is a friendly reminder that you have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''} for ${roundName}. Deadline: ${deadline} Please complete your evaluations before the deadline to ensure your feedback is included in the selection process. View your assignments: ${assignmentsUrl} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate announcement email template */ function getAnnouncementTemplate( name: string, title: string, message: string, ctaText?: string, ctaUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const ctaTextPlain = ctaText && ctaUrl ? `\n${ctaText}: ${ctaUrl}\n` : '' // Escape HTML in message but preserve line breaks const formattedMessage = message .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
') // Title card with success styling const titleCard = `

${title}

` const content = ` ${sectionTitle(greeting)} ${titleCard}
${formattedMessage}
${ctaText && ctaUrl ? ctaButton(ctaUrl, ctaText) : ''} ` return { subject: title, html: getEmailWrapper(content), text: ` ${greeting} ${title} ${message} ${ctaTextPlain} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate jury invitation email template */ function getJuryInvitationTemplate( name: string, url: string, roundName: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const content = ` ${sectionTitle(greeting)} ${paragraph(`You've been invited to serve as a jury member for ${roundName}.`)} ${paragraph('As a jury member, you\'ll evaluate innovative ocean protection projects and help select the most promising initiatives.')} ${ctaButton(url, 'Accept Invitation')}

This link will allow you to access the platform and view your assigned projects.

` return { subject: `You're invited to evaluate projects for ${roundName}`, html: getEmailWrapper(content), text: ` ${greeting} You've been invited to serve as a jury member for ${roundName}. As a jury member, you'll evaluate innovative ocean protection projects and help select the most promising initiatives. Click the link below to accept your invitation: ${url} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } // ============================================================================= // Email Sending Functions // ============================================================================= /** * Send magic link email for authentication */ export async function sendMagicLinkEmail( email: string, url: string ): Promise { const expiryMinutes = parseInt(process.env.MAGIC_LINK_EXPIRY || '900') / 60 const template = getMagicLinkTemplate(url, expiryMinutes) await transporter.sendMail({ from: defaultFrom, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Send generic invitation email (not round-specific) */ export async function sendInvitationEmail( email: string, name: string | null, url: string, role: string ): Promise { const template = getGenericInvitationTemplate(name || '', url, role) await transporter.sendMail({ from: defaultFrom, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Send jury invitation email (round-specific) */ export async function sendJuryInvitationEmail( email: string, name: string | null, url: string, roundName: string ): Promise { const template = getJuryInvitationTemplate(name || '', url, roundName) await transporter.sendMail({ from: defaultFrom, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Send evaluation reminder email */ export async function sendEvaluationReminderEmail( email: string, name: string | null, pendingCount: number, roundName: string, deadline: string, assignmentsUrl: string ): Promise { const template = getEvaluationReminderTemplate( name || '', pendingCount, roundName, deadline, assignmentsUrl ) await transporter.sendMail({ from: defaultFrom, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Send announcement email */ export async function sendAnnouncementEmail( email: string, name: string | null, title: string, message: string, ctaText?: string, ctaUrl?: string ): Promise { const template = getAnnouncementTemplate( name || '', title, message, ctaText, ctaUrl ) await transporter.sendMail({ from: defaultFrom, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Send a test email to verify SMTP configuration */ export async function sendTestEmail(toEmail: string): Promise { try { const content = ` ${sectionTitle('Test Email')} ${paragraph('This is a test email from the MOPC Platform.')} ${infoBox('If you received this, your email configuration is working correctly!', 'success')}

Sent at ${new Date().toISOString()}

` await transporter.sendMail({ from: defaultFrom, to: toEmail, subject: 'MOPC Platform - Test Email', text: 'This is a test email from the MOPC Platform. If you received this, your email configuration is working correctly.', html: getEmailWrapper(content), }) return true } catch { return false } } /** * Verify SMTP connection */ export async function verifyEmailConnection(): Promise { try { await transporter.verify() return true } catch { return false } }