From 8e6ba7c07d683a88f9211ce13cbb5cd027150135 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Fri, 30 Oct 2026 22:55:07 +0000 Subject: [PATCH] fix(mail): block restricted sender before provider fallback --- src/mail.ts | 15 +++++++++++++++ test/mail.test.ts | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/mail.ts b/src/mail.ts index 6ed2c44..889e706 100644 --- a/src/mail.ts +++ b/src/mail.ts @@ -43,6 +43,19 @@ export class MailError extends Error { } } +export function assertEmailSenderAllowed(...addresses: string[]): void { + const blocked = new Set( + (process.env.EMAIL_QUEUE_BLOCKED_SENDERS ?? '') + .split(',') + .map((address) => address.trim().toLowerCase()) + .filter(Boolean), + ); + const senders = addresses.map((value) => (/<([^>]+)>/.exec(value)?.[1] ?? value).trim().toLowerCase()); + if (senders.some((address) => blocked.has(address))) { + throw new MailError('Outbound sending from this mailbox is paused for administrator review.'); + } +} + // --------------------------------------------------------------------------- // Providers // --------------------------------------------------------------------------- @@ -1619,6 +1632,7 @@ export function smtpTransport(account: Account) { } export const sendViaSmtp: SmtpSender = async (account, outgoing) => { + assertEmailSenderAllowed(account.email, account.user, outgoing.from); const transporter = smtpTransport(account); const info = await transporter.sendMail({ from: outgoing.from, @@ -1703,6 +1717,7 @@ export async function sendMail( isBlocked?: HostBlockCheck; } = {}, ): Promise { + assertEmailSenderAllowed(account.email, account.user, outgoing.from); const choice = chooseTransport(account, options.resendKey, options.via); const smtp = options.smtp ?? sendViaSmtp; const resend = options.resend ?? resendSender(); diff --git a/test/mail.test.ts b/test/mail.test.ts index c427118..56a2833 100644 --- a/test/mail.test.ts +++ b/test/mail.test.ts @@ -631,6 +631,26 @@ describe('sendMail', () => { expect(isTransportFailure('550 5.1.1 recipient rejected')).toBe(false); expect(isTransportFailure('Resend refused the message: domain is not verified')).toBe(false); }); + + it('stops a blocked mailbox before SMTP or Resend fallback', async () => { + const previous = process.env.EMAIL_QUEUE_BLOCKED_SENDERS; + process.env.EMAIL_QUEUE_BLOCKED_SENDERS = 'ettinger@gmail.com'; + const calls: string[] = []; + try { + await expect(sendMail(account({ email: 'ettinger@gmail.com', user: 'ettinger@gmail.com' }), { + ...outgoing, + from: 'Me ', + }, { + resendKey: 'key', + smtp: async () => { calls.push('smtp'); return 'must-not-send'; }, + resend: async () => { calls.push('resend'); return 'must-not-send'; }, + })).rejects.toThrow(/paused for administrator review/); + expect(calls).toEqual([]); + } finally { + if (previous === undefined) delete process.env.EMAIL_QUEUE_BLOCKED_SENDERS; + else process.env.EMAIL_QUEUE_BLOCKED_SENDERS = previous; + } + }); }); describe('Resend', () => {