Skip to content

Commit 74f2a06

Browse files
authored
Version 0.38.1 (#946)
- fix: DSN matching for cases where Envelope is not sent in SMTP result
2 parents ad3068a + 1a4a7b8 commit 74f2a06

File tree

4 files changed

+109
-22
lines changed

4 files changed

+109
-22
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "conveniat27",
3-
"version": "0.38.0",
3+
"version": "0.38.1",
44
"description": "The official website of conveniat27",
55
"license": "MIT",
66
"author": "Cyrill Püntener v/o JPG (cyrill.puentener@cevi.ch)",

src/features/payload-cms/payload-cms/collections/outgoing-emails.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,14 @@ export const OutgoingEmails: CollectionConfig = {
151151
},
152152
],
153153
},
154+
{
155+
name: 'createdAt',
156+
type: 'date',
157+
index: true,
158+
admin: {
159+
readOnly: true,
160+
position: 'sidebar',
161+
},
162+
},
154163
],
155164
};

src/features/payload-cms/payload-cms/tasks/fetch-smtp-bounces.ts

Lines changed: 97 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = /Message-ID:\s*<?([^@>\s]+)/gi;
348+
extractMatches(messageIdRegex, rawEmailString);
349+
extractMatches(messageIdRegex, textForRegex);
350+
351+
const queuedRegex = /queued as\s*([a-zA-Z0-9_-]+)/gi;
352+
extractMatches(queuedRegex, rawEmailString);
353+
extractMatches(queuedRegex, textForRegex);
354+
355+
const postfixRegex = /X-Postfix-Queue-ID:\s*([a-zA-Z0-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 = /with ESMTPSA id\s*([a-zA-Z0-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

src/features/payload-cms/payload-types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2572,8 +2572,8 @@ export interface OutgoingEmail {
25722572
| boolean
25732573
| null;
25742574
rawDsnEmail?: string | null;
2575-
updatedAt: string;
25762575
createdAt: string;
2576+
updatedAt: string;
25772577
}
25782578
/**
25792579
* This is a collection of automatically created search results. These results are used by the global site search and will be updated automatically as documents in the CMS are created or updated.
@@ -3961,8 +3961,8 @@ export interface OutgoingEmailsSelect<T extends boolean = true> {
39613961
smtpResults?: T;
39623962
rawSmtpResults?: T;
39633963
rawDsnEmail?: T;
3964-
updatedAt?: T;
39653964
createdAt?: T;
3965+
updatedAt?: T;
39663966
}
39673967
/**
39683968
* This interface was referenced by `Config`'s JSON-Schema

0 commit comments

Comments
 (0)