Skip to content

Commit 5ef97d9

Browse files
committed
updates
1 parent 0febaa2 commit 5ef97d9

2 files changed

Lines changed: 130 additions & 71 deletions

File tree

packages/fxa-auth-server/lib/email/delivery-delay.ts

Lines changed: 112 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,23 @@
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
33
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
44

5+
/**
6+
* Handles AWS SES Delivery Delay notifications from SQS.
7+
*
8+
* Delivery delays are TRANSIENT failures where email delivery is temporarily delayed
9+
* but may eventually succeed. This differs from bounces which are PERMANENT failures.
10+
* Delays can occur due to mailbox full, temporary network issues, rate limiting, etc.
11+
*
12+
* Integration Requirements:
13+
* - AWS SES must be configured to publish DeliveryDelay notifications to an SQS queue
14+
* - Environment variable DELIVERY_DELAY_QUEUE_URL must point to the queue
15+
* - SQS queue must have proper IAM permissions for the auth-server to consume messages
16+
*/
17+
518
import { StatsD } from 'hot-shots';
619
import { Logger } from 'mozlog';
720
import { EventEmitter } from 'events';
8-
const utils = require('./utils/helpers');
21+
import * as utils from './utils/helpers';
922

1023
interface SESMailHeader {
1124
name: string;
@@ -25,6 +38,10 @@ interface DelayedRecipient {
2538
diagnosticCode?: string;
2639
}
2740

41+
/**
42+
* AWS SES Delivery Delay types as documented in:
43+
* https://docs.aws.amazon.com/ses/latest/dg/event-publishing-retrieving-sns-contents.html
44+
*/
2845
interface DeliveryDelay {
2946
delayType:
3047
| 'InternalFailure'
@@ -58,83 +75,107 @@ interface SQSReceiver extends EventEmitter {
5875
export = function (log: Logger, statsd: StatsD) {
5976
return function start(deliveryDelayQueue: SQSReceiver) {
6077
async function handleDeliveryDelay(message: SESDeliveryDelayMessage) {
61-
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'deliveryDelay');
62-
63-
statsd.increment('email.deliveryDelay.message', {
64-
delayType: message?.deliveryDelay?.delayType || 'none',
65-
hasExpiration: String(!!message?.deliveryDelay?.expirationTime),
66-
template: utils.getHeaderValue('X-Template-Name', message) || 'none',
67-
});
68-
69-
let recipients: DelayedRecipient[] = [];
70-
if (
71-
message.deliveryDelay &&
72-
(message.eventType === 'DeliveryDelay' ||
73-
message.notificationType === 'DeliveryDelay')
74-
) {
75-
recipients = message.deliveryDelay.delayedRecipients || [];
76-
}
77-
78-
const templateName = utils.getHeaderValue('X-Template-Name', message);
79-
const language = utils.getHeaderValue('Content-Language', message);
80-
const delayType = message.deliveryDelay?.delayType;
81-
const expirationTime = message.deliveryDelay?.expirationTime;
82-
const reportingMTA = message.deliveryDelay?.reportingMTA;
83-
const timestamp = message.deliveryDelay?.timestamp;
84-
85-
for (const recipient of recipients) {
86-
const email = recipient.emailAddress;
87-
const emailDomain = utils.getAnonymizedEmailDomain(email);
88-
const logData: {
89-
email: string;
90-
domain: string;
91-
delayType?: DeliveryDelay['delayType'];
92-
status?: string;
93-
diagnosticCode?: string;
94-
template?: string;
95-
lang?: string;
96-
expirationTime?: string;
97-
reportingMTA?: string;
98-
timestamp?: string;
99-
} = {
100-
email: email,
101-
domain: emailDomain,
102-
delayType: delayType,
103-
};
104-
105-
if (recipient.status) {
106-
logData.status = recipient.status;
107-
}
108-
if (recipient.diagnosticCode) {
109-
logData.diagnosticCode = recipient.diagnosticCode;
110-
}
111-
112-
if (templateName) {
113-
logData.template = templateName;
78+
try {
79+
utils.logErrorIfHeadersAreWeirdOrMissing(log, message, 'deliveryDelay');
80+
81+
// Track message age to monitor how long delays persist
82+
let messageAgeSeconds = 0;
83+
if (message.mail?.timestamp) {
84+
const mailTimestamp = new Date(message.mail.timestamp).getTime();
85+
const now = Date.now();
86+
messageAgeSeconds = Math.floor((now - mailTimestamp) / 1000);
87+
statsd.timing('email.deliveryDelay.ageSeconds', messageAgeSeconds);
11488
}
11589

116-
if (language) {
117-
logData.lang = language;
90+
statsd.increment('email.deliveryDelay.message', {
91+
delayType: message?.deliveryDelay?.delayType || 'none',
92+
hasExpiration: String(!!message?.deliveryDelay?.expirationTime),
93+
template: utils.getHeaderValue('X-Template-Name', message) || 'none',
94+
});
95+
96+
let recipients: DelayedRecipient[] = [];
97+
if (
98+
message.deliveryDelay &&
99+
(message.eventType === 'DeliveryDelay' ||
100+
message.notificationType === 'DeliveryDelay')
101+
) {
102+
recipients = message.deliveryDelay.delayedRecipients || [];
118103
}
119104

120-
if (expirationTime) {
121-
logData.expirationTime = expirationTime;
105+
const templateName = utils.getHeaderValue('X-Template-Name', message);
106+
const language = utils.getHeaderValue('Content-Language', message);
107+
const delayType = message.deliveryDelay?.delayType;
108+
const expirationTime = message.deliveryDelay?.expirationTime;
109+
const reportingMTA = message.deliveryDelay?.reportingMTA;
110+
const timestamp = message.deliveryDelay?.timestamp;
111+
112+
for (const recipient of recipients) {
113+
const email = recipient.emailAddress;
114+
const emailDomain = utils.getAnonymizedEmailDomain(email);
115+
const logData: {
116+
email: string;
117+
domain: string;
118+
delayType?: DeliveryDelay['delayType'];
119+
status?: string;
120+
diagnosticCode?: string;
121+
template?: string;
122+
lang?: string;
123+
expirationTime?: string;
124+
reportingMTA?: string;
125+
timestamp?: string;
126+
messageAgeSeconds?: number;
127+
} = {
128+
email: email,
129+
domain: emailDomain,
130+
delayType: delayType,
131+
};
132+
133+
if (recipient.status) {
134+
logData.status = recipient.status;
135+
}
136+
if (recipient.diagnosticCode) {
137+
logData.diagnosticCode = recipient.diagnosticCode;
138+
}
139+
140+
if (templateName) {
141+
logData.template = templateName;
142+
}
143+
144+
if (language) {
145+
logData.lang = language;
146+
}
147+
148+
if (expirationTime) {
149+
logData.expirationTime = expirationTime;
150+
}
151+
152+
if (reportingMTA) {
153+
logData.reportingMTA = reportingMTA;
154+
}
155+
156+
if (timestamp) {
157+
logData.timestamp = timestamp;
158+
}
159+
160+
if (messageAgeSeconds > 0) {
161+
logData.messageAgeSeconds = messageAgeSeconds;
162+
}
163+
164+
utils.logAccountEventFromMessage(message, 'emailDelayed');
165+
166+
log.info('handleDeliveryDelay', logData);
122167
}
123168

124-
if (reportingMTA) {
125-
logData.reportingMTA = reportingMTA;
126-
}
127-
128-
if (timestamp) {
129-
logData.timestamp = timestamp;
130-
}
131-
132-
utils.logAccountEventFromMessage(message, 'emailDelayed');
133-
134-
log.info('handleDeliveryDelay', logData);
169+
message.del();
170+
} catch (err) {
171+
// Log error but still delete message to prevent infinite retry loop
172+
log.error('handleDeliveryDelay.error', {
173+
err: err,
174+
messageId: message?.mail?.messageId,
175+
});
176+
statsd.increment('email.deliveryDelay.error');
177+
message.del();
135178
}
136-
137-
message.del();
138179
}
139180

140181
deliveryDelayQueue.on('data', handleDeliveryDelay);

packages/fxa-auth-server/test/local/email/delivery-delay.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,22 @@ describe('delivery delay messages', () => {
189189
assert.equal(log.info.callCount, 0);
190190
sinon.assert.calledOnce(mockMsg.del);
191191
});
192+
193+
it('should handle errors and still delete message', async () => {
194+
const log = mockLog();
195+
const statsd = mockStatsd();
196+
const mockMsg = createDeliveryDelayMessage();
197+
198+
sandbox.stub(emailHelpers, 'getAnonymizedEmailDomain').throws(new Error('Test error'));
199+
200+
await mockedDeliveryDelay(log, statsd).handleDeliveryDelay(mockMsg);
201+
202+
sinon.assert.calledWith(log.error, 'handleDeliveryDelay.error');
203+
assert.include(log.error.args[0][1], {
204+
messageId: 'test-message-id',
205+
});
206+
207+
sinon.assert.calledWith(statsd.increment, 'email.deliveryDelay.error');
208+
sinon.assert.calledOnce(mockMsg.del);
209+
});
192210
});

0 commit comments

Comments
 (0)