import nodemailer from 'nodemailer' import type { Transporter } from 'nodemailer' import { prisma } from '@/lib/prisma' // Cached transporter and config hash to detect changes let cachedTransporter: Transporter | null = null let cachedConfigHash = '' let cachedFrom = '' /** * Get SMTP transporter using database settings with env var fallback. * Caches the transporter and rebuilds it when settings change. */ async function getTransporter(): Promise<{ transporter: Transporter; from: string }> { // Read DB settings const dbSettings = await prisma.systemSettings.findMany({ where: { key: { in: ['smtp_host', 'smtp_port', 'smtp_user', 'smtp_password', 'email_from_name', 'email_from'] }, }, select: { key: true, value: true }, }) const db: Record = {} for (const s of dbSettings) { db[s.key] = s.value } // DB settings take priority, env vars as fallback const host = db.smtp_host || process.env.SMTP_HOST || 'localhost' const port = db.smtp_port || process.env.SMTP_PORT || '587' const user = db.smtp_user || process.env.SMTP_USER || '' const pass = db.smtp_password || process.env.SMTP_PASS || '' // Combine sender name and email into "Name " format const fromName = db.email_from_name || 'MOPC Portal' const rawFromEmail = db.email_from || process.env.EMAIL_FROM || 'noreply@monaco-opc.com' // Strip "Name <...>" wrapper if present — extract just the email address const fromEmail = rawFromEmail.includes('<') ? rawFromEmail.match(/<([^>]+)>/)?.[1] || rawFromEmail : rawFromEmail const from = `${fromName} <${fromEmail}>` // Check if config changed since last call const configHash = `${host}:${port}:${user}:${pass}:${from}` if (cachedTransporter && configHash === cachedConfigHash) { return { transporter: cachedTransporter, from: cachedFrom } } // Create new transporter cachedTransporter = nodemailer.createTransport({ host, port: parseInt(port), secure: port === '465', auth: { user, pass }, }) cachedConfigHash = configHash cachedFrom = from return { transporter: cachedTransporter, from: cachedFrom } } // Legacy references for backward compat — default sender from env const defaultFrom = process.env.EMAIL_FROM || 'MOPC Portal ' // ============================================================================= // Helpers // ============================================================================= /** * Get the base URL for links in emails. * Uses NEXTAUTH_URL with a safe production fallback. */ export function getBaseUrl(): string { return process.env.NEXTAUTH_URL || 'https://portal.monaco-opc.com' } /** * Ensure a URL is absolute (has protocol + host). * Converts relative paths like "/jury/competitions" to full URLs. */ export function ensureAbsoluteUrl(url: string | undefined): string | undefined { if (!url) return undefined if (url.startsWith('http://') || url.startsWith('https://')) return url if (url.startsWith('/')) return `${getBaseUrl()}${url}` return `${getBaseUrl()}/${url}` } // ============================================================================= // Brand Colors & Logo URLs // ============================================================================= const BRAND = { red: '#de0f1e', redHover: '#b91c1c', darkBlue: '#053d57', teal: '#557f8c', white: '#fefefe', lightGray: '#f5f5f5', textDark: '#1f2937', textMuted: '#6b7280', } const getSmallLogoUrl = () => 'https://s3.monaco-opc.com/public/MOPC-blue-small.png' const getBigLogoUrl = () => 'https://s3.monaco-opc.com/public/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 { // Ensure URL is always absolute for email clients const safeUrl = ensureAbsoluteUrl(url) || url return `
${text}

If the button doesn't work: ${safeUrl}

` } /** * 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 Portal.')} ${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 Portal', html: getEmailWrapper(content), text: ` Sign in to MOPC Portal ========================= 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 formatExpiryLabel(hours: number): string { if (hours < 24) return `${hours} hour${hours !== 1 ? 's' : ''}` const days = Math.round(hours / 24) return `${days} day${days !== 1 ? 's' : ''}` } function getGenericInvitationTemplate( name: string, url: string, role: string, expiryHours: number = 72 ): EmailTemplate { const roleLabel = role === 'JURY_MEMBER' ? 'jury member' : role.toLowerCase().replace('_', ' ') const article = /^[aeiou]/i.test(roleLabel) ? 'an' : 'a' const greeting = name ? `Hello ${name},` : 'Hello,' const expiryLabel = formatExpiryLabel(expiryHours) const content = ` ${sectionTitle(greeting)} ${paragraph(`You've been invited to join the Monaco Ocean Protection Challenge platform as ${article} ${roleLabel}.`)} ${paragraph('Click the button below to set up your account and get started.')} ${ctaButton(url, 'Accept Invitation')} ${infoBox(`This link will expire in ${expiryLabel}.`, 'info')} ` return { subject: "You're invited to join the MOPC Portal", html: getEmailWrapper(content), text: ` ${greeting} You've been invited to join the Monaco Ocean Protection Challenge platform as ${article} ${roleLabel}. Click the link below to set up your account and get started: ${url} This link will expire in ${expiryLabel}. --- 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. `, } } /** * Generate application confirmation email template */ function getApplicationConfirmationTemplate( name: string, projectName: string, programName: string, customMessage?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const customMessageHtml = customMessage ? `
${customMessage.replace(/\n/g, '
')}
` : '' const content = ` ${sectionTitle(greeting)} ${paragraph(`Thank you for submitting your application to ${programName}!`)} ${infoBox(`Your project "${projectName}" has been successfully received.`, 'success')} ${customMessageHtml} ${paragraph('Our team will review your submission and get back to you soon. In the meantime, if you have any questions, please don\'t hesitate to reach out.')}

You will receive email updates about your application status.

` return { subject: `Application Received - ${projectName}`, html: getEmailWrapper(content), text: ` ${greeting} Thank you for submitting your application to ${programName}! Your project "${projectName}" has been successfully received. ${customMessage || ''} Our team will review your submission and get back to you soon. In the meantime, if you have any questions, please don't hesitate to reach out. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate team member invite email template */ function getTeamMemberInviteTemplate( name: string, projectName: string, teamLeadName: string, inviteUrl: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const content = ` ${sectionTitle(greeting)} ${paragraph(`${teamLeadName} has invited you to join their team for the project "${projectName}" on the Monaco Ocean Protection Challenge platform.`)} ${paragraph('Click the button below to accept the invitation and set up your account.')} ${ctaButton(inviteUrl, 'Accept Invitation')} ${infoBox('This invitation link will expire in 30 days.', 'info')}

If you weren't expecting this invitation, you can safely ignore this email.

` return { subject: `You've been invited to join "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} ${teamLeadName} has invited you to join their team for the project "${projectName}" on the Monaco Ocean Protection Challenge platform. Click the link below to accept the invitation and set up your account: ${inviteUrl} This invitation link will expire in 30 days. If you weren't expecting this invitation, you can safely ignore this email. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } // ============================================================================= // Notification Email Templates // ============================================================================= /** * Context passed to notification email templates */ export interface NotificationEmailContext { name?: string title: string message: string linkUrl?: string linkLabel?: string metadata?: Record } /** * Generate "Advanced to Semi-Finals" email template */ function getAdvancedSemifinalTemplate( name: string, projectName: string, programName: string, nextSteps?: string ): EmailTemplate { const greeting = name ? `Congratulations ${name}!` : 'Congratulations!' const celebrationBanner = `

Exciting News

You're a Semi-Finalist!

` const content = ` ${sectionTitle(greeting)} ${celebrationBanner} ${paragraph(`Your project "${projectName}" has been selected to advance to the semi-finals of ${programName}.`)} ${infoBox('Your innovative approach to ocean protection stood out among hundreds of submissions.', 'success')} ${nextSteps ? paragraph(`Next Steps: ${nextSteps}`) : paragraph('Our team will be in touch shortly with details about the next phase of the competition.')} ` return { subject: `Congratulations! "${projectName}" advances to Semi-Finals`, html: getEmailWrapper(content), text: ` ${greeting} Your project "${projectName}" has been selected to advance to the semi-finals of ${programName}. Your innovative approach to ocean protection stood out among hundreds of submissions. ${nextSteps || 'Our team will be in touch shortly with details about the next phase of the competition.'} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Selected as Finalist" email template */ function getAdvancedFinalTemplate( name: string, projectName: string, programName: string, nextSteps?: string ): EmailTemplate { const greeting = name ? `Incredible news, ${name}!` : 'Incredible news!' const celebrationBanner = `

Outstanding Achievement

You're a Finalist!

` const content = ` ${sectionTitle(greeting)} ${celebrationBanner} ${paragraph(`Your project "${projectName}" has been selected as a Finalist in ${programName}.`)} ${infoBox('You are now among the top projects competing for the grand prize!', 'success')} ${nextSteps ? paragraph(`What Happens Next: ${nextSteps}`) : paragraph('Prepare for the final stage of the competition. Our team will contact you with details about the finalist presentations and judging.')} ` return { subject: `You're a Finalist! "${projectName}" selected for finals`, html: getEmailWrapper(content), text: ` ${greeting} Your project "${projectName}" has been selected as a Finalist in ${programName}. You are now among the top projects competing for the grand prize! ${nextSteps || 'Prepare for the final stage of the competition. Our team will contact you with details about the finalist presentations and judging.'} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Mentor Assigned" email template (for team) */ function getMentorAssignedTemplate( name: string, projectName: string, mentorName: string, mentorBio?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const mentorCard = `

Your Mentor

${mentorName}

${mentorBio ? `

${mentorBio}

` : ''}
` const content = ` ${sectionTitle(greeting)} ${paragraph(`Great news! A mentor has been assigned to support your project "${projectName}".`)} ${mentorCard} ${paragraph('Your mentor will provide guidance, feedback, and support as you develop your ocean protection initiative. They will reach out to you shortly to introduce themselves and schedule your first meeting.')} ${infoBox('Mentorship is a valuable opportunity - make the most of their expertise!', 'info')} ` return { subject: `A mentor has been assigned to "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} Great news! A mentor has been assigned to support your project "${projectName}". Your Mentor: ${mentorName} ${mentorBio || ''} Your mentor will provide guidance, feedback, and support as you develop your ocean protection initiative. They will reach out to you shortly to introduce themselves and schedule your first meeting. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Not Selected" email template */ function getNotSelectedTemplate( name: string, projectName: string, roundName: string, feedbackUrl?: string, encouragement?: string ): EmailTemplate { const greeting = name ? `Dear ${name},` : 'Dear Applicant,' const content = ` ${sectionTitle(greeting)} ${paragraph(`Thank you for participating in ${roundName} with your project "${projectName}".`)} ${paragraph('After careful consideration by our jury, we regret to inform you that your project was not selected to advance to the next round.')} ${infoBox('This decision was incredibly difficult given the high quality of submissions we received this year.', 'info')} ${feedbackUrl ? ctaButton(feedbackUrl, 'View Jury Feedback') : ''} ${paragraph(encouragement || 'We encourage you to continue developing your ocean protection initiative and to apply again in future editions. Your commitment to protecting our oceans is valuable and appreciated.')}

Thank you for being part of the Monaco Ocean Protection Challenge community.

` return { subject: `Update on your application: "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} Thank you for participating in ${roundName} with your project "${projectName}". After careful consideration by our jury, we regret to inform you that your project was not selected to advance to the next round. This decision was incredibly difficult given the high quality of submissions we received this year. ${feedbackUrl ? `View jury feedback: ${feedbackUrl}` : ''} ${encouragement || 'We encourage you to continue developing your ocean protection initiative and to apply again in future editions.'} Thank you for being part of the Monaco Ocean Protection Challenge community. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Winner Announcement" email template */ function getWinnerAnnouncementTemplate( name: string, projectName: string, awardName: string, prizeDetails?: string ): EmailTemplate { const greeting = name ? `Congratulations ${name}!` : 'Congratulations!' const trophyBanner = `

🏆

Winner

${awardName}

` const content = ` ${sectionTitle(greeting)} ${trophyBanner} ${paragraph(`We are thrilled to announce that your project "${projectName}" has been selected as the winner of the ${awardName}!`)} ${infoBox('Your outstanding work in ocean protection has made a lasting impression on our jury.', 'success')} ${prizeDetails ? paragraph(`Your Prize: ${prizeDetails}`) : ''} ${paragraph('Our team will be in touch shortly with details about the award ceremony and next steps.')} ` return { subject: `You Won! "${projectName}" wins ${awardName}`, html: getEmailWrapper(content), text: ` ${greeting} We are thrilled to announce that your project "${projectName}" has been selected as the winner of the ${awardName}! Your outstanding work in ocean protection has made a lasting impression on our jury. ${prizeDetails ? `Your Prize: ${prizeDetails}` : ''} Our team will be in touch shortly with details about the award ceremony and next steps. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Assigned to Project" email template (for jury) */ function getAssignedToProjectTemplate( name: string, projectName: string, roundName: string, deadline?: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const projectCard = `

New Assignment

${projectName}

` const deadlineBox = deadline ? `

Deadline

${deadline}

` : '' const content = ` ${sectionTitle(greeting)} ${paragraph(`You have been assigned a new project to evaluate for ${roundName}.`)} ${projectCard} ${deadlineBox} ${paragraph('Please review the project materials and submit your evaluation before the deadline.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignment') : ''} ` return { subject: `New Assignment: "${projectName}" - ${roundName}`, html: getEmailWrapper(content), text: ` ${greeting} You have been assigned a new project to evaluate for ${roundName}. Project: ${projectName} ${deadline ? `Deadline: ${deadline}` : ''} Please review the project materials and submit your evaluation before the deadline. ${assignmentsUrl ? `View assignment: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "COI Reassignment" email template (for jury receiving a reassigned project) */ function getCOIReassignedTemplate( name: string, projectName: string, roundName: string, deadline?: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const projectCard = `

Reassigned Project

${projectName}

` const deadlineBox = deadline ? `

Deadline

${deadline}

` : '' const content = ` ${sectionTitle(greeting)} ${paragraph(`A project has been reassigned to you for evaluation in ${roundName}, because the previously assigned juror declared a conflict of interest.`)} ${projectCard} ${deadlineBox} ${paragraph('Please review the project materials and submit your evaluation before the deadline. This is an additional project on top of your existing assignments.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignment') : ''} ` return { subject: `Project Reassigned to You: "${projectName}" - ${roundName}`, html: getEmailWrapper(content), text: ` ${greeting} A project has been reassigned to you for evaluation in ${roundName}, because the previously assigned juror declared a conflict of interest. Project: ${projectName} ${deadline ? `Deadline: ${deadline}` : ''} Please review the project materials and submit your evaluation before the deadline. This is an additional project on top of your existing assignments. ${assignmentsUrl ? `View assignment: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Manual Reassignment" email template (for jury) * Sent when an admin manually transfers a project assignment to a juror. */ function getManualReassignedTemplate( name: string, projectNames: string[], roundName: string, deadline?: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const count = projectNames.length const isSingle = count === 1 const projectList = projectNames.map((p) => `

${p}

`).join('') const deadlineBox = deadline ? `

Deadline

${deadline}

` : '' const content = ` ${sectionTitle(greeting)} ${paragraph(`An administrator has reassigned ${isSingle ? 'a project' : `${count} projects`} to you for evaluation in ${roundName}.`)} ${projectList} ${deadlineBox} ${paragraph(`Please review the project material${isSingle ? '' : 's'} and submit your evaluation${isSingle ? '' : 's'} before the deadline.`)} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignments') : ''} ` const projectListText = projectNames.map((p) => ` - ${p}`).join('\n') return { subject: `Project${isSingle ? '' : 's'} Reassigned to You - ${roundName}`, html: getEmailWrapper(content), text: ` ${greeting} An administrator has reassigned ${isSingle ? 'a project' : `${count} projects`} to you for evaluation in ${roundName}. ${isSingle ? `Project: ${projectNames[0]}` : `Projects:\n${projectListText}`} ${deadline ? `Deadline: ${deadline}` : ''} Please review the project material${isSingle ? '' : 's'} and submit your evaluation${isSingle ? '' : 's'} before the deadline. ${assignmentsUrl ? `View assignments: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Dropout Reassignment" email template (for jury) * Sent when a juror drops out and their projects are redistributed. */ function getDropoutReassignedTemplate( name: string, projectNames: string[], roundName: string, droppedJurorName: string, deadline?: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const count = projectNames.length const isSingle = count === 1 const projectList = projectNames.map((p) => `

${p}

`).join('') const deadlineBox = deadline ? `

Deadline

${deadline}

` : '' const content = ` ${sectionTitle(greeting)} ${paragraph(`Due to a juror becoming unavailable, ${isSingle ? 'a project has' : `${count} projects have`} been reassigned to you for evaluation in ${roundName}.`)} ${projectList} ${deadlineBox} ${paragraph(`${isSingle ? 'This project was' : 'These projects were'} previously assigned to ${droppedJurorName}, who is no longer available. Please review the project material${isSingle ? '' : 's'} and submit your evaluation${isSingle ? '' : 's'} before the deadline.`)} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'View Assignments') : ''} ` const projectListText = projectNames.map((p) => ` - ${p}`).join('\n') return { subject: `Project${isSingle ? '' : 's'} Reassigned to You (Juror Unavailable) - ${roundName}`, html: getEmailWrapper(content), text: ` ${greeting} Due to a juror becoming unavailable, ${isSingle ? 'a project has' : `${count} projects have`} been reassigned to you for evaluation in ${roundName}. ${isSingle ? `Project: ${projectNames[0]}` : `Projects:\n${projectListText}`} ${deadline ? `Deadline: ${deadline}` : ''} ${isSingle ? 'This project was' : 'These projects were'} previously assigned to ${droppedJurorName}, who is no longer available. Please review the project material${isSingle ? '' : 's'} and submit your evaluation${isSingle ? '' : 's'} before the deadline. ${assignmentsUrl ? `View assignments: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Batch Assigned" email template (for jury) */ function getBatchAssignedTemplate( name: string, projectCount: number, roundName: string, deadline?: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const deadlineBox = deadline ? `

Deadline

${deadline}

` : '' const content = ` ${sectionTitle(greeting)} ${paragraph(`You have been assigned projects to evaluate for ${roundName}.`)} ${statCard('Projects Assigned', projectCount)} ${deadlineBox} ${paragraph('Please review each project and submit your evaluations before the deadline. Your expert assessment is crucial to identifying the most promising ocean protection initiatives.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'View All Assignments') : ''} ` return { subject: `${projectCount} Projects Assigned - ${roundName}`, html: getEmailWrapper(content), text: ` ${greeting} You have been assigned ${projectCount} project${projectCount !== 1 ? 's' : ''} to evaluate for ${roundName}. ${deadline ? `Deadline: ${deadline}` : ''} Please review each project and submit your evaluations before the deadline. ${assignmentsUrl ? `View assignments: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Round Now Open" email template (for jury) */ function getRoundNowOpenTemplate( name: string, roundName: string, projectCount: number, deadline?: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const openBanner = `

Evaluation Round

${roundName} is Now Open

` const deadlineBox = deadline ? `

Deadline

${deadline}

` : '' const content = ` ${sectionTitle(greeting)} ${openBanner} ${statCard('Projects to Evaluate', projectCount)} ${deadlineBox} ${paragraph('The evaluation round is now open. Please log in to the MOPC Portal to begin reviewing your assigned projects.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'Start Evaluating') : ''} ` return { subject: `${roundName} is Now Open - ${projectCount} Projects Await`, html: getEmailWrapper(content), text: ` ${greeting} ${roundName} is now open for evaluation. You have ${projectCount} project${projectCount !== 1 ? 's' : ''} to evaluate. ${deadline ? `Deadline: ${deadline}` : ''} Please log in to the MOPC Portal to begin reviewing your assigned projects. ${assignmentsUrl ? `Start evaluating: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "24 Hour Reminder" email template (for jury) */ function getReminder24HTemplate( name: string, pendingCount: number, roundName: string, deadline: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const urgentBox = `

⚠ 24 Hours Remaining

` const content = ` ${sectionTitle(greeting)} ${urgentBox} ${paragraph(`This is a reminder that ${roundName} closes in 24 hours.`)} ${statCard('Pending Evaluations', pendingCount)} ${infoBox(`Deadline: ${deadline}`, 'warning')} ${paragraph('Please complete your remaining evaluations before the deadline to ensure your feedback is included in the selection process.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'Complete Evaluations') : ''} ` return { subject: `Reminder: ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} due in 24 hours`, html: getEmailWrapper(content), text: ` ${greeting} This is a reminder that ${roundName} closes in 24 hours. You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''}. Deadline: ${deadline} Please complete your remaining evaluations before the deadline. ${assignmentsUrl ? `Complete evaluations: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "3 Days Remaining" email template (for jury) */ function getReminder3DaysTemplate( name: string, pendingCount: number, roundName: string, deadline: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const urgentBox = `

⚠ 3 Days Remaining

` const content = ` ${sectionTitle(greeting)} ${urgentBox} ${paragraph(`This is a reminder that ${roundName} closes in 3 days.`)} ${statCard('Pending Evaluations', pendingCount)} ${infoBox(`Deadline: ${deadline}`, 'warning')} ${paragraph('Please plan to complete your remaining evaluations before the deadline to ensure your feedback is included in the selection process.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'Complete Evaluations') : ''} ` return { subject: `Reminder: ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} due in 3 days`, html: getEmailWrapper(content), text: ` ${greeting} This is a reminder that ${roundName} closes in 3 days. You have ${pendingCount} pending evaluation${pendingCount !== 1 ? 's' : ''}. Deadline: ${deadline} Please plan to complete your remaining evaluations before the deadline. ${assignmentsUrl ? `Complete evaluations: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "1 Hour Reminder" email template (for jury) */ function getReminder1HTemplate( name: string, pendingCount: number, roundName: string, deadline: string, assignmentsUrl?: string ): EmailTemplate { const greeting = name ? `${name},` : 'Attention,' const urgentBanner = `

Urgent

1 Hour Remaining

` const content = ` ${sectionTitle(greeting)} ${urgentBanner} ${paragraph(`${roundName} closes in 1 hour.`)} ${statCard('Evaluations Still Pending', pendingCount)} ${paragraph('Please submit your remaining evaluations immediately to ensure they are counted.')} ${assignmentsUrl ? ctaButton(assignmentsUrl, 'Submit Now') : ''} ` return { subject: `URGENT: ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} due in 1 hour`, html: getEmailWrapper(content), text: ` URGENT ${roundName} closes in 1 hour! You have ${pendingCount} evaluation${pendingCount !== 1 ? 's' : ''} still pending. Please submit your remaining evaluations immediately. ${assignmentsUrl ? `Submit now: ${assignmentsUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Award Voting Open" email template (for jury) */ function getAwardVotingOpenTemplate( name: string, awardName: string, finalistCount: number, deadline?: string, votingUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const awardBanner = `

🏆

Special Award

${awardName}

` const content = ` ${sectionTitle(greeting)} ${awardBanner} ${paragraph(`Voting is now open for the ${awardName}.`)} ${statCard('Finalists', finalistCount)} ${deadline ? infoBox(`Voting closes: ${deadline}`, 'warning') : ''} ${paragraph('Please review the finalist projects and cast your vote for the most deserving recipient.')} ${votingUrl ? ctaButton(votingUrl, 'Cast Your Vote') : ''} ` return { subject: `Vote Now: ${awardName}`, html: getEmailWrapper(content), text: ` ${greeting} Voting is now open for the ${awardName}. ${finalistCount} finalists are competing for this award. ${deadline ? `Voting closes: ${deadline}` : ''} Please review the finalist projects and cast your vote. ${votingUrl ? `Cast your vote: ${votingUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Mentee Assigned" email template (for mentor) */ function getMenteeAssignedTemplate( name: string, projectName: string, teamLeadName: string, teamLeadEmail?: string, projectUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const projectCard = `

Your New Mentee

${projectName}

Team Lead: ${teamLeadName}${teamLeadEmail ? ` (${teamLeadEmail})` : ''}

` const content = ` ${sectionTitle(greeting)} ${paragraph('You have been assigned as a mentor to a new project in the Monaco Ocean Protection Challenge.')} ${projectCard} ${paragraph('As a mentor, you play a crucial role in guiding this team toward success. Please reach out to introduce yourself and schedule your first meeting.')} ${infoBox('Your expertise and guidance can make a significant impact on their ocean protection initiative.', 'info')} ${projectUrl ? ctaButton(projectUrl, 'View Project Details') : ''} ` return { subject: `New Mentee Assignment: "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} You have been assigned as a mentor to a new project in the Monaco Ocean Protection Challenge. Project: ${projectName} Team Lead: ${teamLeadName}${teamLeadEmail ? ` (${teamLeadEmail})` : ''} Please reach out to introduce yourself and schedule your first meeting. ${projectUrl ? `View project: ${projectUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Mentee Advanced" email template (for mentor) */ function getMenteeAdvancedTemplate( name: string, projectName: string, roundName: string, nextRoundName?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' const content = ` ${sectionTitle(greeting)} ${infoBox('Great news about your mentee!', 'success')} ${paragraph(`Your mentee project "${projectName}" has advanced to the next stage!`)} ${statCard('Advanced From', roundName)} ${nextRoundName ? paragraph(`They will now compete in ${nextRoundName}.`) : ''} ${paragraph('Your guidance is making a difference. Continue supporting the team as they progress in the competition.')} ` return { subject: `Your Mentee Advanced: "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} Great news! Your mentee project "${projectName}" has advanced to the next stage. Advanced from: ${roundName} ${nextRoundName ? `Now competing in: ${nextRoundName}` : ''} Your guidance is making a difference. Continue supporting the team as they progress. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Mentee Won" email template (for mentor) */ function getMenteeWonTemplate( name: string, projectName: string, awardName: string ): EmailTemplate { const greeting = name ? `Congratulations ${name}!` : 'Congratulations!' const trophyBanner = `

🏆

Your Mentee Won!

` const content = ` ${sectionTitle(greeting)} ${trophyBanner} ${paragraph(`Your mentee project "${projectName}" has won the ${awardName}!`)} ${infoBox('Your mentorship played a vital role in their success.', 'success')} ${paragraph('Thank you for your dedication and support. The impact of your guidance extends beyond this competition.')} ` return { subject: `Your Mentee Won: "${projectName}" - ${awardName}`, html: getEmailWrapper(content), text: ` ${greeting} Your mentee project "${projectName}" has won the ${awardName}! Your mentorship played a vital role in their success. Thank you for your dedication and support. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "New Application" email template (for admins) */ function getNewApplicationTemplate( projectName: string, applicantName: string, applicantEmail: string, programName: string, reviewUrl?: string ): EmailTemplate { const applicationCard = `

New Application

${projectName}

Applicant: ${applicantName} (${applicantEmail})

` const content = ` ${sectionTitle('New Application Received')} ${paragraph(`A new application has been submitted to ${programName}.`)} ${applicationCard} ${reviewUrl ? ctaButton(reviewUrl, 'Review Application') : ''} ` return { subject: `New Application: "${projectName}"`, html: getEmailWrapper(content), text: ` New Application Received A new application has been submitted to ${programName}. Project: ${projectName} Applicant: ${applicantName} (${applicantEmail}) ${reviewUrl ? `Review application: ${reviewUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Project Advanced" notification email template */ export function getAdvancementNotificationTemplate( name: string, projectName: string, fromRoundName: string, toRoundName: string, customMessage?: string, accountUrl?: string, ): EmailTemplate { const greeting = name ? `Congratulations ${name}!` : 'Congratulations!' const celebrationBanner = `

Great News

Your project has advanced!

` const escapedMessage = customMessage ? customMessage .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
') : null const content = ` ${sectionTitle(greeting)} ${celebrationBanner} ${infoBox(`"${projectName}"`, 'success')} ${infoBox(`Advanced from ${fromRoundName} to ${toRoundName}`, 'info')} ${ escapedMessage ? `
${escapedMessage}
` : paragraph('Our team will be in touch with more details about the next phase.') } ${accountUrl ? ctaButton(accountUrl, 'Create Your Account') : ctaButton('/applicant', 'View Your Dashboard')} ` return { subject: `Your project has advanced: "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} Your project has advanced! Project: ${projectName} Advanced from: ${fromRoundName} To: ${toRoundName} ${customMessage || 'Our team will be in touch with more details about the next phase.'} ${accountUrl ? `Create your account: ${getBaseUrl()}${accountUrl}` : `Visit your dashboard: ${getBaseUrl()}/applicant`} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Project Not Advanced" (rejection) notification email template */ export function getRejectionNotificationTemplate( name: string, projectName: string, roundName: string, customMessage?: string ): EmailTemplate { const greeting = name ? `Dear ${name},` : 'Dear Applicant,' const escapedMessage = customMessage ? customMessage .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
') : null const content = ` ${sectionTitle(greeting)} ${paragraph(`Thank you for your participation in ${roundName} with your project "${projectName}".`)} ${infoBox('After careful review by our jury, we regret to inform you that your project was not selected to advance at this stage.', 'info')} ${ escapedMessage ? `
${escapedMessage}
` : '' } ${paragraph('We encourage you to continue developing your ocean protection initiative and to apply again in future editions. Your commitment to protecting our oceans is valuable and appreciated.')}

Thank you for being part of the Monaco Ocean Protection Challenge community.

` return { subject: `Update on your application: "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} Thank you for your participation in ${roundName} with your project "${projectName}". After careful review by our jury, we regret to inform you that your project was not selected to advance at this stage. ${customMessage || ''} We encourage you to continue developing your ocean protection initiative and to apply again in future editions. Thank you for being part of the Monaco Ocean Protection Challenge community. --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate "Selected for Special Award" notification email template */ export function getAwardSelectionNotificationTemplate( name: string, projectName: string, awardName: string, customMessage?: string, accountUrl?: string, ): EmailTemplate { const greeting = name ? `Dear ${name},` : 'Dear Applicant,' const celebrationBanner = `

Congratulations

Your project has been selected!

` const escapedMessage = customMessage ? customMessage .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
') : null const content = ` ${sectionTitle(greeting)} ${celebrationBanner} ${infoBox(`"${projectName}" has been selected for the ${awardName}`, 'success')} ${ escapedMessage ? `
${escapedMessage}
` : paragraph('Our team will be in touch with more details about this award and next steps.') } ${accountUrl ? ctaButton(accountUrl, 'Create Your Account') : ctaButton('/applicant', 'View Your Dashboard')} ` return { subject: `Your project has been selected for ${awardName}: "${projectName}"`, html: getEmailWrapper(content), text: ` ${greeting} Your project has been selected! Project: ${projectName} Award: ${awardName} ${customMessage || 'Our team will be in touch with more details about this award and next steps.'} ${accountUrl ? `Create your account: ${getBaseUrl()}${accountUrl}` : `Visit your dashboard: ${getBaseUrl()}/applicant`} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Generate a preview HTML wrapper for admin email previews */ export function getEmailPreviewHtml(subject: string, body: string): string { const formattedBody = body .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
') const content = ` ${sectionTitle(subject)}
${formattedBody}
` return getEmailWrapper(content) } /** * Template registry mapping notification types to template generators */ type TemplateGenerator = (context: NotificationEmailContext) => EmailTemplate export const NOTIFICATION_EMAIL_TEMPLATES: Record = { // Team/Applicant templates ADVANCED_SEMIFINAL: (ctx) => getAdvancedSemifinalTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.programName as string) || 'MOPC', ctx.metadata?.nextSteps as string | undefined ), ADVANCED_FINAL: (ctx) => getAdvancedFinalTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.programName as string) || 'MOPC', ctx.metadata?.nextSteps as string | undefined ), MENTOR_ASSIGNED: (ctx) => getMentorAssignedTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.mentorName as string) || 'Your Mentor', ctx.metadata?.mentorBio as string | undefined ), NOT_SELECTED: (ctx) => getNotSelectedTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.roundName as string) || 'this round', ctx.linkUrl, ctx.metadata?.encouragement as string | undefined ), WINNER_ANNOUNCEMENT: (ctx) => getWinnerAnnouncementTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.awardName as string) || 'the Award', ctx.metadata?.prizeDetails as string | undefined ), // Jury templates ASSIGNED_TO_PROJECT: (ctx) => getAssignedToProjectTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Project', (ctx.metadata?.roundName as string) || 'this round', ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), COI_REASSIGNED: (ctx) => getCOIReassignedTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Project', (ctx.metadata?.roundName as string) || 'this round', ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), MANUAL_REASSIGNED: (ctx) => getManualReassignedTemplate( ctx.name || '', (ctx.metadata?.projectNames as string[]) || [(ctx.metadata?.projectName as string) || 'Project'], (ctx.metadata?.roundName as string) || 'this round', ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), DROPOUT_REASSIGNED: (ctx) => getDropoutReassignedTemplate( ctx.name || '', (ctx.metadata?.projectNames as string[]) || [(ctx.metadata?.projectName as string) || 'Project'], (ctx.metadata?.roundName as string) || 'this round', (ctx.metadata?.droppedJurorName as string) || 'a fellow juror', ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), BATCH_ASSIGNED: (ctx) => getBatchAssignedTemplate( ctx.name || '', (ctx.metadata?.projectCount as number) || 1, (ctx.metadata?.roundName as string) || 'this round', ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), ROUND_NOW_OPEN: (ctx) => getRoundNowOpenTemplate( ctx.name || '', (ctx.metadata?.roundName as string) || 'Evaluation Round', (ctx.metadata?.projectCount as number) || 0, ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), REMINDER_3_DAYS: (ctx) => getReminder3DaysTemplate( ctx.name || '', (ctx.metadata?.pendingCount as number) || 0, (ctx.metadata?.roundName as string) || 'this round', (ctx.metadata?.deadline as string) || 'Soon', ctx.linkUrl ), REMINDER_24H: (ctx) => getReminder24HTemplate( ctx.name || '', (ctx.metadata?.pendingCount as number) || 0, (ctx.metadata?.roundName as string) || 'this round', (ctx.metadata?.deadline as string) || 'Soon', ctx.linkUrl ), REMINDER_1H: (ctx) => getReminder1HTemplate( ctx.name || '', (ctx.metadata?.pendingCount as number) || 0, (ctx.metadata?.roundName as string) || 'this round', (ctx.metadata?.deadline as string) || 'Very Soon', ctx.linkUrl ), AWARD_VOTING_OPEN: (ctx) => getAwardVotingOpenTemplate( ctx.name || '', (ctx.metadata?.awardName as string) || 'Special Award', (ctx.metadata?.finalistCount as number) || 0, ctx.metadata?.deadline as string | undefined, ctx.linkUrl ), // Mentor templates MENTEE_ASSIGNED: (ctx) => getMenteeAssignedTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Project', (ctx.metadata?.teamLeadName as string) || 'Team Lead', ctx.metadata?.teamLeadEmail as string | undefined, ctx.linkUrl ), MENTEE_ADVANCED: (ctx) => getMenteeAdvancedTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Project', (ctx.metadata?.roundName as string) || 'this round', ctx.metadata?.nextRoundName as string | undefined ), MENTEE_WON: (ctx) => getMenteeWonTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Project', (ctx.metadata?.awardName as string) || 'Award' ), ADVANCEMENT_NOTIFICATION: (ctx) => getAdvancementNotificationTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.fromRoundName as string) || 'previous round', (ctx.metadata?.toRoundName as string) || 'next round', ctx.metadata?.customMessage as string | undefined, ctx.metadata?.accountUrl as string | undefined, ), REJECTION_NOTIFICATION: (ctx) => getRejectionNotificationTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.roundName as string) || 'this round', ctx.metadata?.customMessage as string | undefined ), AWARD_SELECTION_NOTIFICATION: (ctx) => getAwardSelectionNotificationTemplate( ctx.name || '', (ctx.metadata?.projectName as string) || 'Your Project', (ctx.metadata?.awardName as string) || 'Special Award', ctx.metadata?.customMessage as string | undefined, ctx.metadata?.accountUrl as string | undefined, ), // Admin templates NEW_APPLICATION: (ctx) => getNewApplicationTemplate( (ctx.metadata?.projectName as string) || 'New Project', (ctx.metadata?.applicantName as string) || 'Applicant', (ctx.metadata?.applicantEmail as string) || '', (ctx.metadata?.programName as string) || 'MOPC', ctx.linkUrl ), } /** * Send styled notification email using the appropriate template */ export async function sendStyledNotificationEmail( email: string, name: string, type: string, context: NotificationEmailContext, subjectOverride?: string ): Promise { // Safety net: always ensure linkUrl is absolute before passing to templates const safeContext = { ...context, linkUrl: ensureAbsoluteUrl(context.linkUrl), } const templateGenerator = NOTIFICATION_EMAIL_TEMPLATES[type] let template: EmailTemplate if (templateGenerator) { // Use styled template template = templateGenerator({ ...safeContext, name }) // Apply subject override if provided if (subjectOverride) { template.subject = subjectOverride } } else { // Fall back to generic template template = getNotificationEmailTemplate( name, subjectOverride || safeContext.title, safeContext.message, safeContext.linkUrl ) } const { transporter, from } = await getTransporter() await transporter.sendMail({ from, to: email, subject: template.subject, text: template.text, html: template.html, }) } // ============================================================================= // 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) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, 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, expiryHours?: number ): Promise { const template = getGenericInvitationTemplate(name || '', url, role, expiryHours) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, 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) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, 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 ) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, 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 ) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, 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 Portal.')} ${infoBox('If you received this, your email configuration is working correctly!', 'success')}

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

` const { transporter, from } = await getTransporter() await transporter.sendMail({ from, to: toEmail, subject: 'MOPC Portal - Test Email', text: 'This is a test email from the MOPC Portal. 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 { const { transporter } = await getTransporter() await transporter.verify() return true } catch { return false } } /** * Send application confirmation email to applicant */ export async function sendApplicationConfirmationEmail( email: string, applicantName: string, projectName: string, programName: string, customMessage?: string ): Promise { const template = getApplicationConfirmationTemplate( applicantName, projectName, programName, customMessage ) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Send team member invite email */ export async function sendTeamMemberInviteEmail( email: string, memberName: string, projectName: string, teamLeadName: string, inviteUrl: string ): Promise { const template = getTeamMemberInviteTemplate( memberName, projectName, teamLeadName, inviteUrl ) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, to: email, subject: template.subject, text: template.text, html: template.html, }) } /** * Generate notification email template */ function getNotificationEmailTemplate( name: string, subject: string, body: string, linkUrl?: string ): EmailTemplate { const greeting = name ? `Hello ${name},` : 'Hello,' // Format body text preserving line breaks const formattedBody = body .replace(/&/g, '&') .replace(//g, '>') .replace(/\n/g, '
') const content = ` ${sectionTitle(greeting)}
${formattedBody}
${linkUrl ? ctaButton(linkUrl, 'View Details') : ''}

You received this email because of your notification preferences on the MOPC Portal.

` return { subject, html: getEmailWrapper(content), text: ` ${greeting} ${body} ${linkUrl ? `View details: ${linkUrl}` : ''} --- Monaco Ocean Protection Challenge Together for a healthier ocean. `, } } /** * Send notification email (triggered by in-app notification system) */ export async function sendNotificationEmail( email: string, name: string, subject: string, body: string, linkUrl?: string ): Promise { const template = getNotificationEmailTemplate(name, subject, body, ensureAbsoluteUrl(linkUrl)) const { transporter, from } = await getTransporter() await transporter.sendMail({ from, to: email, subject: template.subject, text: template.text, html: template.html, }) }