This commit is contained in:
Vijay Budhram 2025-12-17 13:37:08 -05:00
parent 0febaa2233
commit 5ef97d987e
No known key found for this signature in database
GPG Key ID: 9778545895B2532B
2 changed files with 134 additions and 75 deletions

View File

@ -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);

View File

@ -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);
});
});