diff --git a/firestore-send-email/functions/__tests__/nodemailer-sendgrid/index.test.ts b/firestore-send-email/functions/__tests__/nodemailer-sendgrid/index.test.ts index 3ec2c7753..bab3e3445 100644 --- a/firestore-send-email/functions/__tests__/nodemailer-sendgrid/index.test.ts +++ b/firestore-send-email/functions/__tests__/nodemailer-sendgrid/index.test.ts @@ -408,6 +408,62 @@ describe("SendGridTransport", () => { }); }); + test("send: forwards customArgs object", async () => { + const transport = new SendGridTransport({ apiKey: "KEY" }); + const fakeMail: any = { + normalize: (cb: any) => + cb(null, { + from: { address: "a@x.com" }, + to: [{ address: "b@x.com" }], + subject: "Custom args test", + customArgs: { campaign: "welcome", source: "signup" }, + }), + }; + const cb = jest.fn(); + + transport.send(fakeMail, cb); + await new Promise((r) => setImmediate(r)); + + const sent = (sgMail.send as jest.Mock).mock.calls[0][0]; + expect(sent.customArgs).toEqual({ campaign: "welcome", source: "signup" }); + expect(cb).toHaveBeenCalledWith(null, { + messageId: null, + queueId: "test-message-id", + accepted: ["b@x.com"], + rejected: [], + pending: [], + response: "status=202", + }); + }); + + test("send: forwards ipPoolName string", async () => { + const transport = new SendGridTransport({ apiKey: "KEY" }); + const fakeMail: any = { + normalize: (cb: any) => + cb(null, { + from: { address: "a@x.com" }, + to: [{ address: "b@x.com" }], + subject: "IP pool test", + ipPoolName: "transactional", + }), + }; + const cb = jest.fn(); + + transport.send(fakeMail, cb); + await new Promise((r) => setImmediate(r)); + + const sent = (sgMail.send as jest.Mock).mock.calls[0][0]; + expect(sent.ipPoolName).toEqual("transactional"); + expect(cb).toHaveBeenCalledWith(null, { + messageId: null, + queueId: "test-message-id", + accepted: ["b@x.com"], + rejected: [], + pending: [], + response: "status=202", + }); + }); + test("send: deduplicates and normalizes email addresses", async () => { const transport = new SendGridTransport(); const source = { diff --git a/firestore-send-email/functions/__tests__/validation.test.ts b/firestore-send-email/functions/__tests__/validation.test.ts index 1d74fca27..e54990b3c 100644 --- a/firestore-send-email/functions/__tests__/validation.test.ts +++ b/firestore-send-email/functions/__tests__/validation.test.ts @@ -212,6 +212,28 @@ describe("validatePayload", () => { }); }); + it("should validate a SendGrid payload with customArgs", () => { + const validPayload = { + to: "test@example.com", + sendGrid: { + templateId: "d-template-id", + customArgs: { campaign: "welcome", source: "signup" }, + }, + }; + expect(() => validatePayload(validPayload)).not.toThrow(); + }); + + it("should validate a SendGrid payload with ipPoolName", () => { + const validPayload = { + to: "test@example.com", + sendGrid: { + templateId: "d-template-id", + ipPoolName: "transactional", + }, + }; + expect(() => validatePayload(validPayload)).not.toThrow(); + }); + it("should validate a SendGrid payload with only mailSettings", () => { const validPayload = { to: "test@example.com", @@ -255,6 +277,26 @@ describe("validatePayload", () => { ); }); + it("should throw ValidationError for SendGrid customArgs with non-string values", () => { + const invalidPayload = { + to: "test@example.com", + sendGrid: { + customArgs: { campaign: 123 }, + }, + }; + expect(() => validatePayload(invalidPayload)).toThrow(ValidationError); + }); + + it("should throw ValidationError for SendGrid ipPoolName with non-string value", () => { + const invalidPayload = { + to: "test@example.com", + sendGrid: { + ipPoolName: 123, + }, + }; + expect(() => validatePayload(invalidPayload)).toThrow(ValidationError); + }); + it("should throw ValidationError for custom template without name", () => { const invalidPayload = { to: "test@example.com", diff --git a/firestore-send-email/functions/src/index.ts b/firestore-send-email/functions/src/index.ts index 1611a051e..97cc48e09 100644 --- a/firestore-send-email/functions/src/index.ts +++ b/firestore-send-email/functions/src/index.ts @@ -179,6 +179,8 @@ async function deliver(ref: DocumentReference): Promise { templateId: payload.sendGrid?.templateId, dynamicTemplateData: payload.sendGrid?.dynamicTemplateData, mailSettings: payload.sendGrid?.mailSettings, + customArgs: payload.sendGrid?.customArgs, + ipPoolName: payload.sendGrid?.ipPoolName, }; logs.info("Sending via transport.sendMail()", { mailOptions }); diff --git a/firestore-send-email/functions/src/nodemailer-sendgrid/index.ts b/firestore-send-email/functions/src/nodemailer-sendgrid/index.ts index 9d7d24c78..e02807aa3 100644 --- a/firestore-send-email/functions/src/nodemailer-sendgrid/index.ts +++ b/firestore-send-email/functions/src/nodemailer-sendgrid/index.ts @@ -152,6 +152,12 @@ export class SendGridTransport { case "mailSettings": msg.mailSettings = source.mailSettings; break; + case "customArgs": + msg.customArgs = source.customArgs; + break; + case "ipPoolName": + msg.ipPoolName = source.ipPoolName; + break; default: msg[key] = source[key]; diff --git a/firestore-send-email/functions/src/nodemailer-sendgrid/types.ts b/firestore-send-email/functions/src/nodemailer-sendgrid/types.ts index f8e6e626c..3f7ed10c5 100644 --- a/firestore-send-email/functions/src/nodemailer-sendgrid/types.ts +++ b/firestore-send-email/functions/src/nodemailer-sendgrid/types.ts @@ -46,6 +46,8 @@ export interface MailSource { templateId?: string; dynamicTemplateData?: Record; mailSettings?: Record; + customArgs?: Record; + ipPoolName?: string; [key: string]: unknown; } @@ -91,5 +93,7 @@ export interface SendGridMessage { templateId?: string; dynamicTemplateData?: Record; mailSettings?: Record; + customArgs?: Record; + ipPoolName?: string; [key: string]: unknown; } diff --git a/firestore-send-email/functions/src/types.ts b/firestore-send-email/functions/src/types.ts index e03e74e74..1b476fb23 100644 --- a/firestore-send-email/functions/src/types.ts +++ b/firestore-send-email/functions/src/types.ts @@ -86,6 +86,8 @@ export interface QueuePayload { templateId?: string; dynamicTemplateData?: { [key: string]: any }; mailSettings?: { [key: string]: any }; + customArgs?: Record; + ipPoolName?: string; }; to: string[]; toUids?: string[]; @@ -127,4 +129,6 @@ export interface ExtendedSendMailOptions extends nodemailer.SendMailOptions { templateId?: string; dynamicTemplateData?: Record; mailSettings?: Record; + customArgs?: Record; + ipPoolName?: string; } diff --git a/firestore-send-email/functions/src/validation.ts b/firestore-send-email/functions/src/validation.ts index 0d13af707..98f4e4ca7 100644 --- a/firestore-send-email/functions/src/validation.ts +++ b/firestore-send-email/functions/src/validation.ts @@ -93,6 +93,8 @@ const sendGridSchema = z templateId: z.string().optional(), dynamicTemplateData: z.record(z.any()).optional(), mailSettings: z.record(z.any()).optional(), + customArgs: z.record(z.string()).optional(), + ipPoolName: z.string().optional(), }) .refine( (data) => {