Skip to content

feat: support smtp interception - #803

Open
kettanaito wants to merge 268 commits into
mainfrom
feat/smtp
Open

feat: support smtp interception#803
kettanaito wants to merge 268 commits into
mainfrom
feat/smtp

Conversation

@kettanaito

@kettanaito kettanaito commented Jul 16, 2026

Copy link
Copy Markdown
Member

Roadmap

  • Redesign the session listener arguments. Too disjoined. Something like session might work nicely.
  • controller and realSocket mustn't expose methods like .emit(). They extend Emitter directly and those methods leak. Construct the emitter internally, forward only read-only methods (on/once/earlyOn/etc).
  • Eventual passthrough isn't supported. You cannot claim, do stuff, then passthrough. There's no replay in place.
  • Address the deferred last-resort passthrough, which is an issue for the socket interceptor in general, not specific to SMTP. SMTP's eager fallback passthrough() for unhandled sessions opens a stray real connection to the upstream that lingers through a co-active HTTP mock.
  • Implement dns.lookup interceptor. SMTP uses dns.lookup to lookup hosts and we currently don't capture that at all, which causes non-localhost connections to be invisible to the interceptor.
⏺ Remaining steps

  1. kSend marker — symbol on upstream writes distinguishing auto-forwarded frames from handler-authored ones (server.send()/replay), so MSW can log them
  differently.
  2. Dual-AbortController teardown in SmtpServerConnection — replace the destroy-based teardown to break close-forwarding loops between the two wires (ws-style).
  3. Protocol exports — abstract SmtpClientConnectionProtocol/SmtpServerConnectionProtocol classes for MSW to type against, instead of exposing only the concrete
  classes.
Summary: SMTP interceptor, no session listener → eagerly passthrough() every socket (smtp/index.ts:138). Opens stray real connection even when co-active HTTP
  interceptor would mock the request. Leak.

  Fix (depends on controller-owned last-resort passthrough landing first):
  1. smtp/index.ts:138 — delete socketController.passthrough() in the listenerCount('session') === 0 branch. Keep IPC port == null passthrough (line 128).
  2. net/index.ts:263 — delete zero-listener passthrough() fallback.
  3. Tests: HTTP+SMTP co-active, no session listener, mocked HTTP request → real server gets 0 connections. Real SMTP conn, no matching session listener → still
  reaches real SMTP server.

@kettanaito
kettanaito marked this pull request as ready for review July 16, 2026 15:42
@socket-security

socket-security Bot commented Jul 17, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​types/​nodemailer@​8.0.11001007691100
Addednodemailer@​9.0.3991009794100
Addedresend@​6.18.098100100100100

View full report

Base automatically changed from feat/net-v2 to main July 22, 2026 15:56
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Adds a complete SMTP interceptor with typed sessions and events, SMTP command and reply parsing, authentication and message handling, dot-stuffing, upstream delivery replay, passthrough control, error handling, and extensive Nodemailer and Resend integration tests.

SMTP protocol and session model

Layer / File(s) Summary
Contracts and byte utilities
src/interceptors/smtp/dot-stuffing.ts, src/interceptors/smtp/event-target.ts, src/interceptors/smtp/smtp-session.ts
Defines typed SMTP session state, listener APIs, and byte-preserving dot-stuffing transformations.
Client protocol handling
src/interceptors/smtp/smtp-client-connection.ts
Parses SMTP commands, manages authentication and DATA payloads, emits typed events, and delivers client replies.
Server delivery handling
src/interceptors/smtp/smtp-server-connection.ts
Parses upstream replies, correlates SMTP phases, replays deliveries, handles queue IDs, and manages connection shutdown.

Interceptor and validation

Layer / File(s) Summary
Interceptor wiring and passthrough
src/interceptors/smtp/index.ts, src/interceptors/net/socket-controller.ts
Creates SMTP sessions, exposes session URLs and events, supports passthrough, and delegates real socket reads.
Mocked SMTP behavior tests
test/modules/smtp/smtp-*.test.ts, package.json
Covers authentication, messages, recipients, greetings, errors, session metadata, fallback behavior, and termination.
Upstream and Resend integration tests
test/modules/smtp/smtp-send.test.ts, test/modules/smtp/smtp-passthrough.test.ts, test/modules/smtp/third-party/resend.test.ts
Validates upstream delivery replay, connection reuse, passthrough reply control, termination, and Resend SMTP flows.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Nodemailer
  participant SmtpInterceptor
  participant SmtpClientConnection
  participant SmtpServerConnection
  participant RealSMTPServer
  Nodemailer->>SmtpInterceptor: Connect to SMTP endpoint
  SmtpInterceptor->>SmtpClientConnection: Create intercepted session
  Nodemailer->>SmtpClientConnection: Send SMTP commands and message
  SmtpClientConnection->>SmtpInterceptor: Emit session and command events
  SmtpInterceptor->>SmtpServerConnection: Connect or enable passthrough
  SmtpServerConnection->>RealSMTPServer: Replay SMTP transaction
  RealSMTPServer-->>SmtpServerConnection: Return phase replies and queue ID
  SmtpServerConnection-->>SmtpClientConnection: Forward or substitute replies
  SmtpClientConnection-->>Nodemailer: Deliver SMTP response
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement SMTP mocking/interception support requested by issue #374, including session handling and passthrough behavior.
Out of Scope Changes check ✅ Passed The changes are focused on SMTP interception, related tests, and supporting dependencies with no clear unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.
Title check ✅ Passed The title clearly summarizes the main change: adding SMTP interception support.
Description check ✅ Passed The description is related to the SMTP interception work and the listed roadmap items.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@pkg-pr-new

pkg-pr-new Bot commented Jul 22, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/mswjs/interceptors/@mswjs/interceptors@803

commit: fa42697

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (10)
test/modules/smtp/smtp-auth.test.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

This setup block (interceptor instance, beforeAll/afterEach/afterAll, SMTP_PORT) is repeated verbatim across all 8 SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-auth.test.ts` around lines 1 - 19, Extract the
repeated SmtpInterceptor setup and SMTP_PORT definition into a shared SMTP test
helper, then update each SMTP test file to import and reuse that helper.
Preserve the existing beforeAll, afterEach, and afterAll lifecycle behavior
while removing the duplicated local boilerplate.
test/modules/smtp/smtp-recipient.test.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-recipient.test.ts` around lines 1 - 19, Remove the
duplicated SmtpInterceptor lifecycle setup from this test file and reuse the
shared SMTP test setup used by the other SMTP test files. Preserve the existing
interceptor initialization, listener cleanup, and disposal behavior through that
common setup.
test/modules/smtp/smtp-greeting.test.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-greeting.test.ts` around lines 1 - 19, Extract the
shared SmtpInterceptor lifecycle setup currently defined with beforeAll,
afterEach, and afterAll into a reusable test helper, then update the SMTP test
files to use that helper instead of duplicating the boilerplate. Preserve the
existing apply, removeAllListeners, and dispose behavior.
test/modules/smtp/smtp-session.test.ts (1)

1-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-session.test.ts` around lines 1 - 21, Remove the
duplicated SmtpInterceptor lifecycle setup from this test file and reuse the
shared SMTP test setup established for the other SMTP test files. Preserve the
existing interceptor initialization, cleanup between tests, and disposal after
all tests through that shared helper.
test/modules/smtp/smtp-termination.test.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-termination.test.ts` around lines 1 - 19, Remove the
duplicated SmtpInterceptor lifecycle setup from this test file and reuse the
shared SMTP test setup used by the other SMTP tests. Preserve the existing
interceptor initialization, listener cleanup, and disposal behavior through that
shared helper.
test/modules/smtp/smtp-errors.test.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-errors.test.ts` around lines 1 - 19, Remove the
duplicated SmtpInterceptor lifecycle setup from this test file and reuse the
shared SMTP test setup used by the other SMTP test files. Preserve the existing
interceptor initialization, per-test listener cleanup, and final disposal
behavior through that shared fixture or helper.
test/modules/smtp/smtp-bypass.test.ts (1)

1-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-bypass.test.ts` around lines 1 - 19, Remove the
duplicated SmtpInterceptor lifecycle setup from this test file by reusing the
shared SMTP test setup used across the other SMTP tests. Replace the local
interceptor initialization and beforeAll/afterEach/afterAll hooks around
SmtpInterceptor with the established shared fixture, while preserving the test’s
existing interceptor behavior.
test/modules/smtp/smtp-message.test.ts (1)

1-20: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated interceptor lifecycle boilerplate.

Same setup pattern repeated across all SMTP test files. See consolidated comment below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-message.test.ts` around lines 1 - 20, Consolidate the
repeated SmtpInterceptor lifecycle setup currently defined at module scope in
the SMTP tests. Reuse the shared setup/teardown helper or fixture for
interceptor.apply, removeAllListeners, and dispose, while preserving the
existing lifecycle timing and behavior in smtp-message.test.ts.
src/interceptors/smtp/index.ts (1)

59-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: avoid the as any spread in the super() call.

super(...(['session', {}] as any)) bypasses TypedEvent's init typing and leaves the base data unset (other events pass { data }). Listeners read session/client/server directly so behavior is fine, but a typed super('session', {}) (or { data }) would preserve type checking here and avoid a latent surprise if anything later reads event.data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/interceptors/smtp/index.ts` around lines 59 - 66, Update the
constructor’s TypedEvent initialization to remove the as any spread and use the
typed super call, while preserving the existing session event name and payload.
Ensure the base event data is initialized consistently with the other event
constructors, without changing the session, client, server, or passthrough
assignments.
test/modules/smtp/smtp-send.test.ts (1)

21-92: 📐 Maintainability & Code Quality | 🔵 Trivial

Extract a shared mock-real-SMTP-server test helper. All three files hand-roll the same buffer-based command/DATA-terminator parsing loop for a fake upstream SMTP server; the shared root cause is the lack of a reusable, parameterized helper (e.g. alongside createRawTestServer in test/helpers.ts).

  • test/modules/smtp/smtp-send.test.ts#L21-L92: extract this SmtpServerLog/createSmtpServer() logic into a shared, configurable helper (queue id, optional command log).
  • test/modules/smtp/smtp-passthrough.test.ts#L26-L84: replace this createSmtpServer() with the shared helper, parameterizing the blocked@example.com rejection behavior.
  • test/modules/smtp/third-party/resend.test.ts#L88-L157: replace this createResendSmtpServer() with the shared helper, parameterizing the AUTH PLAIN credential check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/modules/smtp/smtp-send.test.ts` around lines 21 - 92, Extract the shared
buffer-based SMTP command and DATA parsing from createSmtpServer in
test/modules/smtp/smtp-send.test.ts#L21-92 into a reusable parameterized helper
alongside createRawTestServer, including queue-id configuration and optional
command logging; replace createSmtpServer in
test/modules/smtp/smtp-passthrough.test.ts#L26-84 with it while preserving
blocked@example.com rejection, and replace createResendSmtpServer in
test/modules/smtp/third-party/resend.test.ts#L88-157 while preserving AUTH PLAIN
credential validation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/interceptors/smtp/index.ts`:
- Around line 59-66: Update the constructor’s TypedEvent initialization to
remove the as any spread and use the typed super call, while preserving the
existing session event name and payload. Ensure the base event data is
initialized consistently with the other event constructors, without changing the
session, client, server, or passthrough assignments.

In `@test/modules/smtp/smtp-auth.test.ts`:
- Around line 1-19: Extract the repeated SmtpInterceptor setup and SMTP_PORT
definition into a shared SMTP test helper, then update each SMTP test file to
import and reuse that helper. Preserve the existing beforeAll, afterEach, and
afterAll lifecycle behavior while removing the duplicated local boilerplate.

In `@test/modules/smtp/smtp-bypass.test.ts`:
- Around line 1-19: Remove the duplicated SmtpInterceptor lifecycle setup from
this test file by reusing the shared SMTP test setup used across the other SMTP
tests. Replace the local interceptor initialization and
beforeAll/afterEach/afterAll hooks around SmtpInterceptor with the established
shared fixture, while preserving the test’s existing interceptor behavior.

In `@test/modules/smtp/smtp-errors.test.ts`:
- Around line 1-19: Remove the duplicated SmtpInterceptor lifecycle setup from
this test file and reuse the shared SMTP test setup used by the other SMTP test
files. Preserve the existing interceptor initialization, per-test listener
cleanup, and final disposal behavior through that shared fixture or helper.

In `@test/modules/smtp/smtp-greeting.test.ts`:
- Around line 1-19: Extract the shared SmtpInterceptor lifecycle setup currently
defined with beforeAll, afterEach, and afterAll into a reusable test helper,
then update the SMTP test files to use that helper instead of duplicating the
boilerplate. Preserve the existing apply, removeAllListeners, and dispose
behavior.

In `@test/modules/smtp/smtp-message.test.ts`:
- Around line 1-20: Consolidate the repeated SmtpInterceptor lifecycle setup
currently defined at module scope in the SMTP tests. Reuse the shared
setup/teardown helper or fixture for interceptor.apply, removeAllListeners, and
dispose, while preserving the existing lifecycle timing and behavior in
smtp-message.test.ts.

In `@test/modules/smtp/smtp-recipient.test.ts`:
- Around line 1-19: Remove the duplicated SmtpInterceptor lifecycle setup from
this test file and reuse the shared SMTP test setup used by the other SMTP test
files. Preserve the existing interceptor initialization, listener cleanup, and
disposal behavior through that common setup.

In `@test/modules/smtp/smtp-send.test.ts`:
- Around line 21-92: Extract the shared buffer-based SMTP command and DATA
parsing from createSmtpServer in test/modules/smtp/smtp-send.test.ts#L21-92 into
a reusable parameterized helper alongside createRawTestServer, including
queue-id configuration and optional command logging; replace createSmtpServer in
test/modules/smtp/smtp-passthrough.test.ts#L26-84 with it while preserving
blocked@example.com rejection, and replace createResendSmtpServer in
test/modules/smtp/third-party/resend.test.ts#L88-157 while preserving AUTH PLAIN
credential validation.

In `@test/modules/smtp/smtp-session.test.ts`:
- Around line 1-21: Remove the duplicated SmtpInterceptor lifecycle setup from
this test file and reuse the shared SMTP test setup established for the other
SMTP test files. Preserve the existing interceptor initialization, cleanup
between tests, and disposal after all tests through that shared helper.

In `@test/modules/smtp/smtp-termination.test.ts`:
- Around line 1-19: Remove the duplicated SmtpInterceptor lifecycle setup from
this test file and reuse the shared SMTP test setup used by the other SMTP
tests. Preserve the existing interceptor initialization, listener cleanup, and
disposal behavior through that shared helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 449fa9cc-727b-4495-b16e-e3384663af4c

📥 Commits

Reviewing files that changed from the base of the PR and between 7c6d73e and fa42697.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • package.json
  • src/interceptors/net/socket-controller.ts
  • src/interceptors/smtp/dot-stuffing.ts
  • src/interceptors/smtp/event-target.ts
  • src/interceptors/smtp/index.ts
  • src/interceptors/smtp/smtp-client-connection.ts
  • src/interceptors/smtp/smtp-server-connection.ts
  • src/interceptors/smtp/smtp-session.ts
  • test/modules/smtp/smtp-auth.test.ts
  • test/modules/smtp/smtp-bypass.test.ts
  • test/modules/smtp/smtp-errors.test.ts
  • test/modules/smtp/smtp-greeting.test.ts
  • test/modules/smtp/smtp-message.test.ts
  • test/modules/smtp/smtp-passthrough.test.ts
  • test/modules/smtp/smtp-recipient.test.ts
  • test/modules/smtp/smtp-send.test.ts
  • test/modules/smtp/smtp-session.test.ts
  • test/modules/smtp/smtp-termination.test.ts
  • test/modules/smtp/third-party/resend.test.ts

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.

Support mocking SMTP

2 participants