From 18556c818dce97494c9f77b745814f8fc77bb8fd Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Fri, 10 Apr 2026 12:58:04 +0200 Subject: [PATCH] fix(mail): harden mailer and fix missing notification recipients --- app/actions/contact.ts | 14 ++++++++---- lib/env.ts | 2 +- lib/mail/mailer.ts | 49 +++++++++++++++++++++++++++++++++++------- package.json | 2 +- 4 files changed, 53 insertions(+), 14 deletions(-) diff --git a/app/actions/contact.ts b/app/actions/contact.ts index 3f2e3783..688af6db 100644 --- a/app/actions/contact.ts +++ b/app/actions/contact.ts @@ -95,6 +95,7 @@ export async function sendContactFormAction(formData: FormData) { ); if (!isTestSubmission) { + logger.info('Sending internal notification', { recipients: config.mail.recipients }); const notificationResult = await sendEmail({ replyTo: email, subject: notificationSubject, @@ -106,14 +107,18 @@ export async function sendContactFormAction(formData: FormData) { messageId: notificationResult.messageId, }); } else { - logger.error('Notification email FAILED', { + logger.error('Notification email DELIVERY FAILED', { error: notificationResult.error, subject: notificationSubject, - email, + recipients: config.mail.recipients, }); services.errors.captureException( new Error(`Notification email failed: ${notificationResult.error}`), - { action: 'sendContactFormAction_notification', email }, + { + action: 'sendContactFormAction_notification', + email, + recipients: config.mail.recipients + }, ); } } else { @@ -130,6 +135,7 @@ export async function sendContactFormAction(formData: FormData) { ); if (!isTestSubmission) { + logger.info('Sending customer confirmation', { to: email }); const confirmationResult = await sendEmail({ to: email, subject: confirmationSubject, @@ -141,7 +147,7 @@ export async function sendContactFormAction(formData: FormData) { messageId: confirmationResult.messageId, }); } else { - logger.error('Confirmation email FAILED', { + logger.error('Confirmation email DELIVERY FAILED', { error: confirmationResult.error, subject: confirmationSubject, to: email, diff --git a/lib/env.ts b/lib/env.ts index f6a4ed1d..b989cc84 100644 --- a/lib/env.ts +++ b/lib/env.ts @@ -42,7 +42,7 @@ const envExtension = { MAIL_USERNAME: z.string().optional(), MAIL_PASSWORD: z.string().optional(), MAIL_FROM: z.string().optional(), - MAIL_RECIPIENTS: z.string().optional(), + MAIL_RECIPIENTS: z.string().trim().optional(), }; /** diff --git a/lib/mail/mailer.ts b/lib/mail/mailer.ts index ffe8f5d1..d766b7dc 100644 --- a/lib/mail/mailer.ts +++ b/lib/mail/mailer.ts @@ -32,16 +32,27 @@ interface SendEmailOptions { } export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions) { - const recipients = to || config.mail.recipients; const logger = getServerAppServices().logger.child({ component: 'mailer' }); + + // Resolve recipients: priority to 'to' override, fallback to global MAIL_RECIPIENTS + const resolvedTo = to || config.mail.recipients; + + // Normalize recipients (handle arrays or comma-strings) + const recipients = Array.isArray(resolvedTo) + ? resolvedTo.join(', ') + : (resolvedTo?.toString() || ''); - if (!recipients) { - logger.error('No email recipients configured (MAIL_RECIPIENTS is empty and no "to" provided)', { subject }); + if (!recipients || recipients.trim() === '') { + logger.error('Email delivery ABORTED: No recipients configured', { + subject, + providedTo: to, + configRecipients: config.mail.recipients + }); return { success: false as const, error: 'No recipients configured' }; } if (!config.mail.from) { - logger.error('MAIL_FROM is not configured — cannot send email', { subject, recipients }); + logger.error('Email delivery ABORTED: MAIL_FROM is missing', { subject, recipients }); return { success: false as const, error: 'MAIL_FROM is not configured' }; } @@ -53,14 +64,36 @@ export async function sendEmail({ to, replyTo, subject, html }: SendEmailOptions html, }; - try { - const info = await getTransporter().sendMail(mailOptions); - logger.info('Email sent successfully', { messageId: info.messageId, subject, recipients }); + const transporter = getTransporter(); + logger.info('Attempting to send email via SMTP', { + host: config.mail.host, + subject, + recipients, + hasReplyTo: !!replyTo + }); + + const info = await transporter.sendMail(mailOptions); + + logger.info('Email sent successfully', { + messageId: info.messageId, + subject, + recipients, + response: info.response + }); + return { success: true, messageId: info.messageId }; } catch (error) { const errorMsg = error instanceof Error ? error.message : String(error); - logger.error('Error sending email', { error: errorMsg, subject, recipients }); + logger.error('SMTP Transport failed', { + error: errorMsg, + subject, + recipients, + config: { + host: config.mail.host, + user: config.mail.user ? '***' : 'not set' + } + }); return { success: false, error: errorMsg }; } } diff --git a/package.json b/package.json index 08d44fe7..846d1ff0 100644 --- a/package.json +++ b/package.json @@ -160,7 +160,7 @@ "prepare": "husky", "preinstall": "npx only-allow pnpm" }, - "version": "2.3.11", + "version": "2.3.12", "pnpm": { "onlyBuiltDependencies": [ "@parcel/watcher",