diff --git a/src/lib/email.ts b/src/lib/email.ts index d16b235..7932487 100644 --- a/src/lib/email.ts +++ b/src/lib/email.ts @@ -1,17 +1,57 @@ import nodemailer from 'nodemailer' +import type { Transporter } from 'nodemailer' +import { prisma } from '@/lib/prisma' -// 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, - }, -}) +// Cached transporter and config hash to detect changes +let cachedTransporter: Transporter | null = null +let cachedConfigHash = '' +let cachedFrom = '' -// Default sender +/** + * 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'] }, + }, + 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 || '' + const from = db.email_from || process.env.EMAIL_FROM || 'MOPC Platform ' + + // 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 Platform ' // ============================================================================= @@ -69,7 +109,7 @@ function getEmailWrapper(content: string): string { - + @@ -79,31 +119,30 @@ function getEmailWrapper(content: string): string { - -
+ ${content}
- - - - - - - + - - - -
- Monaco Ocean Protection Challenge -
-

- Together for a healthier ocean -

-

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

+
+ + + + + + + +
+ Monaco Ocean Protection Challenge +
+

+ Together for a healthier ocean +

+

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

+
@@ -444,9 +483,10 @@ export async function sendMagicLinkEmail( ): 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: defaultFrom, + from, to: email, subject: template.subject, text: template.text, @@ -464,9 +504,10 @@ export async function sendInvitationEmail( role: string ): Promise { const template = getGenericInvitationTemplate(name || '', url, role) + const { transporter, from } = await getTransporter() await transporter.sendMail({ - from: defaultFrom, + from, to: email, subject: template.subject, text: template.text, @@ -484,9 +525,10 @@ export async function sendJuryInvitationEmail( roundName: string ): Promise { const template = getJuryInvitationTemplate(name || '', url, roundName) + const { transporter, from } = await getTransporter() await transporter.sendMail({ - from: defaultFrom, + from, to: email, subject: template.subject, text: template.text, @@ -512,9 +554,10 @@ export async function sendEvaluationReminderEmail( deadline, assignmentsUrl ) + const { transporter, from } = await getTransporter() await transporter.sendMail({ - from: defaultFrom, + from, to: email, subject: template.subject, text: template.text, @@ -540,9 +583,10 @@ export async function sendAnnouncementEmail( ctaText, ctaUrl ) + const { transporter, from } = await getTransporter() await transporter.sendMail({ - from: defaultFrom, + from, to: email, subject: template.subject, text: template.text, @@ -563,9 +607,10 @@ export async function sendTestEmail(toEmail: string): Promise { Sent at ${new Date().toISOString()}

` + const { transporter, from } = await getTransporter() await transporter.sendMail({ - from: defaultFrom, + from, 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.', @@ -582,6 +627,7 @@ export async function sendTestEmail(toEmail: string): Promise { */ export async function verifyEmailConnection(): Promise { try { + const { transporter } = await getTransporter() await transporter.verify() return true } catch {