Use DB settings for SMTP and unify email design to all-white
Build and Push Docker Image / build (push) Successful in 7m43s
Details
Build and Push Docker Image / build (push) Successful in 7m43s
Details
- Read SMTP settings from database (admin panel) with env var fallback - Cache transporter and rebuild when settings change - Remove dark blue footer from emails; use single white content box with logo and tagline at the bottom separated by a subtle border Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
43680d4173
commit
5aedade41d
124
src/lib/email.ts
124
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<string, string> = {}
|
||||
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 <noreply@monaco-opc.com>'
|
||||
|
||||
// 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 <noreply@monaco-opc.com>'
|
||||
|
||||
// =============================================================================
|
||||
|
|
@ -69,7 +109,7 @@ function getEmailWrapper(content: string): string {
|
|||
|
||||
<!-- Content Box -->
|
||||
<tr>
|
||||
<td style="background-color: ${BRAND.white}; border-radius: 12px 12px 0 0; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);">
|
||||
<td style="background-color: ${BRAND.white}; border-radius: 12px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<!-- Small Logo Header -->
|
||||
<tr>
|
||||
|
|
@ -79,31 +119,30 @@ function getEmailWrapper(content: string): string {
|
|||
</tr>
|
||||
<!-- Email Content -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px 40px 40px;">
|
||||
<td style="padding: 0 40px 32px 40px;">
|
||||
${content}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Footer with Big Logo -->
|
||||
<tr>
|
||||
<td style="background-color: ${BRAND.darkBlue}; border-radius: 0 0 12px 12px; padding: 32px 40px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="${getBigLogoUrl()}" alt="Monaco Ocean Protection Challenge" width="200" height="auto" style="display: block; border: 0; max-width: 200px; margin-bottom: 16px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<p style="color: #8aa3ad; font-size: 13px; margin: 0; line-height: 1.5;">
|
||||
Together for a healthier ocean
|
||||
</p>
|
||||
<p style="color: #6b8a94; font-size: 12px; margin: 12px 0 0 0;">
|
||||
© ${new Date().getFullYear()} Monaco Ocean Protection Challenge
|
||||
</p>
|
||||
<td style="padding: 0 40px 32px 40px; border-top: 1px solid #e5e7eb;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding-top: 24px;">
|
||||
<img src="${getBigLogoUrl()}" alt="Monaco Ocean Protection Challenge" width="180" height="auto" style="display: block; border: 0; max-width: 180px; margin-bottom: 12px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<p style="color: ${BRAND.textMuted}; font-size: 13px; margin: 0; line-height: 1.5;">
|
||||
Together for a healthier ocean
|
||||
</p>
|
||||
<p style="color: ${BRAND.textMuted}; font-size: 12px; margin: 8px 0 0 0;">
|
||||
© ${new Date().getFullYear()} Monaco Ocean Protection Challenge
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
@ -444,9 +483,10 @@ export async function sendMagicLinkEmail(
|
|||
): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<boolean> {
|
|||
Sent at ${new Date().toISOString()}
|
||||
</p>
|
||||
`
|
||||
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<boolean> {
|
|||
*/
|
||||
export async function verifyEmailConnection(): Promise<boolean> {
|
||||
try {
|
||||
const { transporter } = await getTransporter()
|
||||
await transporter.verify()
|
||||
return true
|
||||
} catch {
|
||||
|
|
|
|||
Loading…
Reference in New Issue