Changelog
Every release of Mailroom, newest first, with what each one added, changed and fixed. Unreleased is what the next one holds.
The same notes are in CHANGELOG.md on GitHub.
Unreleased
Changed
- The mailbox's tab icon is drawn the same as the mark in its sidebar.
0.10.0 - 2026-10-19
Run php artisan migrate after upgrading. Addresses move into a table of their own, and the migration copies every stored message's addresses across before dropping the old columns. It can be rolled back.
Added
- A sidebar of mailboxes. Inbox, Unread (for a signed-in reader) and Spam checked, which lists every scored message highest score first, so the one worth looking at is at the top. The sidebar shows one view at a time, like folders: a mailbox, a person or a mailer, with search narrowing whichever is showing.
- People. The sidebar lists the ten addresses on the most messages, as sender, recipient, Cc or Bcc, with a count of the rest, and clicking one lists everything it appears on. A search box finds anyone else by address or name.
- Search as you type. The message search and the people search both update a moment after you stop typing, with the matches highlighted. The message search keeps the address bar in step and, as in Gmail, closes the open message to show the results. An × in either box, or
Esc, clears it. - Keyboard shortcuts.
jandkopen the next and previous message, and/jumps to search. Nothing else is bound. - Coloured counts. Attachment counts are blue, and a spam score is green below 2, amber from 2, and red from SpamAssassin's threshold of 5, in the list, the tab row and the spam pane alike.
- An address API on the message.
addressesFor(AddressRole::To),sender()andenvelopeSender()read the newmailroom_addressestable, and theAddressCollectionthey return hasdisplay(),emails()andincludes(). The table name isdatabase.addresses_table. - A tab icon. The mailbox names its own icon, inline, so its tab stands apart from the application's and the browser stops asking for
/favicon.ico.
Changed
- Confirmations are part of the page. Delete and Clear all ask in the mailbox's own dialog, naming the message or the count, with Cancel focused. Forwarding asks inside its dialog: the first Send names the address, and the second sends.
- The mailbox is redesigned. Three columns (sidebar, list, message), the message in a card with an icon toolbar, segmented tabs, and the recipients line opening the full set of addresses in place of a Details button. Mailers are sidebar views in place of the mailer select, and appear only when more than one mailer has captured mail.
- Rows lead with the recipient. In a dev mailbox the sender is nearly always the application itself, so each row shows who the message went to, with an avatar.
- Tablets and phones get layouts of their own. The sidebar becomes a rail of icons on a tablet. On a phone the list and the message are separate screens, the toolbar shows its labels, and the forward dialog sits at the bottom of the screen.
- Addresses are rows in
mailroom_addresses, one per address and role, with the display name as it appeared on the message.
Removed
- The
from,to,cc,bcc,reply_to,envelope_recipientsandenvelope_sendercolumns onmailroom_messages, and the model attributes of the same names. Read addresses through$message->addressesFor(AddressRole::To)and the other roles,$message->sender()and$message->envelopeSender(). Anything querying those columns directly needs to querymailroom_addresses. MailroomMessage::formatAddressList()andformatAddress(). UseaddressesFor(...)->display()for a list andMailroomAddress::display()for one address.
Fixed
- Searching for "address" or "name" no longer matches every message, and a display name with non-ASCII characters, such as "Müller", can be found.
- Searching for text containing
%or_, such as "50%" or "user_id", finds that text on SQLite and SQL Server, where it found nothing. - Search ignores case on PostgreSQL, as it does on the other databases.
- Deleting a message returns to the list it was opened from, with its search, filters and page, rather than to the first page of everything.
0.9.0 - 2026-09-19
Run php artisan migrate after upgrading. The spam check keeps its result on the message, which needs three new columns.
Added
A spam check, through Postmark's SpamCheck API. A message can be scored by SpamAssassin from its own pane, which shows the score, the rules that fired, and what each one cost. Set
MAILROOM_SPAM_DRIVER=postmarkto switch it on, then check a message whenever you want, and check it again after you change the template.This is the one feature that sends captured mail out of your application: the whole message goes, attachments included. So the route does not exist without a driver, checking needs a signed-in user outside
local, the pane says what it sends before it sends it, and nothing leaves on anything but a click.The score is SpamAssassin's rather than ours, and it is blind to authentication and reputation. Mailroom captures before your relay signs and sends, so there is no SPF, DKIM or DMARC result to read, no Received chain, and no sending IP or domain to look up. The content and the headers are what get scored.
Changed
- Counts in the tab row are badges now, the same ones the message list uses for an attachment count. One fact was being written two ways, faintly beside a tab and as a pill in the list. This is what the attachment count looks like as well, and a spam score over the threshold takes the accent colour.
0.8.0 - 2026-09-16
Run php artisan migrate after upgrading. The mailbox needs the new table before it can track what has been read.
Added
Read and unread, for signed-in users. Opening a message marks it read. Unread rows keep the bolder subject and carry a dot, and the header counts what is still unread, following whatever filter is applied. A message can go back on the pile with Mark unread, and a whole page can be marked either way at once.
It is per person, so two testers sharing a staging mailbox never clear each other's mail. Where nobody is signed in there are no markers, no controls and no count, so a local mailbox with no login looks exactly as it did.
0.7.0 - 2026-08-31
Added
- A Details disclosure in the message header. Cc, Bcc, Reply-To, tags and metadata sit behind it, with the full labelled list of every address. Whether it is open is remembered between messages.
Changed
The message header is about half as tall, and now the same height for every message. It used to be three stacked blocks, so one carrying Cc, Bcc and Reply-To pushed the email much further down the pane than a plain one, and the body shifted as you moved through the list.
It is two rows now: subject and actions, then who it came from, who it went to and when. Opening Details replaces that line rather than repeating it, so every address appears once and in full. Nothing was dropped, and the Headers tab still lists every recorded header.
The README names queue workers as the usual cause of missing attachment bytes. A worker captures the mail and writes the files the web process reads back, so running them as separate containers on a
localdisk breaks downloads the same way an ephemeral platform does. It was documented only under Laravel Cloud before, where nobody on a single server would think to look.
Fixed
- Small text that failed WCAG AA contrast, the preview line under every subject in the message list included. It now clears the minimum for its size, which is most of why the list was harder to read than it looked.
0.6.0 - 2026-08-23
Added
Desktop, tablet and mobile preview. The rendered body can be viewed at 768px and 375px as well as full width, so you can see whether an email holds together on a phone without forwarding it to one. An iframe carries its own viewport, so the message's own media queries fire at the chosen width — this is a real layout at that size, not a scaled picture of one.
Desktop is the default and is unchanged from 0.5.0, so nothing looks different until you pick another width. The choice is remembered between messages.
Changed
composer serveno longer stops after five minutes. Composer applies a 300-second timeout to every script it runs, which was killing the demo server mid-session; theservescript now clears it for itself, leavingtestandlintbounded as before. This affects working on the package rather than applications that install it.
Fixed
- The message list showed a line of CSS as the preview snippet for any message whose HTML carried a
<style>block, which most templated mail does. Only the tags were being removed, leaving the stylesheet itself to stand in as the summary; the contents of<script>elements arrived there the same way. Both are now dropped along with their tags.
0.5.0 - 2026-08-16
Added
MAILROOM_FORWARD_ALLOWEDbounds where a message may be forwarded. Entries are whole addresses, or@domainto permit everyone there. Left empty, which is the default, any address is permitted — so nothing changes for an existing install until you set it.MAILROOM_FORWARD_RATE_LIMITcaps forwards a minute, counted per signed-in user and otherwise per IP, defaulting to ten. Set it tonullto remove the ceiling. Counting runs through your cache store, as Laravel's throttling does.Exceeding it reopens the dialog with an explanation rather than returning a browser-facing 429, so a refusal reads like every other forwarding failure.
0.4.0 - 2026-08-06
Added
A Forward button on each message. Set
MAILROOM_FORWARD_MAILERto a mailer that can deliver and a tester can send one message to a real inbox to see how it renders in Gmail or Outlook. Nothing is sent without a click, so SMTP covers only what someone actually checked.Sending to the original recipient replays the stored MIME byte for byte. Sending anywhere else rewrites
To, dropsCc, and keeps the originals asX-Mailroom-Original-ToandX-Mailroom-Original-Cc. Only the chosen address receives a copy, and the stored message is never modified.The button is present whether or not forwarding is configured — without a mailer it explains how to switch it on, which is how anyone finds out the feature exists. The route that does the sending is never registered until you configure one.
MAILROOM_FORWARD_REQUIRE_AUTH, on by default. Outside thelocalenvironment a forward needs a signed-in user, so opening the mailbox up with a permissiveMailroom::auth()callback grants reading without also granting the ability to send from your domain.
0.3.2 - 2026-08-06
Fixed
- A database refresh no longer deletes captured mail during a test run. Laravel's
RefreshDatabaserunsmigrate:freshas it boots each test, which fired the storage cleanup added in 0.3.1 — before a test could callStorage::fake(), so it emptied the real disk. Anyone running their suite lost the mail they had captured while developing. The refresh belongs to a throwaway test database, so it is no longer acted on while tests are running.
0.3.1 - 2026-08-05
Fixed
migrate:freshandmigrate:refreshleft every captured message's.emland attachments on disk. Wiping your database now takes the stored files with it. Upgrading will not reclaim orphans from earlier releases — runphp artisan mailroom:clearonce to remove those.- Storage is emptied only when the refreshed connection is the one Mailroom uses, so a
migrate:freshelsewhere leaves mail on a separateMAILROOM_DB_CONNECTIONintact.migrate:rollbackandmigrate:resetstill leave files behind. mailroom:clearreportedCleared 0 captured message(s)while deleting orphaned files. It now reports the stored files separately from the row count.
0.3.0 - 2026-08-04
Contains a breaking change. If you set MAILROOM_FORWARD, remove it — see below for why, and for what replaces it.
Added
- A Staging and QA section in the README. Mailroom is as useful on a shared staging environment as it is locally: testers read captured mail in the browser without a third-party catcher, without shared inbox credentials, and without mail leaving your infrastructure. Covers enabling it where the platform reports
APP_ENV=production, gating it behind your own login, and pointing the disk at persistent storage.
Removed
mailroom.forwardandMAILROOM_FORWARD. Naming another mailer there relayed every captured message on send, to the message's original recipients — so pointing it at working SMTP on a staging environment delivered test mail to real customers. It also billed an SMTP send for every message whether or not anyone wanted it delivered.Deliberate, per-message forwarding from the mailbox replaces it in 0.3.1: everything is captured, and a message goes out only when someone opens it and chooses to send it, which is the point at which you want a real client to render it.
If you relied on capture-and-deliver, keep a second mailer configured and select it directly for the mail you want delivered.
Changed
- The transport is now terminal in all configurations, which removes the self-forward and recursion guards it needed to carry.
0.2.0 - 2026-08-01
Contains breaking changes. Two public surfaces were removed, so this is a minor rather than a patch release. If you pinned ^0.1, Composer resolves that to 0.1.* and will not pick this up until you bump the constraint deliberately.
Added
mailroom:installcan now run the migrations, so installation is a single command. It refuses to do so while the package is disabled, because the migrations are not registered in that state and running them would only execute the application's unrelated pending migrations.- Flags for every prompt, so the installer can run unattended:
--set-mailer,--migrate,--no-migrate,--no-config.
Changed
- The README no longer implies
mailroom:installis required. It is a convenience: the provider is auto-discovered, config is merged, migrations are loaded from the package, and themailroommailer is registered, socomposer requireplusmigrateplusMAIL_MAILER=mailroomis enough.
Removed
- The
mailroom-migrationspublish tag. Publishing put a copy indatabase/migrationsthat ran regardless ofMAILROOM_ENABLED, defeating the guard that keeps a production deploy from gaining the tables. Table names and the connection are already config-driven; schema changes belong in your ownALTERmigration. - The global
Mailroomclass alias. It was never a facade, and the class is imported normally.
0.1.0 - 2026-07-30
First release.
Added
- A
mailroommail transport that captures every outgoing message: HTML and text bodies, attachments, inline images, tags, metadata and custom headers. It hooks in as a Symfony transport, so it sees the finished message rather than reconstructing one. - An auth-gated mailbox at
/mailroomwith search, a per-mailer filter, pagination, and light, dark and system themes. No build step and no published assets — the styles are inlined. - Attachment previews in a lightbox: images, SVG, PDF, audio, video, text, Markdown, CSV and TSV as tables, pretty-printed JSON, source files,
.icsinvites and nested.emlmessages. Media supports HTTP Range, so seeking works. Office documents and archives are download-only. - Export as
.eml, or as a standalone.htmlfile with embedded images rewritten todata:URIs. - Three levels of access control: local-only by default, a
viewMailroomgate, or aMailroom::auth()callback. The package ships no login page and defers to the host application's. mailroom:install,mailroom:pruneandmailroom:clearcommands. The message model isPrunable, somodel:pruneworks too.- A
MessageStoredevent carrying the persisted row, which Laravel's ownMessageSentdoes not provide. - Optional forwarding, so a captured message can also be delivered through another mailer.
Security
- Disabled in production by default. The transport throws rather than silently accepting mail, and the mailbox routes are never registered.
- Attachment previews serve a content type taken from an allowlist, never the attachment's own. Anything text-shaped,
.htmlincluded, is escaped into the page rather than served, and SVG is rendered only through<img>under asandboxcontent policy. - Attachment downloads are always
application/octet-streamwithContent-Disposition: attachment.