Skip to content

feat(providers): forward email headers across all providers for reply threading fixes NV-7402#10836

Merged
ChmaraX merged 4 commits intonextfrom
cursor/d056dc5a
Apr 23, 2026
Merged

feat(providers): forward email headers across all providers for reply threading fixes NV-7402#10836
ChmaraX merged 4 commits intonextfrom
cursor/d056dc5a

Conversation

@ChmaraX
Copy link
Copy Markdown
Contributor

@ChmaraX ChmaraX commented Apr 23, 2026

Summary

  • Add headers forwarding to the 9 email providers that were missing it (Mailgun, SES, Postmark, Nodemailer/SMTP, Outlook365, SparkPost, Mandrill, Mailjet, EmailJS, Braze, NetCore), completing the set started in feat: add custom header support for resend, brevo and sendgrid #5343
  • Fix a correctness bug in the EmailJS provider where custom headers spread at the end of the header object could inadvertently override from, to, and subject
  • Add headers?: Record<string, string> to the internal Mandrill interface so the type correctly describes the payload

How it works

Each provider maps IEmailOptions.headers to its own API-specific format:

Provider API format
Mailgun h:HeaderName keys in form-data
SES / Nodemailer / Outlook365 SendMailOptions.headers
Postmark Headers: [{ Name, Value }] array
SparkPost content.headers object
Mandrill message.headers object
Mailjet Headers key inside transform call
EmailJS Spread first into header object (core fields always win)
Braze Populate existing messages.email.headers field
NetCore personalizations[0].headers
MailerSend emailParams.setInReplyTo() for In-Reply-To (SDK has no generic headers)

No regression risk — the worker always initialises headers to {}, and every provider guards with Object.keys(headers).length > 0 (or a no-op spread) before touching the upstream API.

Test plan

  • New tests added for Mailgun, Postmark, SES, SparkPost, and Mandrill covering:
    • custom headers are forwarded in the correct provider format
    • no Headers/headers field is added when none are supplied
  • All 34 existing + new provider tests pass (vitest run)
  • No linter errors across all 12 changed provider files

Made with Cursor

What changed

Email headers from IEmailOptions are now forwarded into upstream provider payloads so reply-threading headers (e.g., In-Reply-To, References) are preserved across transports. Providers that previously ignored custom headers were updated to map them into each provider’s API format, and EmailJS was fixed so core fields (from, to, subject) cannot be overridden by user-provided headers.

Affected areas

providers: Multiple email providers were updated to forward headers into their API formats: Mailgun (form-data keys as h:HeaderName), Postmark (Headers array of { Name, Value }), SparkPost (content.headers), Mandrill (message.headers), Mailjet (Headers in transformed message), Braze (messages.email.headers), NetCore (personalizations[0].headers), SES/Nodemailer/Outlook365 (SendMailOptions.headers), EmailJS (spread custom headers first so core fields win), and MailerSend (uses emailParams.setInReplyTo() for In-Reply-To). The email worker initializes headers = {} and providers only attach headers when the headers object is present and non-empty to avoid behavior changes when callers don’t supply headers.

Key technical decisions

  • Only attach headers when a non-empty headers object is provided to avoid regressions for callers that don’t send headers.
  • EmailJS merges custom headers before assigning core fields so core fields take precedence.
  • Mandrill typings updated: IMandrillSendOptionsMessage now includes optional headers?: Record<string, string>.

Testing

New and updated unit tests cover Mailgun, Postmark, SES, SparkPost, Mandrill, and Nodemailer; existing provider tests were run with the new tests and no linter errors were reported. No additional e2e or manual testing was required beyond these provider-specific unit tests.

@linear
Copy link
Copy Markdown

linear Bot commented Apr 23, 2026

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 23, 2026

Deploy Preview for dashboard-v2-novu-staging canceled.

Name Link
🔨 Latest commit f614bd5
🔍 Latest deploy log https://app.netlify.com/projects/dashboard-v2-novu-staging/deploys/69ea2d244941070008097959

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds conditional propagation of caller-provided email headers across multiple email providers, mapping options.headers into each provider's outbound request using provider-specific formats and avoiding duplicate Reply-To injection.

Changes

Cohort / File(s) Summary
Provider Implementations — Header Forwarding
packages/providers/src/lib/email/braze/braze.provider.ts, packages/providers/src/lib/email/emailjs/emailjs.provider.ts, packages/providers/src/lib/email/mailersend/mailersend.provider.ts, packages/providers/src/lib/email/mailgun/mailgun.provider.ts, packages/providers/src/lib/email/mailjet/mailjet.provider.ts, packages/providers/src/lib/email/mandrill/mandrill.provider.ts, packages/providers/src/lib/email/netcore/netcore.provider.ts, packages/providers/src/lib/email/nodemailer/nodemailer.provider.ts, packages/providers/src/lib/email/outlook365/outlook365.provider.ts, packages/providers/src/lib/email/postmark/postmark.provider.ts, packages/providers/src/lib/email/ses/ses.provider.ts, packages/providers/src/lib/email/sparkpost/sparkpost.provider.ts
Providers now read options.headers when present and non-empty and include them in outbound payloads using provider-specific conventions (e.g., Mailgun h:HeaderName, Postmark { Name, Value } array, SparkPost content.headers, MailerSend In-Reply-To mapping, Nodemailer/SES headers in SendMailOptions). Reply-To is deduped when both replyTo and headers contain it.
Type Definitions
packages/providers/src/lib/email/mandrill/mandril.interface.ts
Added optional headers?: Record<string,string> to Mandrill message interface.
Tests — Header Propagation
packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts, packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts, packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts, packages/providers/src/lib/email/postmark/postmark.provider.spec.ts, packages/providers/src/lib/email/ses/ses.provider.spec.ts, packages/providers/src/lib/email/sparkpost/sparkpost.provider.spec.ts
Added/updated unit tests asserting that custom headers are forwarded into provider-specific request fields when provided and omitted when absent.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (caller)
    participant Provider as Email Provider
    participant API as External Email API
    Client->>Provider: sendMessage(payload with headers)
    Provider->>Provider: validate/merge headers (dedupe reply-to, case-insensitive lookup)
    Provider->>API: forward message with provider-specific header mapping
    API-->>Provider: response/ack
    Provider-->>Client: return/send result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title does not follow Conventional Commits format. It includes 'fixes' in the description instead of ending with 'fixes NV-7402' as required by the guidelines. Update the title to: 'feat(providers): forward email headers across all providers for reply threading (fixes NV-7402)' or 'feat(providers): forward email headers across all providers for reply threading' followed by 'fixes NV-7402' at the end.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/providers/src/lib/email/mailgun/mailgun.provider.ts (1)

109-117: ⚠️ Potential issue | 🟠 Major

Preserve explicit replyTo over custom headers.

The new loop runs after Line 110, so emailOptions.headers['Reply-To'] can overwrite the typed replyTo value. Keep the structured field authoritative.

🐛 Proposed fix
-    if (emailOptions.replyTo) {
-      data['h:Reply-To'] = emailOptions.replyTo;
-    }
-
     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;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/lib/email/mailgun/mailgun.provider.ts` around lines
109 - 117, The replyTo value set from emailOptions.replyTo is being overwritten
by entries in emailOptions.headers; update the mailgun provider to preserve the
explicit replyTo by either moving the headers loop to run before applying
emailOptions.replyTo or by skipping header keys that represent Reply-To
(case-insensitive, e.g. "Reply-To" or "reply-to") when copying
emailOptions.headers into data; ensure the authoritative assignment uses the
same key (`h:Reply-To`) and that the code references the existing variables
`emailOptions`, `emailOptions.replyTo`, `emailOptions.headers`, and `data` in
`mailgun.provider.ts`.
🧹 Nitpick comments (1)
packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts (1)

36-63: LGTM.

Multipart body inspection for h:-prefixed fields is a reasonable way to verify the Mailgun header convention end-to-end.

Optional: to make the assertion more robust against future body reshaping, you could also assert the concrete header values (e.g., body.includes('<original-message-id@example.com>')) rather than only the field names.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts` around
lines 36 - 63, The test 'should forward custom headers as h: prefixed fields' in
MailgunEmailProvider spec currently asserts only that the multipart body
contains name="h:In-Reply-To" and name="h:References"; update the assertion
callback passed to nock.post to also assert the actual header values are present
(e.g., that the body includes '<original-message-id@example.com>' or the exact
values from the mockNovuMessage.headers) so the test verifies both the h: field
names and their concrete values are forwarded.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/providers/src/lib/email/braze/braze.provider.ts`:
- Line 72: The request currently always sets headers: options.headers || {},
which forces an empty headers object into Braze requests; change this to only
include the headers property when options.headers is provided (and non-empty).
Locate the code that builds the request object (where options and headers are
referenced in braze.provider.ts) and replace the unconditional assignment with a
conditional spread such as ...(options.headers &&
Object.keys(options.headers).length ? { headers: options.headers } : {}) so
headers is omitted when there are no custom headers.

In `@packages/providers/src/lib/email/mailersend/mailersend.provider.ts`:
- Around line 79-83: The code reads the In-Reply-To header using exact casing
(options.headers?.['In-Reply-To']) so variants like "in-reply-to" are missed;
update the header lookup in mailersend.provider.ts (where inReplyTo is assigned
and passed to emailParams.setInReplyTo) to perform a case-insensitive search of
options.headers keys (for example normalize keys to lower-case or check both the
lower-cased 'in-reply-to' and existing key names) and use the found value when
calling emailParams.setInReplyTo.

In `@packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts`:
- Around line 52-53: The mock implementations for
provider['transporter'].messages.send (the vi.spyOn(...).mockImplementation
async callbacks) are missing the required blank line before their return
statements; update both mock implementations (the one using
provider['transporter'].messages.send and the other similar mock later) to
insert a blank line immediately before each return to satisfy the TypeScript
style rule.
- Around line 90-94: The test's matcher is too loose: instead of using
expect.not.objectContaining({ headers: expect.anything() }) update the assertion
to explicitly verify the headers property is absent on the message object by
pulling the actual call arg from the spy (e.g., const arg = spy.mock.calls[0][0]
or spy.calls[0][0]) and asserting that
Object.prototype.hasOwnProperty.call(arg.message, 'headers') is false (or
expect('headers' in arg.message).toBe(false)), referencing the existing spy and
message.headers to locate the code to change.

In `@packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts`:
- Around line 99-118: The test "should forward custom headers to sendMail" is
never executed because the enclosing test suite uses
describe.skip('NodemailerProvider', ...) — update the test file so the suite is
not skipped (change describe.skip to describe) or move the specific test into
its own non-skipped describe/it block; ensure you modify the
describe.skip('NodemailerProvider') usage (or relocate the test) so that the
NodemailerProvider header propagation assertion actually runs in Vitest.

In `@packages/providers/src/lib/email/ses/ses.provider.spec.ts`:
- Around line 112-113: The mock implementation for SESv2Client.prototype.send
(the vi.spyOn(...).mockImplementation async callback) is missing the required
blank line before its return; edit the mock function passed to
mockImplementation (the async () => { ... }) to insert a blank line immediately
before the return statement so it conforms to the repo TypeScript style rule
requiring a blank line before every return.

---

Outside diff comments:
In `@packages/providers/src/lib/email/mailgun/mailgun.provider.ts`:
- Around line 109-117: The replyTo value set from emailOptions.replyTo is being
overwritten by entries in emailOptions.headers; update the mailgun provider to
preserve the explicit replyTo by either moving the headers loop to run before
applying emailOptions.replyTo or by skipping header keys that represent Reply-To
(case-insensitive, e.g. "Reply-To" or "reply-to") when copying
emailOptions.headers into data; ensure the authoritative assignment uses the
same key (`h:Reply-To`) and that the code references the existing variables
`emailOptions`, `emailOptions.replyTo`, `emailOptions.headers`, and `data` in
`mailgun.provider.ts`.

---

Nitpick comments:
In `@packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts`:
- Around line 36-63: The test 'should forward custom headers as h: prefixed
fields' in MailgunEmailProvider spec currently asserts only that the multipart
body contains name="h:In-Reply-To" and name="h:References"; update the assertion
callback passed to nock.post to also assert the actual header values are present
(e.g., that the body includes '<original-message-id@example.com>' or the exact
values from the mockNovuMessage.headers) so the test verifies both the h: field
names and their concrete values are forwarded.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f868c7b5-ac3a-4d47-baaa-2cfe9d4de265

📥 Commits

Reviewing files that changed from the base of the PR and between b58504f and e6a5fe8.

📒 Files selected for processing (19)
  • packages/providers/src/lib/email/braze/braze.provider.ts
  • packages/providers/src/lib/email/emailjs/emailjs.provider.ts
  • packages/providers/src/lib/email/mailersend/mailersend.provider.ts
  • packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts
  • packages/providers/src/lib/email/mailgun/mailgun.provider.ts
  • packages/providers/src/lib/email/mailjet/mailjet.provider.ts
  • packages/providers/src/lib/email/mandrill/mandril.interface.ts
  • packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts
  • packages/providers/src/lib/email/mandrill/mandrill.provider.ts
  • packages/providers/src/lib/email/netcore/netcore.provider.ts
  • packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts
  • packages/providers/src/lib/email/nodemailer/nodemailer.provider.ts
  • packages/providers/src/lib/email/outlook365/outlook365.provider.ts
  • packages/providers/src/lib/email/postmark/postmark.provider.spec.ts
  • packages/providers/src/lib/email/postmark/postmark.provider.ts
  • packages/providers/src/lib/email/ses/ses.provider.spec.ts
  • packages/providers/src/lib/email/ses/ses.provider.ts
  • packages/providers/src/lib/email/sparkpost/sparkpost.provider.spec.ts
  • packages/providers/src/lib/email/sparkpost/sparkpost.provider.ts

plaintext_body: options.text || null,
extras: options.payloadDetails || {},
headers: {},
headers: options.headers || {},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Omit headers when no custom headers are present.

Line 72 sends headers: {} whenever headers are unset or initialized empty. That differs from the non-empty guard used elsewhere and can change Braze request shape unnecessarily.

🐛 Proposed fix
-          headers: options.headers || {},
+          ...(options.headers && Object.keys(options.headers).length > 0 && { headers: options.headers }),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers: options.headers || {},
...(options.headers && Object.keys(options.headers).length > 0 && { headers: options.headers }),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/lib/email/braze/braze.provider.ts` at line 72, The
request currently always sets headers: options.headers || {}, which forces an
empty headers object into Braze requests; change this to only include the
headers property when options.headers is provided (and non-empty). Locate the
code that builds the request object (where options and headers are referenced in
braze.provider.ts) and replace the unconditional assignment with a conditional
spread such as ...(options.headers && Object.keys(options.headers).length ? {
headers: options.headers } : {}) so headers is omitted when there are no custom
headers.

Comment thread packages/providers/src/lib/email/mailersend/mailersend.provider.ts Outdated
Comment thread packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts
Comment thread packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts Outdated
Comment thread packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts Outdated
Comment thread packages/providers/src/lib/email/ses/ses.provider.spec.ts
ChmaraX added 3 commits April 23, 2026 16:14
… threading fixes NV-7402

Add header forwarding to all email providers that were missing it, enabling
standard reply-threading headers (In-Reply-To, References) to flow through
to the upstream provider API.

- Mailgun: map headers as h:* prefixed form-data keys
- SES: pass headers through to nodemailer SendMailOptions
- Postmark: convert to Headers [{Name, Value}] format
- Nodemailer (Custom SMTP): pass headers to SendMailOptions
- Outlook365: pass headers to SendMailOptions
- SparkPost: add headers inside content.headers
- Mandrill: add headers to message object; extend interface type
- Mailjet: include Headers in transform call (type-safe)
- EmailJS: spread custom headers first so core fields always win
- Braze: populate existing headers field from options
- NetCore: add to personalizations[0].headers
- MailerSend: map In-Reply-To to dedicated setInReplyTo() field

Guards ensure empty default headers ({}) never change existing behaviour.
Add tests for Mailgun, Postmark, SES, SparkPost, and Mandrill.

Made-with: Cursor
- braze: use ternary to only pass headers when non-empty
- mailersend: case-insensitive lookup for In-Reply-To header
- mailgun: apply custom headers before explicit replyTo so replyTo always wins; skip Reply-To key from headers when replyTo is set
- mandrill spec: blank lines before return in mock impls; tighten no-headers assertion to use not.toHaveProperty
- nodemailer spec: lift header forwarding test outside describe.skip into its own non-skipped suite
- ses spec: blank line before return in mock implementation

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/providers/src/lib/email/mailjet/mailjet.provider.ts`:
- Line 121: Add unit tests for the Mailjet provider mirroring Postmark/SES
coverage: in the Mailjet provider test suite (e.g., mailjet.provider.spec.ts)
add one test that calls the provider's send method with non-empty
options.headers (e.g., { "In-Reply-To": "msgid", "References": "ref" }) and
assert the outgoing payload contains Messages[0].Headers with those key/value
pairs, and another test that calls send with options.headers undefined or {} and
asserts the outgoing payload does NOT include a Headers field; use the same
mocking/inspection helpers used by other provider tests to capture the Mailjet
request body, and run pnpm build in packages/ after adding the tests.

In `@packages/providers/src/lib/email/postmark/postmark.provider.spec.ts`:
- Around line 89-102: The test should assert the Headers property is absent
rather than merely non-matching; after calling PostmarkEmailProvider.sendMessage
(and spying on (provider as any).client.sendEmail), capture the actual call
payload from spy.mock.calls[0][0] and assert it does not have the Headers
property (e.g., expect(payload).not.toHaveProperty('Headers') or
expect(payload.Headers).toBeUndefined()), replacing the current
expect.not.objectContaining check in the test for stricter verification.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d14bc426-4557-4ed6-81bb-2a0458b39fb2

📥 Commits

Reviewing files that changed from the base of the PR and between dfe595d and d86ef25.

📒 Files selected for processing (19)
  • packages/providers/src/lib/email/braze/braze.provider.ts
  • packages/providers/src/lib/email/emailjs/emailjs.provider.ts
  • packages/providers/src/lib/email/mailersend/mailersend.provider.ts
  • packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts
  • packages/providers/src/lib/email/mailgun/mailgun.provider.ts
  • packages/providers/src/lib/email/mailjet/mailjet.provider.ts
  • packages/providers/src/lib/email/mandrill/mandril.interface.ts
  • packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts
  • packages/providers/src/lib/email/mandrill/mandrill.provider.ts
  • packages/providers/src/lib/email/netcore/netcore.provider.ts
  • packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts
  • packages/providers/src/lib/email/nodemailer/nodemailer.provider.ts
  • packages/providers/src/lib/email/outlook365/outlook365.provider.ts
  • packages/providers/src/lib/email/postmark/postmark.provider.spec.ts
  • packages/providers/src/lib/email/postmark/postmark.provider.ts
  • packages/providers/src/lib/email/ses/ses.provider.spec.ts
  • packages/providers/src/lib/email/ses/ses.provider.ts
  • packages/providers/src/lib/email/sparkpost/sparkpost.provider.spec.ts
  • packages/providers/src/lib/email/sparkpost/sparkpost.provider.ts
✅ Files skipped from review due to trivial changes (5)
  • packages/providers/src/lib/email/netcore/netcore.provider.ts
  • packages/providers/src/lib/email/postmark/postmark.provider.ts
  • packages/providers/src/lib/email/mandrill/mandril.interface.ts
  • packages/providers/src/lib/email/mailgun/mailgun.provider.spec.ts
  • packages/providers/src/lib/email/mandrill/mandrill.provider.spec.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/providers/src/lib/email/outlook365/outlook365.provider.ts
  • packages/providers/src/lib/email/nodemailer/nodemailer.provider.ts
  • packages/providers/src/lib/email/braze/braze.provider.ts
  • packages/providers/src/lib/email/mailersend/mailersend.provider.ts
  • packages/providers/src/lib/email/emailjs/emailjs.provider.ts
  • packages/providers/src/lib/email/sparkpost/sparkpost.provider.spec.ts
  • packages/providers/src/lib/email/ses/ses.provider.spec.ts
  • packages/providers/src/lib/email/mailgun/mailgun.provider.ts
  • packages/providers/src/lib/email/ses/ses.provider.ts
  • packages/providers/src/lib/email/sparkpost/sparkpost.provider.ts

Comment thread packages/providers/src/lib/email/mailjet/mailjet.provider.ts
Comment thread packages/providers/src/lib/email/postmark/postmark.provider.spec.ts
… of module mock

The module-level vi.mock for nodemailer was not reliably intercepting sendMail,
causing the new non-skipped test to hit a real DNS lookup. Rewrite to use
vi.spyOn on provider['transports'].sendMail directly after construction,
consistent with how other provider tests (Postmark, Mandrill, SES) are written.
Also add a second assertion verifying headers are absent when none are provided.

Made-with: Cursor
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts`:
- Around line 241-249: Add a test that covers the case where headers is an empty
object so we don't leak headers: {} to Nodemailer: in the existing spec for
NodemailerProvider (nodemailer.provider.spec.ts) call provider.sendMessage using
a message that has headers: {} (e.g., set mockNovuMessage.headers = {}) and
assert the captured payload from provider['transports'].sendMail does not have
the 'headers' property; reference the provider.sendMessage call and the spy on
provider['transports'].sendMail to locate where to add this assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 91af43c2-887c-4d92-98a4-30da53131363

📥 Commits

Reviewing files that changed from the base of the PR and between d86ef25 and f614bd5.

📒 Files selected for processing (1)
  • packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts

Comment on lines +241 to +249
test('should not include headers field when no custom headers provided', async () => {
const provider = new NodemailerProvider(mockConfig);
const spy = vi.spyOn(provider['transports'], 'sendMail').mockResolvedValue({ messageId: 'test-id' } as any);

await provider.sendMessage(mockNovuMessage);

const payload = spy.mock.calls[0][0] as Record<string, unknown>;

expect(payload).not.toHaveProperty('headers');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cover the empty headers: {} path as well.

Line 245 verifies omitted headers, but the worker path described in this PR initializes headers as {}. A truthy-object guard would still leak headers: {} to Nodemailer and this test would not catch it.

🧪 Proposed additional coverage
   test('should not include headers field when no custom headers provided', async () => {
     const provider = new NodemailerProvider(mockConfig);
     const spy = vi.spyOn(provider['transports'], 'sendMail').mockResolvedValue({ messageId: 'test-id' } as any);
 
     await provider.sendMessage(mockNovuMessage);
 
     const payload = spy.mock.calls[0][0] as Record<string, unknown>;
 
     expect(payload).not.toHaveProperty('headers');
   });
+
+  test('should not include headers field when custom headers are empty', async () => {
+    const provider = new NodemailerProvider(mockConfig);
+    const spy = vi.spyOn(provider['transports'], 'sendMail').mockResolvedValue({ messageId: 'test-id' } as any);
+
+    await provider.sendMessage({
+      ...mockNovuMessage,
+      headers: {},
+    });
+
+    const payload = spy.mock.calls[0][0] as Record<string, unknown>;
+
+    expect(payload).not.toHaveProperty('headers');
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/providers/src/lib/email/nodemailer/nodemailer.provider.spec.ts`
around lines 241 - 249, Add a test that covers the case where headers is an
empty object so we don't leak headers: {} to Nodemailer: in the existing spec
for NodemailerProvider (nodemailer.provider.spec.ts) call provider.sendMessage
using a message that has headers: {} (e.g., set mockNovuMessage.headers = {})
and assert the captured payload from provider['transports'].sendMail does not
have the 'headers' property; reference the provider.sendMessage call and the spy
on provider['transports'].sendMail to locate where to add this assertion.

@ChmaraX ChmaraX merged commit 6a9dfa7 into next Apr 23, 2026
37 checks passed
@ChmaraX ChmaraX deleted the cursor/d056dc5a branch April 23, 2026 14:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant