From 784a392b40865dc4fac2d6079e709eb06900f8c3 Mon Sep 17 00:00:00 2001 From: Bjarn Bronsveld Date: Thu, 13 Aug 2026 11:39:42 +0200 Subject: [PATCH] Complete email settings, attachments, and batch idempotency --- src/endpoints/email.spec.ts | 64 ++++++++++++++++++++++++++++++++++++- src/endpoints/email.ts | 30 +++++++++++++++-- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/src/endpoints/email.spec.ts b/src/endpoints/email.spec.ts index 5560f0a..d555a90 100644 --- a/src/endpoints/email.spec.ts +++ b/src/endpoints/email.spec.ts @@ -150,7 +150,10 @@ describe('EmailEndpoint', () => { }); it('should set custom headers', () => { - const headers = { 'X-Custom': 'Value', 'X-Another': 'Another Value' }; + const headers = { + 'Message-ID': '', + 'X-LM-Preserve-Message-ID': 'true', + }; const result = emailEndpoint.headers(headers); expect(result).toBe(emailEndpoint); @@ -209,6 +212,47 @@ describe('EmailEndpoint', () => { }); }); + it('should add attachments with content_id and content_type', () => { + const result = emailEndpoint.attach('invite.ics', 'base64calendar', 'invite', 'text/calendar'); + + expect(result).toBe(emailEndpoint); + + return emailEndpoint.send().then(() => { + expect(client.post).toHaveBeenCalledWith( + '/send', + expect.objectContaining({ + attachments: [ + { + filename: 'invite.ics', + content: 'base64calendar', + content_id: 'invite', + content_type: 'text/calendar', + }, + ], + }), + undefined + ); + }); + }); + + it('should set per-email settings', () => { + const settings = { + track_opens: false, + track_clicks: true, + tls: 'enforced' as const, + }; + + expect(emailEndpoint.settings(settings)).toBe(emailEndpoint); + + return emailEndpoint.send().then(() => { + expect(client.post).toHaveBeenCalledWith( + '/send', + expect.objectContaining({ settings }), + undefined + ); + }); + }); + it('should set the route', () => { const result = emailEndpoint.route('test-route'); @@ -400,4 +444,22 @@ describe('EmailEndpoint', () => { }, }); }); + + it('should set and reset the batch idempotency key', async () => { + const payload = [ + { + from: 'sender@example.com', + to: ['recipient@example.com'], + subject: 'Test Subject', + }, + ]; + + await emailEndpoint.idempotencyKey('batch-key').sendBatch(payload); + await emailEndpoint.sendBatch(payload); + + expect(client.post).toHaveBeenNthCalledWith(1, '/send/batch', payload, { + headers: { 'Idempotency-Key': 'batch-key' }, + }); + expect(client.post).toHaveBeenNthCalledWith(2, '/send/batch', payload, undefined); + }); }); diff --git a/src/endpoints/email.ts b/src/endpoints/email.ts index b4f26f4..754a473 100644 --- a/src/endpoints/email.ts +++ b/src/endpoints/email.ts @@ -175,9 +175,15 @@ export class EmailEndpoint extends Endpoint { * @param filename The attachment filename * @param content The base64-encoded file content * @param content_id The Content-ID for inline attachments (optional) + * @param content_type The MIME type for the attachment (optional) * @returns The current instance for chaining */ - public attach(filename: string, content: string, content_id?: string): this { + public attach( + filename: string, + content: string, + content_id?: string, + content_type?: string + ): this { if (!this.payload.attachments) { this.payload.attachments = []; } @@ -186,11 +192,23 @@ export class EmailEndpoint extends Endpoint { filename, content, ...(content_id && { content_id }), + ...(content_type && { content_type }), }); return this; } + /** + * Set per-email delivery and tracking settings + * + * @param settings Settings that override the selected route for this email + * @returns The current instance for chaining + */ + public settings(settings: NonNullable): this { + this.payload.settings = settings; + return this; + } + /** * Set metadata for the email * @@ -236,7 +254,15 @@ export class EmailEndpoint extends Endpoint { } public async sendBatch(payload: SendBatchMailRequest): Promise { - return this.httpClient.post('/send/batch', payload); + const config = this.idempotencyKeyValue + ? { headers: { 'Idempotency-Key': this.idempotencyKeyValue } } + : undefined; + + try { + return await this.httpClient.post('/send/batch', payload, config); + } finally { + this.reset(); + } } public async ping(): Promise {