From 5ef97d987e291e5bbe141cd8fec206ce4a0e1b9b Mon Sep 17 00:00:00 2001 From: Vijay Budhram Date: Wed, 17 Dec 2025 13:37:08 -0500 Subject: [PATCH] updates --- .../lib/email/delivery-delay.ts | 191 +++++++++++------- .../test/local/email/delivery-delay.js | 18 ++ 2 files changed, 134 insertions(+), 75 deletions(-) diff --git a/packages/fxa-auth-server/lib/email/delivery-delay.ts b/packages/fxa-auth-server/lib/email/delivery-delay.ts index 341faabbb5..3c7d7dc34d 100644 --- a/packages/fxa-auth-server/lib/email/delivery-delay.ts +++ b/packages/fxa-auth-server/lib/email/delivery-delay.ts @@ -2,10 +2,23 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/** + * Handles AWS SES Delivery Delay notifications from SQS. + * + * Delivery delays are TRANSIENT failures where email delivery is temporarily delayed + * but may eventually succeed. This differs from bounces which are PERMANENT failures. + * Delays can occur due to mailbox full, temporary network issues, rate limiting, etc. + * + * Integration Requirements: + * - AWS SES must be configured to publish DeliveryDelay notifications to an SQS queue + * - Environment variable DELIVERY_DELAY_QUEUE_URL must point to the queue + * - SQS queue must have proper IAM permissions for the auth-server to consume messages + */ + import { StatsD } from 'hot-shots'; import { Logger } from 'mozlog'; import { EventEmitter } from 'events'; -const utils = require('./utils/helpers'); +import * as utils from './utils/helpers'; interface SESMailHeader { name: string; @@ -25,6 +38,10 @@ interface DelayedRecipient { diagnosticCode?: string; } +/** + * AWS SES Delivery Delay types as documented in: + * https://docs.aws.amazon.com/ses/latest/dg/event-publishing-retrieving-sns-contents.html + */ interface DeliveryDelay { delayType: | 'InternalFailure' @@ -58,83 +75,107 @@ interface SQSReceiver extends EventEmitter { export = function (log: Logger, statsd: StatsD) { return function start(deliveryDelayQueue: SQSReceiver) { async function handleDeliveryDelay(message: SESDeliveryDelayMessage) { - utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'deliveryDelay'); + try { + utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'deliveryDelay'); - statsd.increment('email.deliveryDelay.message', { - delayType: message?.deliveryDelay?.delayType || 'none', - hasExpiration: String(!!message?.deliveryDelay?.expirationTime), - template: utils.getHeaderValue('X-Template-Name', message) || 'none', - }); + // Track message age to monitor how long delays persist + let messageAgeSeconds = 0; + if (message.mail?.timestamp) { + const mailTimestamp = new Date(message.mail.timestamp).getTime(); + const now = Date.now(); + messageAgeSeconds = Math.floor((now - mailTimestamp) / 1000); + statsd.timing('email.deliveryDelay.ageSeconds', messageAgeSeconds); + } - let recipients: DelayedRecipient[] = []; - if ( - message.deliveryDelay && - (message.eventType === 'DeliveryDelay' || - message.notificationType === 'DeliveryDelay') - ) { - recipients = message.deliveryDelay.delayedRecipients || []; + statsd.increment('email.deliveryDelay.message', { + delayType: message?.deliveryDelay?.delayType || 'none', + hasExpiration: String(!!message?.deliveryDelay?.expirationTime), + template: utils.getHeaderValue('X-Template-Name', message) || 'none', + }); + + let recipients: DelayedRecipient[] = []; + if ( + message.deliveryDelay && + (message.eventType === 'DeliveryDelay' || + message.notificationType === 'DeliveryDelay') + ) { + recipients = message.deliveryDelay.delayedRecipients || []; + } + + const templateName = utils.getHeaderValue('X-Template-Name', message); + const language = utils.getHeaderValue('Content-Language', message); + const delayType = message.deliveryDelay?.delayType; + const expirationTime = message.deliveryDelay?.expirationTime; + const reportingMTA = message.deliveryDelay?.reportingMTA; + const timestamp = message.deliveryDelay?.timestamp; + + for (const recipient of recipients) { + const email = recipient.emailAddress; + const emailDomain = utils.getAnonymizedEmailDomain(email); + const logData: { + email: string; + domain: string; + delayType?: DeliveryDelay['delayType']; + status?: string; + diagnosticCode?: string; + template?: string; + lang?: string; + expirationTime?: string; + reportingMTA?: string; + timestamp?: string; + messageAgeSeconds?: number; + } = { + email: email, + domain: emailDomain, + delayType: delayType, + }; + + if (recipient.status) { + logData.status = recipient.status; + } + if (recipient.diagnosticCode) { + logData.diagnosticCode = recipient.diagnosticCode; + } + + if (templateName) { + logData.template = templateName; + } + + if (language) { + logData.lang = language; + } + + if (expirationTime) { + logData.expirationTime = expirationTime; + } + + if (reportingMTA) { + logData.reportingMTA = reportingMTA; + } + + if (timestamp) { + logData.timestamp = timestamp; + } + + if (messageAgeSeconds > 0) { + logData.messageAgeSeconds = messageAgeSeconds; + } + + utils.logAccountEventFromMessage(message, 'emailDelayed'); + + log.info('handleDeliveryDelay', logData); + } + + message.del(); + } catch (err) { + // Log error but still delete message to prevent infinite retry loop + log.error('handleDeliveryDelay.error', { + err: err, + messageId: message?.mail?.messageId, + }); + statsd.increment('email.deliveryDelay.error'); + message.del(); } - - const templateName = utils.getHeaderValue('X-Template-Name', message); - const language = utils.getHeaderValue('Content-Language', message); - const delayType = message.deliveryDelay?.delayType; - const expirationTime = message.deliveryDelay?.expirationTime; - const reportingMTA = message.deliveryDelay?.reportingMTA; - const timestamp = message.deliveryDelay?.timestamp; - - for (const recipient of recipients) { - const email = recipient.emailAddress; - const emailDomain = utils.getAnonymizedEmailDomain(email); - const logData: { - email: string; - domain: string; - delayType?: DeliveryDelay['delayType']; - status?: string; - diagnosticCode?: string; - template?: string; - lang?: string; - expirationTime?: string; - reportingMTA?: string; - timestamp?: string; - } = { - email: email, - domain: emailDomain, - delayType: delayType, - }; - - if (recipient.status) { - logData.status = recipient.status; - } - if (recipient.diagnosticCode) { - logData.diagnosticCode = recipient.diagnosticCode; - } - - if (templateName) { - logData.template = templateName; - } - - if (language) { - logData.lang = language; - } - - if (expirationTime) { - logData.expirationTime = expirationTime; - } - - if (reportingMTA) { - logData.reportingMTA = reportingMTA; - } - - if (timestamp) { - logData.timestamp = timestamp; - } - - utils.logAccountEventFromMessage(message, 'emailDelayed'); - - log.info('handleDeliveryDelay', logData); - } - - message.del(); } deliveryDelayQueue.on('data', handleDeliveryDelay); diff --git a/packages/fxa-auth-server/test/local/email/delivery-delay.js b/packages/fxa-auth-server/test/local/email/delivery-delay.js index b8e65d35e2..2ff461c8e6 100644 --- a/packages/fxa-auth-server/test/local/email/delivery-delay.js +++ b/packages/fxa-auth-server/test/local/email/delivery-delay.js @@ -189,4 +189,22 @@ describe('delivery delay messages', () => { assert.equal(log.info.callCount, 0); sinon.assert.calledOnce(mockMsg.del); }); + + it('should handle errors and still delete message', async () => { + const log = mockLog(); + const statsd = mockStatsd(); + const mockMsg = createDeliveryDelayMessage(); + + sandbox.stub(emailHelpers, 'getAnonymizedEmailDomain').throws(new Error('Test error')); + + await mockedDeliveryDelay(log, statsd).handleDeliveryDelay(mockMsg); + + sinon.assert.calledWith(log.error, 'handleDeliveryDelay.error'); + assert.include(log.error.args[0][1], { + messageId: 'test-message-id', + }); + + sinon.assert.calledWith(statsd.increment, 'email.deliveryDelay.error'); + sinon.assert.calledOnce(mockMsg.del); + }); });