fix(mail-ingest): read on to To when a delivery header names no address

Stalwart writes Delivered-To as the mailbox itself, its sub-address dropped,
so a forwarded mail to trek+<token>@ was refused as unknown_address: the
first delivery header won even though it carried no token. The headers are
now read in their order of trust, and the first that names a live address
decides.
This commit is contained in:
Maurice
2026-11-13 20:57:55 +01:00
parent 7a40225852
commit a67361c6bd
3 changed files with 44 additions and 10 deletions
@@ -138,6 +138,25 @@ describe('a mail to several addresses', () => {
expect(await findRow(orm(), MailIngestMessages, { user: mia.id })).not.toBeNull();
expect(await findRow(orm(), MailIngestMessages, { user: noah.id })).toBeNull();
});
it('reads on to To when the delivery header names the box without its token, as Stalwart writes it', async () => {
const { user: mia } = await makeUser(orm());
const miaOwn = (await addresses().renewOwn(mia.id))?.address ?? '';
const box = miaOwn.replace(/\+[^@]+@/, '@');
const headers = [`Delivered-To: ${box}`, `To: <${miaOwn}>`, 'From: x@example.com'];
const raw = Buffer.from(
`${headers.join('\r\n')}\r\nSubject: WG: Booking\r\nMessage-ID: <fwd@x>\r\n\r\nBooking ABC123`,
);
expect(await t.get(MailIngestIntakeService).receive(raw, 'imap')).toEqual({ status: 'accepted' });
expect(await findRow(orm(), MailIngestMessages, { user: mia.id })).not.toBeNull();
const nobody = Buffer.from(`Delivered-To: ${box}\r\nTo: ${box}\r\nSubject: Hi\r\nMessage-ID: <none@x>\r\n\r\nHi`);
expect(await t.get(MailIngestIntakeService).receive(nobody, 'imap')).toEqual({
status: 'refused',
reason: 'unknown_address',
});
});
});
describe('a trip address', () => {