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+
518import { StatsD } from 'hot-shots' ;
619import { Logger } from 'mozlog' ;
720import { EventEmitter } from 'events' ;
8- const utils = require ( './utils/helpers' ) ;
21+ import * as utils from './utils/helpers' ;
922
1023interface 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+ */
2845interface DeliveryDelay {
2946 delayType :
3047 | 'InternalFailure'
@@ -58,83 +75,107 @@ interface SQSReceiver extends EventEmitter {
5875export = 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 ) ;
0 commit comments