Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 packages/providers/src/lib/email/braze/braze.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export class BrazeEmailProvider extends BaseProvider implements IEmailProvider {
bcc: options.bcc?.join(','),
plaintext_body: options.text || null,
extras: options.payloadDetails || {},
headers: {},
headers: options.headers && Object.keys(options.headers).length > 0 ? options.headers : {},
should_inline_css: true,
attachments: [],
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class EmailJsProvider extends BaseProvider implements IEmailProvider {
await this.ensureClientInitialized();

const headers: Message['header'] = {
...emailOptions.headers,
from: emailOptions.from || this.config.from,
to: emailOptions.to,
subject: emailOptions.subject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ export class MailersendEmailProvider extends BaseProvider implements IEmailProvi
emailParams.setReplyTo(replyTo);
}

const inReplyTo = Object.entries(options.headers ?? {}).find(
([headerName]) => headerName.toLowerCase() === 'in-reply-to'
)?.[1];

if (inReplyTo) {
emailParams.setInReplyTo(inReplyTo);
}

return emailParams;
}

Expand Down
29 changes: 29 additions & 0 deletions packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,35 @@ test('should trigger mailgun correctly', async () => {
api.done();
});

test('should forward custom headers as h: prefixed fields', async () => {
const provider = new MailgunEmailProvider(mockConfig);

const api = nock('https://api.mailgun.net');

api
.post('/v3/test.com/messages', (body) => {
expect(body.includes('name="h:In-Reply-To"')).toBeTruthy();
expect(body.includes('name="h:References"')).toBeTruthy();

return true;
})
.reply(200, {
message: 'Queued. Thank you.',
id: '<20111114174239.25659.5817@samples.mailgun.org>',
});

await provider.sendMessage({
...mockNovuMessage,
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
});

expect(api.isDone()).toBeTruthy();
api.done();
});

test('should trigger mailgun correctly with custom baseUrl', async () => {
const provider = new MailgunEmailProvider({
...mockConfig,
Expand Down
10 changes: 10 additions & 0 deletions packages/providers/src/lib/email/mailgun/mailgun.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ export class MailgunEmailProvider extends BaseProvider implements IEmailProvider
}),
};

if (emailOptions.headers) {
for (const [key, value] of Object.entries(emailOptions.headers)) {
if (emailOptions.replyTo && key.toLowerCase() === 'reply-to') {
continue;
}

data[`h:${key}`] = value;
}
}

if (emailOptions.replyTo) {
data['h:Reply-To'] = emailOptions.replyTo;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export class MailjetEmailProvider extends BaseProvider implements IEmailProvider
Base64Content: attachment.file.toString('base64'),
ContentID: attachment.cid,
})),
...(options.headers && Object.keys(options.headers).length > 0 && { Headers: options.headers }),
Comment thread
ChmaraX marked this conversation as resolved.
}).body;

if (options.replyTo) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface IMandrillSendOptionsMessage {
html: string;
to: { email: string; type: 'to' | string }[];
attachments: IMandrillAttachment[];
headers?: Record<string, string>;
}
interface IMandrillTemplateSendOptionsMessage extends IMandrillSendOptionsMessage {
global_merge_vars?: { name: string; content: string }[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,53 @@ test('should send a standard email through Mandrill', async () => {
});
});

test('should forward custom headers in message.headers', async () => {
const provider = new MandrillProvider(mockConfig);
const spy = vi.spyOn(provider['transporter'].messages, 'send').mockImplementation(async () => {

return [{}] as any;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

await provider.sendMessage({
to: ['test2@test.com'],
subject: 'test subject',
html: '<div> Mail Content </div>',
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
});

expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
message: expect.objectContaining({
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
}),
})
);
});

test('should not add headers to message when no custom headers provided', async () => {
const provider = new MandrillProvider(mockConfig);
const spy = vi.spyOn(provider['transporter'].messages, 'send').mockImplementation(async () => {

return [{}] as any;
});

await provider.sendMessage({
to: ['test2@test.com'],
subject: 'test subject',
html: '<div> Mail Content </div>',
});

const payload = spy.mock.calls[0][0];

expect(payload.message).not.toHaveProperty('headers');
});

test('should send an email using a Mandrill template', async () => {
const provider = new MandrillProvider(mockConfig);
const spy = vi.spyOn(provider['transporter'].messages, 'sendTemplate').mockImplementation(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export class MandrillProvider extends BaseProvider implements IEmailProvider {
type: attachment.mime,
name: attachment?.name,
})),
...(emailOptions.headers &&
Object.keys(emailOptions.headers).length > 0 && { headers: emailOptions.headers }),
};

const { customData } = emailOptions;
Expand Down
4 changes: 4 additions & 0 deletions packages/providers/src/lib/email/netcore/netcore.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ export class NetCoreProvider extends BaseProvider implements IEmailProvider {
});
}

if (options.headers && Object.keys(options.headers).length > 0) {
data.personalizations[0].headers = options.headers;
}

const emailOptions = {
method: 'POST',
url: '/mail/send',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,40 @@ describe.skip('NodemailerProvider', () => {
});
});
});

describe('NodemailerProvider header forwarding', () => {
afterEach(() => {
sendMailMock.mockReset();
});

test('should forward custom headers to sendMail', async () => {
const mockConfig = {
host: 'test.test.email',
port: 587,
secure: false,
from: 'test@test.com',
senderName: 'John Doe',
user: 'test@test.com',
password: 'test123',
};

const provider = new NodemailerProvider(mockConfig);
await provider.sendMessage({
...mockNovuMessage,
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
});

expect(sendMailMock).toHaveBeenCalled();
expect(sendMailMock).toHaveBeenCalledWith(
expect.objectContaining({
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
})
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ export class NodemailerProvider extends BaseProvider implements IEmailProvider {
sendMailOptions.replyTo = options.replyTo;
}

if (options.headers && Object.keys(options.headers).length > 0) {
sendMailOptions.headers = options.headers;
}

return sendMailOptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export class Outlook365Provider extends BaseProvider implements IEmailProvider {
sendMailOptions.replyTo = options.replyTo;
}

if (options.headers && Object.keys(options.headers).length > 0) {
sendMailOptions.headers = options.headers;
}

return sendMailOptions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,45 @@ test('should trigger postmark correctly', async () => {
});
});

test('should forward custom headers in Postmark Headers format', async () => {
const provider = new PostmarkEmailProvider(mockConfig);
const spy = vi.spyOn((provider as any).client, 'sendEmail').mockImplementation(async () => {
return {};
});

await provider.sendMessage({
...mockNovuMessage,
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
});

expect(spy).toHaveBeenCalledWith(
expect.objectContaining({
Headers: [
{ Name: 'In-Reply-To', Value: '<original-message-id@example.com>' },
{ Name: 'References', Value: '<original-message-id@example.com>' },
],
})
);
});

test('should not add Headers field when no custom headers provided', async () => {
const provider = new PostmarkEmailProvider(mockConfig);
const spy = vi.spyOn((provider as any).client, 'sendEmail').mockImplementation(async () => {
return {};
});

await provider.sendMessage(mockNovuMessage);

expect(spy).toHaveBeenCalledWith(
expect.not.objectContaining({
Headers: expect.anything(),
})
);
});
Comment thread
ChmaraX marked this conversation as resolved.

test('should trigger postmark correctly with _passthrough', async () => {
const provider = new PostmarkEmailProvider(mockConfig);
const spy = vi.spyOn((provider as any).client, 'sendEmail').mockImplementation(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ export class PostmarkEmailProvider extends BaseProvider implements IEmailProvide
mailData.ReplyTo = options.replyTo;
}

if (options.headers && Object.keys(options.headers).length > 0) {
mailData.Headers = Object.entries(options.headers).map(([Name, Value]) => ({ Name, Value }));
}

return mailData;
}

Expand Down
24 changes: 24 additions & 0 deletions packages/providers/src/lib/email/ses/ses.provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,30 @@ test('should trigger ses library correctly', async () => {
expect(response.id).toEqual('<mock-message-id@email.amazonses.com>');
});

test('should forward custom headers in raw email content', async () => {
const mockResponse = { MessageId: 'mock-message-id' };
const spy = vi.spyOn(SESv2Client.prototype, 'send').mockImplementation(async () => {

return mockResponse as any;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

const provider = new SESEmailProvider(mockConfig);
await provider.sendMessage({
...mockNovuMessage,
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
});

const bufferArray = spy.mock.calls[0][0].input['Content']['Raw']['Data'];
const emailContent = Buffer.from(bufferArray).toString();

expect(spy).toHaveBeenCalled();
expect(emailContent.includes('In-Reply-To: <original-message-id@example.com>')).toBe(true);
expect(emailContent.includes('References: <original-message-id@example.com>')).toBe(true);
});

test('should trigger ses library correctly with _passthrough', async () => {
const mockResponse = { MessageId: 'mock-message-id' };
const spy = vi.spyOn(SESv2Client.prototype, 'send').mockImplementation(async () => {
Expand Down
6 changes: 4 additions & 2 deletions packages/providers/src/lib/email/ses/ses.provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class SESEmailProvider extends BaseProvider implements IEmailProvider {
}

private async sendMail(
{ html, text, to, from, senderName, subject, attachments, cc, bcc, replyTo },
{ html, text, to, from, senderName, subject, attachments, cc, bcc, replyTo, headers = {} },
bridgeProviderData: WithPassthrough<Record<string, unknown>> = {}
) {
const transporter = nodemailer.createTransport({
Expand All @@ -54,6 +54,7 @@ export class SESEmailProvider extends BaseProvider implements IEmailProvider {
cc,
bcc,
replyTo,
...(headers && Object.keys(headers).length > 0 && { headers }),
...(this.config.configurationSetName && {
ses: { ConfigurationSetName: this.config.configurationSetName },
}),
Expand All @@ -63,7 +64,7 @@ export class SESEmailProvider extends BaseProvider implements IEmailProvider {
}

async sendMessage(
{ html, text, to, from, subject, attachments, cc, bcc, replyTo, senderName }: IEmailOptions,
{ html, text, to, from, subject, attachments, cc, bcc, replyTo, senderName, headers }: IEmailOptions,
bridgeProviderData: WithPassthrough<Record<string, unknown>> = {}
): Promise<ISendMessageSuccessResponse> {
const info = await this.sendMail(
Expand All @@ -84,6 +85,7 @@ export class SESEmailProvider extends BaseProvider implements IEmailProvider {
cc,
bcc,
replyTo,
headers,
},
bridgeProviderData
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,38 @@ test('should trigger sparkpost library correctly', async () => {
);
});

test('should forward custom headers inside content.headers', async () => {
const { mockPost: spy } = axiosSpy({
data: {
results: {
id: 'id',
},
},
});
const provider = new SparkPostEmailProvider(mockConfig);

await provider.sendMessage({
...mockNovuMessage,
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
});

expect(spy).toHaveBeenCalledWith(
'/transmissions',
expect.objectContaining({
content: expect.objectContaining({
headers: {
'In-Reply-To': '<original-message-id@example.com>',
References: '<original-message-id@example.com>',
},
}),
}),
expect.anything()
);
});

test('should trigger sparkpost library correctly with _passthrough', async () => {
const { mockPost: spy } = axiosSpy({
data: {
Expand Down
Loading
Loading