@@ -309,41 +309,119 @@ export const fetchSmtpBouncesTask: TaskConfig<'fetchSmtpBounces'> = {
309309
310310 try {
311311 const rawEmail = await pop3 . RETR ( messageId ) ;
312- const parsedEmail = await simpleParser ( String ( rawEmail ) ) ;
312+ const rawEmailString = String ( rawEmail ) ;
313+ const parsedEmail = await simpleParser ( rawEmailString ) ;
313314
315+ const { isSuccess, dsnString } = determineDeliveryStatus ( parsedEmail ) ;
314316 const envId = getOriginalEnvelopeId ( parsedEmail ) ;
315317
318+ let matched = false ;
319+
316320 // Standard Payload ID length check (24 chars for ObjectID)
317- if ( envId ?. length === 24 ) {
318- const { isSuccess, dsnString } = determineDeliveryStatus ( parsedEmail ) ;
319- const matched = await updateTrackingRecords (
321+ if ( typeof envId === 'string' && envId . length === 24 ) {
322+ matched = await updateTrackingRecords (
320323 payload ,
321324 envId ,
322325 isSuccess ,
323326 dsnString ,
324- String ( rawEmail ) ,
327+ rawEmailString ,
325328 ) ;
329+ }
326330
327- if ( matched ) {
328- logger . info ( `Processed bounce for submission/outgoing email ${ envId } successfully.` ) ;
331+ // Fallback matching if Original-Envelope-Id matching fails or doesn't exist
332+ if ( ! matched ) {
333+ const possibleIds = new Set < string > ( ) ;
329334
330- // Only delete if successfully processed and matches an ID in our database
331- await pop3 . DELE ( messageId ) ;
335+ const textForRegex = typeof parsedEmail . text === 'string' ? parsedEmail . text : '' ;
332336
333- // Clear failure tracking if successful
334- if ( trackingRecord ?. id !== undefined ) {
335- await payload . delete ( {
336- collection : 'smtp-bounce-mail-tracking' ,
337- id : trackingRecord . id ,
337+ const extractMatches = ( regex : RegExp , sourceString : string ) : void => {
338+ let m ;
339+ // reset regex state if global
340+ regex . lastIndex = 0 ;
341+ while ( ( m = regex . exec ( sourceString ) ) !== null ) {
342+ if ( m [ 1 ] !== undefined ) possibleIds . add ( m [ 1 ] . trim ( ) ) ;
343+ }
344+ } ;
345+
346+ // Normalize extracted IDs by removing angle brackets and domain parts for robust comparison
347+ const messageIdRegex = / M e s s a g e - I D : \s * < ? ( [ ^ @ > \s ] + ) / gi;
348+ extractMatches ( messageIdRegex , rawEmailString ) ;
349+ extractMatches ( messageIdRegex , textForRegex ) ;
350+
351+ const queuedRegex = / q u e u e d a s \s * ( [ a - z A - Z 0 - 9 _ - ] + ) / gi;
352+ extractMatches ( queuedRegex , rawEmailString ) ;
353+ extractMatches ( queuedRegex , textForRegex ) ;
354+
355+ const postfixRegex = / X - P o s t f i x - Q u e u e - I D : \s * ( [ a - z A - Z 0 - 9 _ - ] + ) / gi;
356+ extractMatches ( postfixRegex , rawEmailString ) ;
357+ extractMatches ( postfixRegex , textForRegex ) ;
358+
359+ // Also check the Received header that contains the Queue ID before sending
360+ const receivedRegex = / w i t h E S M T P S A i d \s * ( [ a - z A - Z 0 - 9 _ - ] + ) / gi;
361+ extractMatches ( receivedRegex , rawEmailString ) ;
362+
363+ const extractedIds = [ ...possibleIds ] ;
364+
365+ if ( extractedIds . length > 0 ) {
366+ // Scan recent outgoing emails (up to 1000, within the last 30 days) for these IDs in their smtpResults
367+ const thirtyDaysAgo = new Date ( ) ;
368+ thirtyDaysAgo . setDate ( thirtyDaysAgo . getDate ( ) - 30 ) ;
369+
370+ const recentOutgoing = await payload . find ( {
371+ collection : 'outgoing-emails' ,
372+ where : {
373+ createdAt : {
374+ greater_than_equal : thirtyDaysAgo . toISOString ( ) ,
375+ } ,
376+ } ,
377+ limit : 1000 ,
378+ sort : '-createdAt' ,
379+ } ) ;
380+
381+ for ( const outgoingDocument of recentOutgoing . docs ) {
382+ const stringifiedResults = JSON . stringify ( outgoingDocument . smtpResults ?? [ ] ) ;
383+ const foundMatch = extractedIds . some ( ( id ) => {
384+ // Only match if the ID appears as a complete token, not as a substring
385+ const regex = new RegExp (
386+ `\\b${ id . replaceAll ( / [ . * + ? ^ $ { } ( ) | [ \\ ] \\ \\ ] / g, String . raw `\\$&` ) } \\b` ,
387+ ) ;
388+ return regex . test ( stringifiedResults ) ;
338389 } ) ;
390+
391+ if ( foundMatch ) {
392+ matched = await updateTrackingRecords (
393+ payload ,
394+ String ( outgoingDocument . id ) ,
395+ isSuccess ,
396+ dsnString ,
397+ rawEmailString ,
398+ ) ;
399+
400+ if ( matched ) {
401+ logger . info (
402+ `Fallback processed bounce for outgoing email ${ String ( outgoingDocument . id ) } using ID(s) ${ extractedIds . join ( ',' ) } ` ,
403+ ) ;
404+ break ;
405+ }
406+ }
339407 }
340- } else {
341- logger . info (
342- `Ignored message ${ messageId } as envId ${ envId } was not found in this instance.` ,
343- ) ;
408+ }
409+ }
410+ if ( matched ) {
411+ // Only delete if successfully processed and matches an ID in our database
412+ await pop3 . DELE ( messageId ) ;
413+
414+ // Clear failure tracking if successful
415+ if ( trackingRecord ?. id !== undefined ) {
416+ await payload . delete ( {
417+ collection : 'smtp-bounce-mail-tracking' ,
418+ id : trackingRecord . id ,
419+ } ) ;
344420 }
345421 } else {
346- logger . info ( `Ignored message ${ messageId } as it lacks our expected ID format.` ) ;
422+ logger . info (
423+ `Ignored message ${ messageId } as envId ${ envId } and fallback IDs were not found in this instance.` ,
424+ ) ;
347425 }
348426 } catch ( error : unknown ) {
349427 // We isolate individual message failures so the loop continues
0 commit comments