Your Laravel app might be emailing customers their own API keys right now. Outgoing mail is the security blind spot almost nobody audits. A debug variable left in a Blade template, a misconfigured mailable, a stack trace forwarded to support, and suddenly a real secret or a customer's personal data is sitting in an inbox you do not control. Once it sends, it is gone. There is no recall, no patch, no second chance.
We spend most of an audit on what comes into an app: form input, query strings, uploads. The mail that goes out gets almost none of that scrutiny, even though it renders the same untrusted data through Blade and ships it straight past every firewall you own. So I built laravel-mail-guard: an open-source package that scans every outgoing message before it leaves your app, and refuses to send the dangerous ones.
A mailable is a template that runs with whatever you hand it. The failure modes are boringly common:
$user fields you never meant to send.None of these throw an error. They render, they queue, they send. And an inbox is the worst place for a secret to land: it is retained for years, often readable by support staff, frequently forwarded, and rarely encrypted at rest. You cannot rotate your way out of a key that is already in a thousand mailboxes.
Every message is run through a set of rules before it is handed to the transport. The defaults that ship today:
secrets.private_key · critical: a private key (PEM block) that ended up in the message.secrets.stripe_key · critical: a Stripe secret or live key in the body.pii.credit_card · critical: a credit-card number sitting in plain text.compliance.list_unsubscribe · warning: bulk mail with no List-Unsubscribe header.privacy.tracking_pixel · warning: a remote tracking pixel quietly phoning home.Critical findings are the ones that can stop a send; warnings are logged so you can triage them. The list is a starting point, not a ceiling: you add your own rules through one small contract, covered below.
That is the whole setup. The guard hooks into Laravel's mail pipeline automatically, so every Mail::send() and queued mailable in your app is now scanned. It runs on Laravel 11, 12 and 13, PHP 8.2 and up.
By default the guard is non-destructive: it scans, logs and stores a redacted copy, but it never stops a send. When you are ready to enforce, flip one environment variable so a critical finding hard-stops the message in production:
While you are developing, point your browser at the local inbox to see exactly what your app is about to send, findings highlighted, before it goes anywhere:
The runtime guard protects production. The scan command protects the pull request. It renders your mailables, runs the same rules, exits non-zero when a critical fires, and writes SARIF so the findings show up as code-scanning alerts right on the diff:
Wiring it into GitHub Actions is two steps: run the scan, upload the SARIF.
Now a mailable that starts leaking a key fails the build, and the reviewer sees the finding inline instead of discovering it in a customer's inbox three weeks later.
The package ships test helpers so the same rules become assertions. Drop them into a feature test for any mailable that touches sensitive data:
The full set is assertNoFindings(), assertNoCriticalFindings(), assertFlagged($ruleId) and assertNotFlagged($ruleId). Use them to pin down a known-good template so a future refactor cannot quietly start leaking.
The built-in rules cover the obvious secrets and PII. Your leaks are probably more specific: an internal hostname, a coupon code, a particular ID format. One contract covers it:
Register it under scan.rules in config/mail-guard.php and it runs everywhere the defaults do: at send time, in the scan command, and in your tests. One rule, three enforcement points.
Most teams do not know this gap exists until a secret lands in the wrong inbox, and by then the only options are apology emails and key rotation. laravel-mail-guard is a five-minute install that closes the gap before that happens: Laravel 11, 12 and 13, PHP 8.2 and up, MIT licensed. The copies it stores are redacted, so you get an audit trail of what went out without keeping a second copy of the secret itself.
Source, issues and rule ideas are all welcome on GitHub: github.com/laravelsecurityaudit/laravel-mail-guard. If it catches one leak for you, it has paid for itself.
A scanner catches the patterns it knows about. If you want a senior engineer to read every mailable, queued job and exception handler the way an attacker would, that is what a security audit of your AI-generated app is for.
The runtime check runs as the message is built and is cheap; mail is already an I/O-bound, usually queued operation, so the scan is noise next to the SMTP round trip. The heavier full render lives in the mail-guard:scan command, which you run in CI, not on the request.
Not by default. Out of the box it only warns and logs. Blocking is opt-in through MAIL_GUARD_BLOCK=true, and only critical-severity rules stop a send. You can lower the severity of any rule, disable it, or add an allow-list rule in config/mail-guard.php.
The stored copies are redacted: anything a rule flags as a secret is masked before the record is written. You get an audit trail of what your app sent without persisting the secret a second time.
Laravel 11, 12 and 13 on PHP 8.2 or newer, released under the MIT license.
A senior engineer reviews every line your assistant wrote, mailables included. Fixed price, every finding with a fix.