From 23eb18483846c14a20b4053881a9a165ad360cc8 Mon Sep 17 00:00:00 2001 From: Brian Cooper Date: Sun, 19 Jul 2026 23:50:44 -0600 Subject: [PATCH] fix(notifications): preserve the From display name for Herald Herald renders a `Name ` display name in the From header (it parses the bare address for the envelope itself), but the provider stripped `from` down to the bare address before sending, so a friendly From never reached the header. Pass `from` through unchanged; recipient fields (to/cc/bcc/replyTo) are still reduced to bare addresses, which Herald requires. --- build/index.js | 2 +- build/notifications/index.js | 2 +- src/notifications/herald.test.ts | 24 ++++++++++++++++++++++-- src/notifications/herald.ts | 12 ++++++++---- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/build/index.js b/build/index.js index e2e552e..bc194ce 100644 --- a/build/index.js +++ b/build/index.js @@ -79965,7 +79965,7 @@ class HeraldNotificationProvider { for (const [index, recipient] of recipients.entries()) { const body = { to: toBareEmail(recipient), - from: toBareEmail(params.from ?? this.config.defaultFrom), + from: params.from ?? this.config.defaultFrom, subject: params.subject, html: params.body, ...params.html ? {} : { text: params.body }, diff --git a/build/notifications/index.js b/build/notifications/index.js index 0e4a686..bab445f 100644 --- a/build/notifications/index.js +++ b/build/notifications/index.js @@ -110,7 +110,7 @@ class HeraldNotificationProvider { for (const [index, recipient] of recipients.entries()) { const body = { to: toBareEmail(recipient), - from: toBareEmail(params.from ?? this.config.defaultFrom), + from: params.from ?? this.config.defaultFrom, subject: params.subject, html: params.body, ...params.html ? {} : { text: params.body }, diff --git a/src/notifications/herald.test.ts b/src/notifications/herald.test.ts index d5ab0e0..c8b3619 100644 --- a/src/notifications/herald.test.ts +++ b/src/notifications/herald.test.ts @@ -69,6 +69,24 @@ describe("HeraldNotificationProvider.sendEmail", () => { expect(sent.text).toBeUndefined(); }); + it("preserves a display-name From (does not strip it to a bare address)", async () => { + const { calls } = stubFetch(200, { id: "m_from", status: "queued" }); + const provider = makeProvider(); + + await provider.sendEmail({ + to: "Display Name ", + from: "Omni ", + subject: "Hello", + body: "

Hi

", + html: true, + }); + + const sent = JSON.parse(calls[0]?.init?.body as string); + // from keeps its display name; recipient fields are still bare + expect(sent.from).toBe("Omni "); + expect(sent.to).toBe("user@example.com"); + }); + it("returns failure without throwing on a non-2xx response", async () => { stubFetch(502, { error: "engine down" }); const provider = makeProvider(); @@ -187,7 +205,7 @@ describe("HeraldNotificationProvider.sendEmail", () => { expect(result.error).toBeTruthy(); }); - it("strips display names so Herald receives bare from/to addresses", async () => { + it("strips recipient display names but keeps the From display name", async () => { const { calls } = stubFetch(200, { id: "m_5", status: "queued" }); const provider = new HeraldNotificationProvider({ apiKey: API_KEY, @@ -207,7 +225,9 @@ describe("HeraldNotificationProvider.sendEmail", () => { from: string; to: string; }; - expect(body.from).toBe("orders@send.omni.dev"); + // from keeps its display name (Herald renders it in the header); recipients + // are still reduced to bare addresses + expect(body.from).toBe("Omni Orders "); expect(body.to).toBe("buyer@example.com"); }); }); diff --git a/src/notifications/herald.ts b/src/notifications/herald.ts index 19fa7eb..3981b5a 100644 --- a/src/notifications/herald.ts +++ b/src/notifications/herald.ts @@ -59,9 +59,11 @@ type HeraldMessageBody = { }; /** - * Extract a bare email address from an RFC 5322 address. Herald validates - * `to`/`from` as bare emails and rejects the `Name ` display form, so - * strip any display name down to the address inside the angle brackets. + * Extract a bare email address from an RFC 5322 address. Herald validates the + * recipient fields (`to`/`cc`/`bcc`/`replyTo`) as bare emails, so strip any + * display name down to the address inside the angle brackets. `from` is left + * intact: Herald accepts a `Name ` display form there and renders the + * display name in the From header. */ const toBareEmail = (address: string): string => { const match = address.match(/<([^>]+)>/); @@ -99,7 +101,9 @@ class HeraldNotificationProvider implements NotificationProvider { for (const [index, recipient] of recipients.entries()) { const body: HeraldMessageBody = { to: toBareEmail(recipient), - from: toBareEmail(params.from ?? this.config.defaultFrom), + // Pass `from` through so a `Name ` display name survives to the + // From header; Herald parses the bare address for the envelope itself + from: params.from ?? this.config.defaultFrom, subject: params.subject, // Herald requires a non-empty html part; for plain-text bodies we set // both html and text so the text part is preserved