Skip to content

Mail

Mailpit 1.30.3 catches everything your sites send. SMTP on 11025, web UI on 18025.

Terminal window
rex mail # list caught messages
rex mail open # open the web UI
rex mail clear # delete ALL messages

A busy local site fills Mailpit quickly, so the UI has an unread filter and Mark all read. Read state updates as you act on it rather than waiting for the next poll — a list that catches up seconds later is a list you stop trusting.

Settings → Catch all outgoing mail. On, every site’s mail goes to Mailpit instead of the internet — even when a site is configured for a real SMTP provider. The app’s own words for the off position: “Turn it off only to test a live provider on purpose.” Off, a pool gets neither half of the catch, and a site’s mail leaves exactly as the site configured it.

Absent is on, deliberately: a site created before the switch existed is caught too, because “every site’s mail is caught” must not quietly mean “every site created after you found the switch”. Flipping it installs or removes a WordPress mu-plugin and restarts the running PHP pools, so the toggle stays pending until that is true of the machine.

An agent can flip it too, through the stack tool: catch_mail turns it on, and stop_catching_mail is a system action, because in the tool’s own words it “lets every site on this machine send REAL mail to real addresses.” Both need Changes on the Agent access dial.

Three carriers, because each is a way an app escapes:

  • PHP’s own mail() — the pool’s sendmail_path shim, which is what catches a plain WordPress site.
  • A WordPress SMTP plugin — WP Mail SMTP, FluentSMTP, Post SMTP and every plugin of that shape hook phpmailer_init and call isSMTP(), after which PHPMailer opens its own socket and the shim never sees the message. Measured on a real site: wp_mail() landed in Mailpit, the same call with the site calling isSMTP() went straight to the configured host. rexenv’s mu-plugin, rexenv-mail.php, hooks the same filter last and points PHPMailer at Mailpit. Install and removal are one operation with the switch.
  • Laravel — sendmail_path cannot reach it at all: Laravel’s sendmail transport does not consult php.ini, and MAIL_MAILER=sendmail plus Mail::raw() exited 0, reported success and delivered nothing. The lever that works is Laravel’s own env precedence: a variable already in the process environment is never overwritten by .env. So the pool and the artisan runner carry nine MAIL_* keys — mailer, driver, URL, host, port, username, password, scheme, encryption — and they outrank your .env. Verified live: .env said MAIL_MAILER=log, and the message arrived in Mailpit.

The mu-plugin and the sendmail shim also reach a site on a FrankenPHP override, and the switch respawns that backend.

Anything else that sends SMTP, point at:

host 127.0.0.1
port 11025
encryption none
auth none

For a Laravel site rexenv serves, there is nothing to set — the environment above already wins. For a Laravel app run from your own terminal, MAIL_HOST and MAIL_PORT in .env with MAIL_MAILER=smtp.

Those are Mailpit’s stock ports, and MailHog’s. Using them would mean rexenv collides with a standalone Mailpit, a MailHog, or Herd Pro’s bundled copy the moment both are running.

The offset is why you can keep whatever you already have. See Ports.

rexenv has its own Mail screen: caught messages grouped by the site that sent them, and a preview with four tabs — HTML, Text, Raw source and Headers. rex mail open opens Mailpit’s own UI instead, which shows the same message plus its attachments. Between them, the two questions local mail debugging is usually about: did it send at all, and does the template survive a real mail client.

An email is the one piece of HTML in the app that a stranger wrote, so the preview treats it as one. Its HTML is rewritten before it is shown: a Content-Security-Policy meta is prepended as the first thing in <head> — script-src 'none'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none' — and then script, iframe, object, embed, base, form and meta refresh are removed, along with every on* attribute and every javascript:, vbscript: and data:text/html URL. Two layers, so an email’s scripts cannot run even if the sweep misses the tag carrying it. Styles and images stay — that is what makes an email look like the email.

Every click in the preview is cancelled first, link or not: the frame itself never navigates. An http or https link opens in your preferred browser (the OS handler if you have not chosen one). Anything else — mailto:, file:, a custom scheme — is named rather than handed to the system opener:

That link isn’t a web address, so rexenv left it alone: mailto:someone@example.test