Skip to content

Commit cd40ab4

Browse files
committed
fallback to bounce.to, improve formatting, add bounce test
1 parent ca9a0de commit cd40ab4

2 files changed

Lines changed: 108 additions & 9 deletions

File tree

plugins/core/email-bounce.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,19 @@
33
const os = require('os');
44
const MimeNode = require('nodemailer/lib/mime-node');
55

6+
function formatRecipients(value) {
7+
return []
8+
.concat(value || [])
9+
.map(entry => ((entry && entry.address) || entry || '').toString().trim())
10+
.filter(entry => entry);
11+
}
12+
613
function getSingleRecipient(value) {
7-
return (
8-
[]
9-
.concat(value || [])
10-
.map(entry => ((entry && entry.address) || entry || '').toString().trim())
11-
.find(entry => entry) || ''
12-
);
14+
return formatRecipients(value).find(entry => entry) || '';
1315
}
1416

1517
function getFailedRecipient(bounce) {
16-
return getSingleRecipient(bounce.recipient || bounce.envelope?.to);
18+
return getSingleRecipient(bounce.recipient || bounce.envelope?.to || bounce.to);
1719
}
1820

1921
function getBounceSubject(bounce, isDelayed) {
@@ -41,7 +43,8 @@ module.exports.init = function (app, done) {
4143
let to = bounce.from;
4244
let sendingZone = cfg.sendingZone || app.config.sendingZone;
4345
let failedRecipient = getFailedRecipient(bounce);
44-
let originalRecipientsText = `\nOriginal message recipients:\n ${bounce.to}\n`;
46+
let originalRecipients = formatRecipients(bounce.to);
47+
let originalRecipientsText = originalRecipients.length ? `\nOriginal message recipients:\n ${originalRecipients.join('\n ')}\n` : '';
4548

4649
let rootNode = new MimeNode('multipart/report; report-type=delivery-status');
4750

@@ -51,7 +54,9 @@ module.exports.init = function (app, done) {
5154
rootNode.setHeader('From', fromAddress);
5255
rootNode.setHeader('To', to);
5356
rootNode.setHeader('X-Sending-Zone', sendingZone);
54-
rootNode.setHeader('X-Failed-Recipients', bounce.to);
57+
if (originalRecipients.length) {
58+
rootNode.setHeader('X-Failed-Recipients', originalRecipients.join(', '));
59+
}
5560
rootNode.setHeader('Auto-Submitted', 'auto-replied');
5661
rootNode.setHeader('Subject', getBounceSubject(bounce, isDelayed));
5762

test/email-bounce-test.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
'use strict';
2+
3+
const emailBounce = require('../plugins/core/email-bounce');
4+
5+
function createHeaders() {
6+
return {
7+
getFirst(key) {
8+
if (key === 'Message-ID') {
9+
return '<test-message@example.com>';
10+
}
11+
return '';
12+
},
13+
get(key) {
14+
if (key === 'Received') {
15+
return [];
16+
}
17+
return [];
18+
},
19+
build() {
20+
return 'Message-ID: <test-message@example.com>\r\n';
21+
}
22+
};
23+
}
24+
25+
function createApp() {
26+
const hooks = new Map();
27+
return {
28+
hooks,
29+
config: {
30+
zoneConfig: {},
31+
mailerDaemon: 'Mailer Daemon <mailer-daemon@[HOSTNAME]>',
32+
sendingZone: 'default'
33+
},
34+
addHook(name, handler) {
35+
hooks.set(name, handler);
36+
},
37+
getQueue() {
38+
return {
39+
generateId(callback) {
40+
callback(null, 'test-bounce-id');
41+
}
42+
};
43+
},
44+
logger: {
45+
info() {},
46+
error() {}
47+
},
48+
remotelog() {}
49+
};
50+
}
51+
52+
module.exports['Email bounce falls back to bounce.to for failed recipient'] = test => {
53+
const app = createApp();
54+
emailBounce.init(app, () => {});
55+
56+
const queueBounce = app.hooks.get('queue:bounce');
57+
58+
const bounce = {
59+
id: 'message-id',
60+
sessionId: 'session-id',
61+
zone: 'default',
62+
from: 'sender@example.com',
63+
to: [{ address: 'failed@example.com' }],
64+
seq: '001',
65+
headers: createHeaders(),
66+
name: 'mx.example.com',
67+
arrivalDate: new Date('2026-03-25T10:00:00Z').toISOString(),
68+
response: '550 5.1.1 No such user'
69+
};
70+
71+
const maildrop = {
72+
add(envelope, stream, callback) {
73+
const chunks = [];
74+
stream.on('data', chunk => chunks.push(chunk));
75+
stream.on('error', callback);
76+
stream.on('end', () => {
77+
const message = Buffer.concat(chunks).toString();
78+
79+
test.equal(envelope.to, 'sender@example.com');
80+
test.ok(/Subject: Delivery Status Notification \(Failure: failed@example\.com\)/.test(message));
81+
test.ok(/Final-Recipient: rfc822; failed@example\.com/.test(message));
82+
test.ok(/X-Failed-Recipients: failed@example\.com/.test(message));
83+
test.ok(/Original message recipients:\s+failed@example\.com/.test(message));
84+
85+
callback();
86+
});
87+
}
88+
};
89+
90+
queueBounce(bounce, maildrop, err => {
91+
test.ifError(err);
92+
test.done();
93+
});
94+
};

0 commit comments

Comments
 (0)