Skip to content

Add Mailtrap email transport - #27873

Open
tsokolovs wants to merge 6 commits into
directus:mainfrom
tsokolovs:feat/mailtrap-email-transport
Open

Add Mailtrap email transport#27873
tsokolovs wants to merge 6 commits into
directus:mainfrom
tsokolovs:feat/mailtrap-email-transport

Conversation

@tsokolovs

@tsokolovs tsokolovs commented Jul 10, 2026

Copy link
Copy Markdown

What's Changed

Adds Mailtrap as a native option for EMAIL_TRANSPORT, alongside the existing sendmail, smtp, ses, and mailgun transports. It uses Mailtrap's official mailtrap package, which ships a Nodemailer transport (MailtrapTransport).

The transport supports all three Mailtrap modes:

  • Live sending via an API token.
  • Sandbox / Email Testing by capturing messages in a test inbox.
  • Bulk stream for high-volume sending.

Implementation mirrors the existing Mailgun transport in api/src/mailer.ts (lazy require, env-driven options). Options are only passed to the SDK when their env vars are set.

New environment variables

Variable Type Description
EMAIL_MAILTRAP_TOKEN string Mailtrap API token
EMAIL_MAILTRAP_SANDBOX boolean Send to a test inbox instead of delivering
EMAIL_MAILTRAP_INBOX_ID number Inbox ID used when sandbox is enabled
EMAIL_MAILTRAP_BULK boolean Use the bulk sending stream

Tested Scenarios

  • Unit test added for the new transport; api/src/mailer.test.ts passes (5/5), @directus/env passes (78/78).
  • Live sandbox send: message delivered to a Mailtrap test inbox (verified via the API).
  • Live production send: delivered to a real mailbox.
  • Bulk stream: 10 live messages sent via EMAIL_MAILTRAP_BULK=true, all succeeded.
  • ESLint + Prettier clean on changed files.

Review Notes / Questions / Concerns

  • Adding the mailtrap dependency pulls in axios, which dedupes qs to 6.15.2 in the lockfile. This is the only transitive change beyond the direct addition.

Checklist

Leave unchecked where not applicable

  • Tests added/updated
  • Documentation PR created in directus/docs Document Mailtrap email transport docs#768
  • OpenAPI updated
  • SDK (@directus/sdk) updated to reflect the changes
  • Types (@directus/types) updated to reflect the changes
  • GraphQL schema updated to reflect the changes
  • System data (@directus/system-data) updated for changes to system collections/fields/relations
  • Database migration added for schema/system changes
  • Environment variables documented for new/changed config
  • App translations added for new user-facing strings
  • Security implications apply

@tsokolovs

tsokolovs commented Jul 10, 2026

Copy link
Copy Markdown
Author

I've used an AI agent to create a PR for me (via the gh cli) to make it easier for me to ensure I've fulfilled all the guidelines outlined in contributing.md file. The code itself was written manually. Happy to answer all the questions you have.

@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.61702% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.32%. Comparing base (234bac2) to head (5df0715).

Additional details and impacted files
@@           Coverage Diff           @@
##             main   #27873   +/-   ##
=======================================
  Coverage   70.31%   70.32%           
=======================================
  Files        2460     2460           
  Lines      169097   169144   +47     
  Branches    17143    17158   +15     
=======================================
+ Hits       118898   118944   +46     
- Misses      50199    50200    +1     
Flag Coverage Δ
api 59.31% <92.30%> (+0.02%) ⬆️
app 78.87% <ø> (ø)
composables 83.87% <ø> (ø)
constants 4.66% <ø> (ø)
create-directus-extension 96.55% <ø> (ø)
create-directus-project 98.43% <ø> (ø)
env 99.74% <100.00%> (+<0.01%) ⬆️
errors 96.29% <ø> (ø)
extensions 56.32% <ø> (ø)
extensions-registry 95.43% <ø> (ø)
extensions-sdk 16.68% <ø> (ø)
format-title 100.00% <ø> (ø)
memory 100.00% <ø> (ø)
pressure 77.63% <ø> (ø)
release-notes-generator 80.91% <ø> (ø)
schema-builder 81.42% <ø> (ø)
sdk 34.90% <ø> (ø)
storage 92.00% <ø> (ø)
storage-driver-azure 73.33% <ø> (ø)
storage-driver-cloudinary 80.87% <ø> (ø)
storage-driver-gcs 66.95% <ø> (ø)
storage-driver-local 70.89% <ø> (ø)
storage-driver-s3 52.27% <ø> (ø)
storage-driver-supabase 67.40% <ø> (ø)
system-data 32.42% <ø> (ø)
update-check 55.67% <ø> (ø)
utils 93.19% <ø> (ø)
validation 43.78% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tsokolovs tsokolovs changed the title Add Mailtrap email transport 🤖🤖🤖 Add Mailtrap email transport Jul 14, 2026
@tsokolovs
tsokolovs force-pushed the feat/mailtrap-email-transport branch 3 times, most recently from adeb5aa to b0c90c2 Compare July 22, 2026 12:26
@tsokolovs
tsokolovs force-pushed the feat/mailtrap-email-transport branch 2 times, most recently from 9f945af to 1141592 Compare July 30, 2026 07:43

@AlexGaillard AlexGaillard left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for this, just a few small changes here and also TY for pointing out the shortcomings of our mailer.test.ts, it's a tricky one to test 😅 .

Comment thread api/src/mailer.ts Outdated
Comment on lines +77 to +79
if (env['EMAIL_MAILTRAP_SANDBOX'] !== undefined) mailtrapOptions['sandbox'] = env['EMAIL_MAILTRAP_SANDBOX'];
if (env['EMAIL_MAILTRAP_INBOX_ID'] !== undefined) mailtrapOptions['testInboxId'] = env['EMAIL_MAILTRAP_INBOX_ID'];
if (env['EMAIL_MAILTRAP_BULK'] !== undefined) mailtrapOptions['bulk'] = env['EMAIL_MAILTRAP_BULK'];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this may be better if we can get some more explicit checks in here rather than checking that they're not undefined. mailtrapOptions is expecting and the app will still boot up and the conditional will still pass if EMAIL_MAILTRAP_SANDBOX is defined as a string in the env.

Also a nit, it might be easier to read if we nest the body of the conditions.

ie:

if (env['EMAIL_MAILTRAP_SANDBOX'] === true) {
	mailtrapOptions['sandbox'] = env['EMAIL_MAILTRAP_SANDBOX'];
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Also, EMAIL_MAILTRAP_SANDBOX depends on EMAIL_MAILTRAP_INBOX_ID in order to be able to use the sandbox correctly, so there should probably be a check for that as well.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added more robust checks for Mailtrap and extracted them to the createMailtrapTransport function, in the same mailer.ts file.

Comment thread api/src/mailer.test.ts
Comment on lines +103 to 104
});
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think you've actually identified the shortcomings of our tests above 😓

That being said, this is still essentially testing nothing.

If we look into the getFreshMailer().transporter we can see the params of the created transporter:

  transporter: MailtrapTransport {
    name: 'MailtrapTransport',
    version: '4.6.1',
    client: MailtrapClient {
      axios: [Function],
      testInboxId: 12345,
      accountId: undefined,
      organizationId: undefined,
      bulk: false,
      sandbox: true
    }

I think it might be worthwhile testing that those are created correctly, that way if Mailtrap every change any of these it should hopefully catch it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added more meaningful tests for Mailtrap that check the changes and validations in mailer.ts file.

Comment thread api/src/mailer.test.ts Outdated
Comment on lines +87 to +90
// Re-import getMailer so it runs against a fresh module (the transporter is
// cached at module scope, and beforeEach's resetModules only affects fresh
// imports, not the statically imported getMailer used by the tests above).
// Without this, the cached transporter short-circuits the mailtrap branch.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think maybe we should add a TODO that this should be applied to the tests above.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Added TODO above

@tsokolovs
tsokolovs force-pushed the feat/mailtrap-email-transport branch 2 times, most recently from 55e2309 to 7e6e5d3 Compare August 5, 2026 06:18
@tsokolovs
tsokolovs force-pushed the feat/mailtrap-email-transport branch from e745b8f to 2575b50 Compare August 11, 2026 09:29
@tsokolovs
tsokolovs force-pushed the feat/mailtrap-email-transport branch from 2575b50 to 7a7da81 Compare August 13, 2026 08:13
Add a native `mailtrap` option for `EMAIL_TRANSPORT` using Mailtrap's official nodemailer transport. Supports live sending, the bulk stream, and the sandbox testing inbox via EMAIL_MAILTRAP_TOKEN, EMAIL_MAILTRAP_SANDBOX, EMAIL_MAILTRAP_INBOX_ID, and EMAIL_MAILTRAP_BULK.
Re-import getMailer in the Mailtrap test so the module-scoped transporter cache doesn't short-circuit the new branch, ensuring it actually executes under coverage.
The mailtrap@4.6.1 snapshot referenced axios@1.16.1, which has no
corresponding entry in the lockfile. Point it at the already-resolved
axios@1.18.1 so that pnpm install --frozen-lockfile succeeds.
@tsokolovs
tsokolovs force-pushed the feat/mailtrap-email-transport branch from 7a7da81 to 5df0715 Compare August 14, 2026 11:04
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.

2 participants