Parts · v0.7.0
dev-mailbox
A live local inbox at /dev/mailbox: HTML/text/raw/headers, width and dark previews, codes and links with copy, attachments, search, checks, previews, JSON and a CLI. Development only.
- Add it
cargo offrails add dev-mailbox- Requires
- mailer (added first)
- Needed by
- auth-core
- Files
8 files at app paths, 15 wiring lines
src/kernel/mailbox_cli.rssrc/kernel/mailbox_web.rsstatic/kernel/mailbox.cssstatic/kernel/mailbox.jstemplates/kernel/mailbox/_list.htmltemplates/kernel/mailbox/_message.htmltemplates/kernel/mailbox/index.htmltests/app/dev_mailbox_test.rs
.offrails/template/parts/dev-mailbox/README.md
dev-mailbox
Every email your app sends in development, in a real inbox at /dev/mailbox: live as it arrives, rendered safely, with the sign-in link or one-time code one keystroke away. Rails has letter_opener, Phoenix has Swoosh's mailbox; this is the one that also tells you why your email will break in Gmail.
- Live inbox over Server-Sent Events: mail from a request, a job or a task appears within a second, with a count in the tab title. Pause and resume.
- Message view: the HTML part in a sandboxed frame (no script, opaque origin), the text part, the raw RFC 822 source, every header.
- Desktop, 600 px, 320 px and Dark previews. Dark applies the mail's own dark-mode CSS, or the full-color inversion many mail apps do.
- Codes and links pulled out of the message with big copy buttons; links sent while the app ran on another port get an "Open here".
- Checks before real inboxes see it: relative URLs, a missing text part, images without alt text, Gmail's 102 KB clipping, a layout wider than a 320 px phone (a fixed-width banner, table or block that no
max-widthreins in),<script>, a long subject,List-Unsubscribe. - Attachments listed with size and type, raster images as thumbnails, always downloaded (never rendered on your origin).
- Search, delete, clear,
.emldownload, unread state. - Mailer previews: every mailer rendered from sample data in the same viewer, never sent.
- Keyboard:
j/knext and previous,/search,ccopy the code,oopen the first link,edownload,ddelete,1–4tabs,?the list. - For agents and tests:
/dev/mailbox/latest.json?to=…and/dev/mailbox.json?to=&subject=&since=with links and codes;app mailbox tail|list|show|clear|seedin a terminal. - It styles itself:
static/kernel/mailbox.cssandmailbox.jsship with it and every mailbox page links them. Light and dark, following the kernel header's System / Light / Dark switch (tokens withlight-dark(), theming), WCAG 2.2 AA (axe-clean in both), usable at 320 px. The Dark preview is about the email and works whatever the tool's theme. - Development only: in production the routes don't exist (404, proven by a test).
Extracted from Rust off Rails apps and rebuilt. Fixed on the way: an app moved the mailbox styles into app.css, which kernel pages never load, so the mailbox rendered unstyled; <base target="_blank"> could land before the doctype (quirks mode); the inbox polled every 2 s per tab. Added: the live stream, codes, checks, previews in four widths and themes, keyboard, JSON filters and the CLI.
Guide: .offrails/guides/mailers.md#the-dev-mailbox · .offrails/guides/dev-tools.md. Depends on: mailer.
Screens
Every screen in light and dark, on desktop and phone: the samples repository.
Install
By hand, in the app root, after the mailer sample:
- No crates to add.
- Copy
files/into the app, keeping the paths. In the test file and themain.rswiring, replaceoffrails_hostwith your crate name. - Wiring (each at its
samples: <anchor>comment when the app has one; otherwise at the spot described):src/kernel/mod.rs:pub mod mailbox_cli;andpub mod mailbox_web;.src/kernel/ui.rs: aTab { key: "mailbox", label: "Mailbox", href: "/dev/mailbox", dev_only: true }in the kernel header'sTABS.src/routes.rs: insideif config.is_development() { … },app = app.merge(crate::kernel::mailbox_web::router(crate::mailers::previews::PREVIEWS));; apaths::dev_mailbox()helper; the sixteenROUTESrows, allAccess::Dev(they're insample.toml).src/controllers/dev.rs: the Mailbox and Mailer previews tiles, and the "Send a test email" button.src/kernel/introspect.rs:out["mailbox"]with the counts.src/main.rs: theMailbox { command }subcommand and its dispatch arm.templates/layouts/application.html: a footer link<a href="{{ paths::dev_mailbox() }}" hx-boost="false" class="link">Mailbox</a>next to Dev tools.tests/app/main.rs:mod dev_mailbox_test;;tests/app/security_test.rs:"src/kernel/mailbox_web.rs"in the route sources;tests/app/kernel_web_test.rs: the mailbox paths in the no-store list;tests/app/theme_switch_test.rs:"/dev/mailbox"and"/dev/mailbox/previews"inKERNEL_PAGES.
cargo fmt --all && cargo run -- routes(rewritesdocs/routes.md), then the Definition of Done.
tools/install.sh dev-mailbox <app> installs mailer first if needed, then does steps 1 to 3.
After installing
- Every link from an app page into the mailbox carries
hx-boost="false"(a kernel page has its own<head>; boosted, it renders unstyled).kernel_web_test.rs::app_links_into_kernel_pages_are_not_boostedchecks it. - Try it:
cargo run -- mailbox seed --app-url http://localhost:3000, open/dev/mailbox, press?. - Agents: after a flow that sends mail,
curl -s $URL/dev/mailbox/latest.json?to=ada@example.com | jq '.links[0], .codes[0]', orcargo run -q -- mailbox tailin a second terminal.
Adapting it
- More messages per page, or a different poll:
LIST_LIMITandPOLLinkernel/mailbox_web.rs(// APP:). - Detect your product's codes:
WORDSinkernel/mailbox.rs(from the mailer sample). - Your own checks: add a rule to
checks()inkernel/mailbox_web.rs(add("warn", …)), and a case tochecks_flag_what_breaks_in_real_inboxes. - A mailbox on a staging server: don't route this one there. Run staging with
MAILER=smtpagainst a catcher (Mailpit), which is built for shared use; this mailbox assumes the only person who can reach it owns the machine. - Brand tones: the
--mbx-*tokens at the top ofstatic/kernel/mailbox.css(/* APP: */), eachlight-dark(<light>, <dark>); the shared ones (--accent,--panel…) are inadmin.css. Keep AA contrast in both and never add aprefers-color-schemeblock (it ignores the theme switch). - Assert on mail in end-to-end tests:
GET /dev/mailbox.json?to=…&since=<id>from the browser test, orapp.sent_emails()in Rust tests.
Do not change
- The iframe and the body response stay sandboxed without
allow-scriptsandallow-same-origin(the_html_body_is_served_alone_in_a_sandbox). - The router is merged only in development (
mailbox_routes_do_not_exist_in_production_or_test). - Attachments are always downloads with a sandbox CSP; no SVG thumbnails (
attachments_are_downloads_never_documents_on_this_origin). no_store()on the router (every_mailbox_response_is_no_store).- Mailbox styles live in
static/kernel/mailbox.css, linked by the page'sChrome, never inassets/app.css(every_mailbox_page_links_its_own_stylesheet_and_script).
How it works
The mailer sample's Mailer::Local stores each message in mailbox_messages. This sample reads that table:
| Route | Does |
|---|---|
GET /dev/mailbox | the inbox, newest message open; ?q= searches subject, address and text |
GET /dev/mailbox/{id} | message id open (marks it read); ?tab=html|text|raw|headers, ?view=desktop|600|320|dark |
GET /dev/mailbox/{id}/html | the HTML part alone, for the sandboxed iframe; ?scheme=light|dark |
GET /dev/mailbox/{id}/message.eml | the exact message, as a download |
GET /dev/mailbox/{id}/attachments/{n} | attachment n, as a download |
POST /dev/mailbox/{id}/delete, POST /dev/mailbox/clear | housekeeping, with a flash |
POST /dev/mailbox/test | a sample message, stored locally (never SMTP) |
GET /dev/mailbox/events?since= | Server-Sent Events: mailbox with {newest, total, unread} whenever they change |
GET /dev/mailbox/list?q=&selected= | the list fragment the live inbox swaps in (counts in X-Mailbox-Total/-Unread) |
GET /dev/mailbox/previews, /previews/{mailer}/{action}, …/html | mailer previews in the same viewer |
GET /dev/mailbox.json?to=&subject=&since=, /dev/mailbox/latest.json?to= | for agents and tests |
GET /dev/mailers | 301 to the previews (older apps linked here) |
The stream polls the table once a second per open tab (development only), so mail from another process, a worker or a task, shows up too. The page swaps in the list fragment and announces new mail to screen readers. Without JavaScript everything still works by plain links and forms.
Files
| File | What |
|---|---|
src/kernel/mailbox_web.rs | the router, views, checks, the event stream, in_scheme |
src/kernel/mailbox_cli.rs | app mailbox tail|list|show|clear|seed |
templates/kernel/mailbox/index.html, _list.html, _message.html | the two panes |
static/kernel/mailbox.css, mailbox.js | its own styles and behavior |
tests/app/dev_mailbox_test.rs | the tests named in CHECKS.md |