Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
2 changes: 1 addition & 1 deletion build/notifications/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
24 changes: 22 additions & 2 deletions src/notifications/herald.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <user@example.com>",
from: "Omni <team@omni.dev>",
subject: "Hello",
body: "<p>Hi</p>",
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 <team@omni.dev>");
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();
Expand Down Expand Up @@ -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,
Expand All @@ -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 <orders@send.omni.dev>");
expect(body.to).toBe("buyer@example.com");
});
});
Expand Down
12 changes: 8 additions & 4 deletions src/notifications/herald.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <email>` 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 <email>` display form there and renders the
* display name in the From header.
*/
const toBareEmail = (address: string): string => {
const match = address.match(/<([^>]+)>/);
Expand Down Expand Up @@ -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 <email>` 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
Expand Down
Loading