diff --git a/packages/lib/getReplyToHeader.test.ts b/packages/lib/getReplyToHeader.test.ts new file mode 100644 index 0000000000..0b558d948a --- /dev/null +++ b/packages/lib/getReplyToHeader.test.ts @@ -0,0 +1,92 @@ +import { describe, expect, it, vi } from "vitest"; + +import { getReplyToHeader } from "./getReplyToHeader"; + +/** + * RFC 5322 (Internet Message Format) specifies that the Reply-To header must be + * a comma-separated list of addresses, not an array. Many SMTP servers reject arrays. + * Spec: https://datatracker.ietf.org/doc/html/rfc5322#section-3.6.2 + */ + +vi.mock("./getReplyToEmail", () => ({ + getReplyToEmail: vi.fn((calEvent, excludeOrganizerEmail) => { + if (excludeOrganizerEmail) return null; + return calEvent.organizer?.email || null; + }), +})); + +const createMockCalEvent = (organizerEmail: string) => ({ + organizer: { email: organizerEmail }, + hideOrganizerEmail: false, +}); + +describe("getReplyToHeader", () => { + describe("return type", () => { + it("always returns replyTo as a string, never an array", () => { + const calEvent = createMockCalEvent("organizer@test.com"); + const result = getReplyToHeader(calEvent as any, ["attendee1@test.com", "attendee2@test.com"]); + + expect(result).toHaveProperty("replyTo"); + expect(typeof result.replyTo).toBe("string"); + // Should NOT be an array + expect(Array.isArray(result.replyTo)).toBe(false); + }); + }); + + describe("with single email", () => { + it("returns single email as string", () => { + const calEvent = createMockCalEvent("organizer@test.com"); + const result = getReplyToHeader(calEvent as any); + + expect(result).toEqual({ replyTo: "organizer@test.com" }); + }); + + it("returns additionalEmail as string when provided alone", () => { + const calEvent = createMockCalEvent("organizer@test.com"); + const result = getReplyToHeader(calEvent as any, "additional@test.com", true); + + expect(result).toEqual({ replyTo: "additional@test.com" }); + }); + }); + + describe("with multiple emails", () => { + it("returns comma-separated string for multiple emails", () => { + const calEvent = createMockCalEvent("organizer@test.com"); + const result = getReplyToHeader(calEvent as any, ["attendee1@test.com", "attendee2@test.com"]); + + expect(result).toEqual({ + replyTo: "attendee1@test.com, attendee2@test.com, organizer@test.com", + }); + }); + + it("returns comma-separated string when additionalEmails is array", () => { + const calEvent = createMockCalEvent("organizer@test.com"); + const result = getReplyToHeader(calEvent as any, ["a@test.com", "b@test.com", "c@test.com"], true); + + expect(result).toEqual({ + replyTo: "a@test.com, b@test.com, c@test.com", + }); + }); + }); + + describe("with no emails", () => { + it("returns empty object when no emails available", () => { + const calEvent = { organizer: { email: "" }, hideOrganizerEmail: true }; + const result = getReplyToHeader(calEvent as any, undefined, true); + + expect(result).toEqual({}); + }); + }); + + describe("SMTP compatibility", () => { + it("produces RFC 5322 compliant Reply-To header format", () => { + const calEvent = createMockCalEvent("organizer@test.com"); + const result = getReplyToHeader(calEvent as any, ["a@test.com", "b@test.com"]); + + // RFC 5322 specifies comma-separated list for multiple addresses + expect(result.replyTo).toMatch(/^[^,]+, [^,]+, [^,]+$/); + expect(result.replyTo).not.toContain("["); + expect(result.replyTo).not.toContain("]"); + }); + }); +}); diff --git a/packages/lib/getReplyToHeader.ts b/packages/lib/getReplyToHeader.ts index 36254dfe54..cff6e458e1 100644 --- a/packages/lib/getReplyToHeader.ts +++ b/packages/lib/getReplyToHeader.ts @@ -26,6 +26,6 @@ export function getReplyToHeader( return {}; } - const replyTo = emailArray.length === 1 ? emailArray[0] : emailArray; + const replyTo = emailArray.join(", "); return { replyTo }; }