Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Mailtrap Laravel Package

darvis/mailtrap

A Laravel package that integrates Mailtrap into your application, giving you a simple way to route outgoing mail through Mailtrap for safe testing and debugging. Configure it quickly and keep development email isolated from real recipients.

View on GitHub
Deep Wiki
Context7

Getting Started

1. Install and confirm the package name. The repository is darvis/mailtrap, but the install wizard documented at arviddejong.github.io/mailtrap is published under the maintainer's personal namespace. Check composer.json on the tag you intend to use before running composer require, and pin the constraint you verified rather than guessing:

composer require darvis/mailtrap   # verify against the tag's composer.json first

Package discovery registers the service provider; you should not need to touch config/app.php.

2. Publish config and migrations. This is not a transport-only package — it owns two tables (mail_logs and email_validations) and an inbox UI, so skipping migrations gives you a package that boots but fails on the first send:

php artisan vendor:publish --tag=mailtrap-config
php artisan vendor:publish --tag=mailtrap-migrations
php artisan migrate

3. Run the interactive wizard once, locally, by a human. The wizard wires the mailer and token selection. Since v1.12.0 it ends by asking to sponsor the package ("Buy me a beer? 🍺"), which opens https://github.com/sponsors/ArvidDeJong in a browser. Answering yes is purely a funding action — it changes nothing in the generated config. Never run this command in CI or a post-install script (see Gotchas).

4. Set credentials as environment-differentiated values. Keep a sandbox token for local/staging and a sending token for production, never the same value:

MAILTRAP_SANDBOX_TOKEN=
MAILTRAP_LIVE_TOKEN=
MAIL_MAILER=mailtrap-sandbox

5. First use case. Route all non-production mail into the sandbox so local and staging traffic is captured instead of delivered, then verify the whole chain — validation, logging, transport — with one send:

Mail::raw('mailtrap smoke test', fn ($m) => $m->to('[email protected]')->subject('smoke'));

Then look in three places, in this order: the package's inbox UI (was it captured?), mail_logs (was it logged?), and email_validations (did the recipient pass the MX check?). If all three are populated, the package is wired correctly and any remaining problem is downstream of it.

Where to look first. The published config/mailtrap.php — specifically the validation block and the token/host keys — then the migrations (they tell you what the package actually records, which is the fastest way to understand its behaviour), then the docs at https://arviddejong.github.io/mailtrap/.

Implementation Patterns

Treat validation as a send-time gate, and decide deliberately whether you want it. Every To, Cc and Bcc address passes through EmailValidation::validateEmail() on Laravel's MessageSending event: a format check, then MX record lookup and IP resolution. A missing MX record or a Null MX blocks the address; a DNS lookup that fails (timeout, SERVFAIL) lets the message through without storing a verdict. That asymmetry is the design intent — it fails open on infrastructure problems and closed on genuinely bad domains. Global kill switch:

// config/mailtrap.php
'validation' => [
    'enabled' => env('MAILTRAP_VALIDATION_ENABLED', true),
],

Disable it for high-volume bulk sends where the latency of per-domain DNS lookups on the sending path is unacceptable, and validate up front in a job instead.

Use the logs as your first-line mail debugger instead of log-grepping. mail_logs records every outgoing message, so "did we send it?" becomes a query rather than a hunt through storage/logs:

DB::table('mail_logs')->where('to', 'like', '%@customer.com%')->latest()->limit(20)->get();

Wire this into an internal support view or an artisan command for the support team — it collapses a whole class of "the customer says they never got it" tickets into one lookup, and it answers the question before anyone starts blaming the queue.

Split mailers by environment, and let MAIL_MAILER be the only delta. Define a sandbox mailer for local/staging and a live mailer for production in config/mail.php, keep both tokens in .env, and commit everything else. This is the pattern that makes an accidental staging→production mail leak structurally impossible rather than a matter of reviewer vigilance.

Add a boot-time guard so sandbox can never silently swallow real mail. The worst failure this package enables is production traffic quietly landing in a sandbox inbox. A cheap assertion in a service provider is worth more than any amount of documentation:

// AppServiceProvider::boot()
if (app()->isProduction() && str_contains(config('mail.default'), 'sandbox')) {
    throw new RuntimeException('Production is pointed at the Mailtrap sandbox.');
}

Log the resolved mailer name at boot in non-production too — it makes "why didn't the customer get it?" answerable in one glance at the log.

Treat webhook handling as a controller concern with explicit signature verification. The package processes signed Mailtrap webhook events, which means the route must bypass CSRF, must not run session middleware, and must read the raw request body for signature computation. Verify the signing secret per environment, and make the endpoint idempotent — webhook providers retry, and a duplicate event should be a no-op, not a duplicate row or a duplicate side effect. Gate the route behind the provider's IP allowlist if you have a stable egress, and log rejected signatures with the event ID so you can distinguish "attacker" from "misconfigured secret" during onboarding.

Gate the inbox UI before it ever reaches staging. An inbox UI is a full read of every message your application has sent, including password resets and magic links. In local it's a convenience; anywhere shared it's an incident waiting to happen. Restrict it to a non-production environment and an authorization gate:

Gate::define('viewMailtrapInbox', fn ($user) => $user->isAdmin() && ! app()->isProduction());

Testing: separate the two questions. Mail::fake() answers what was sent and should be the default — never let the standard suite depend on a network round-trip to Mailtrap or on DNS resolution for validation. Reach for the real sandbox inbox only when you need to verify rendered HTML, headers, or attachment encoding. For the validation layer itself, test validateEmail() directly with a known-good and a known-bad domain rather than driving it through a full send, and stub the DNS layer so tests don't depend on resolver behaviour.

Queues need parity with web. ShouldQueue mailables and notifications work unchanged, but the transport and the validation config are resolved at send time in the worker. A worker running stale config will validate and log differently from the web process. Same .env, same config:cache state, same release.

Multi-brand and multi-tenant. If different products need different sender identities or different Mailtrap accounts, register additional mailers in config/mail.php and select explicitly (Mail::mailer('mailtrap-brand-b')). Don't mutate config at runtime — that breaks config:cache and surprises the next person.

Gotchas and Tips

  • config:cache will bite you, and this package has more env() surface than a plain transport. Tokens, host, and the validation toggle must all be read through config/mailtrap.php. If any of them are read via env() closer to the call site, they resolve to null after caching — which for validation means it may silently switch off in production. Run php artisan config:clear after every local .env change, and add a deploy check that the validation flag is true where you expect it.

  • Validation runs synchronously on the sending path. DNS lookups per domain add latency to every send, including queued ones (it just moves the latency into the worker). One mitigation is already built in: lookups are skipped when another address on the same domain has already been validated, so a batch to one domain is cheap and a batch fanning out across many domains is not. Benchmark a realistic fan-out before enabling it on a high-volume list.

  • The skip-if-already-valid optimisation makes behaviour order-dependent. A domain validated earlier in the same process inherits its verdict. That's fine in production and confusing in tests — a test that passes in isolation can behave differently in a suite where an earlier test already validated the domain. Reset or stub the DNS layer between tests.

  • "Validated" is not "deliverable". The check is format + MX + IP resolution. It will happily pass a nonexistent mailbox on a real domain, and it will block a legitimate domain during a resolver outage that returns Null MX rather than SERVFAIL. Don't surface this to users as "this email address is invalid" — surface it as "we couldn't confirm this address" and always retain an override path.

  • DNS failures fail open, by design — make sure that's what you want. Timeout or SERVFAIL lets the message through with no stored verdict. Good for availability, bad if your team assumes nothing sends without a validation row. Query email_validations for blocked addresses; treat an absent row as "not checked", not "checked and fine".

  • Logging every outgoing mail has cost, and it's mostly PII. mail_logs grows with volume and contains recipients, subjects, and possibly bodies. Set a retention policy and a scheduled prune from day one — retrofitting one after the table is a million rows deep is a painful migration. Confirm whether bodies are stored before you let the inbox UI into any environment with real customer data, and check the package against your data-retention obligations.

  • Attachments count against provider limits. Stream from storage rather than loading into memory, and test one attachment-heavy mailable in the sandbox before shipping. This is the class of bug that only appears at 11pm with the one invoice template nobody tested.

  • The wizard is interactive and now ends by opening a browser. Don't invoke it from CI, Docker builds, or post-install-cmd. Generate the config once, commit config/mailtrap.php, and let env vars be the only per-environment delta. If you inherited a provisioning script that calls it, that script will hang or try to spawn a browser.

  • Sandbox ≠ production credentials, and the failure is silent. A sandbox token accepts mail and never delivers. Combined with an aggressive queue retry policy, a staging misconfiguration can look like success in every signal you have except the customer's inbox. The boot-time guard above plus a log line naming the active mailer is the cheapest insurance available.

  • Rate limits and retries belong on the job, not in a loop. Use backoff() and retryUntil() rather than tight retries; let permanent 4xx responses fail fast. A Mailtrap outage surfacing as queued mail exceptions will amplify load if retries are aggressive — set tries and backoff deliberately for mail jobs specifically, not just globally.

  • Webhook debugging checklist. 419/CSRF error → route not excluded from VerifyCsrfToken. Signature always rejected → reading $request->all() instead of the raw body, or the secret differs per environment. Events processed but no effect → duplicate event already handled, check idempotency keys. Events missing entirely → the endpoint isn't reachable from Mailtrap's egress, which means local development needs a tunnel.

  • Upgrading to v1.12.0 needs no migration. The release changes only the wizard's closing prompt (star → GitHub Sponsors) and raises Larastan to level 8 with typed reads of command options/arguments instead of string casts. No config or API changes. Safe to take on a patch bump.

  • Plan for the bus factor. Single maintainer, no commercial support tier, and the Sponsors link is funding rather than an SLA. Pin to a caret constraint, review changelogs on minor bumps, and document the exit path now while it's cheap — because the integration is config-driven and plugs into Laravel's native mailer, dropping back to plain SMTP pointed at Mailtrap is a config/mail.php change plus a regression run of your mail tests. Write that one-paragraph escape hatch down somewhere the next on-call engineer will find it.

  • Extension points beat forking. The transport and HTTP client are container-bound, so swap in a custom Guzzle handler for corporate proxies or a test double for asserting outgoing payloads, rather than patching the package and taking on the maintenance.

  • Mail-vanished debugging order, when you're under pressure: MAIL_MAILER value → mailer block present in config/mail.php → config:cache state → check mail_logs (did the package see the send?) → check email_validations (was the recipient blocked?) → check the inbox UI (did it arrive?) → only then suspect your own code. In practice the answer is nearly always in the first two steps or in the difference between the sandbox and live token.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect