A permanent, delete-proof mirror of company email. Every message that arrives or leaves is copied into a private archive that has no delete button, no delete code, and no delete permission — then made searchable, auditable and answerable from one Gmail-shaped interface.
A construction and consulting firm runs on its correspondence. Approvals, drawings, bill submissions and tender notices all arrive by email, and a single deleted thread can undo months of a contract position. Mail Ledger removes that risk by keeping a second copy that nobody — including the administrator — can alter.
The system reads the company's Google Workspace mailbox with a read-only credential and writes each message into a private database on the firm's own server. Staff sign in to a Gmail-shaped portal to read, search, download and reply. Deleting mail in Gmail has no effect on the ledger.
A second phase absorbed the firm's legacy Yahoo mailbox — 28 GB that could not be migrated into a 30 GB Workspace account. Message bodies went to the server, attachments to object storage, and the address remains fully usable: mail sent from the portal can still go out under it.
Modelled closely on Gmail, deliberately. Staff open this beside their own mailbox all day; anything they have to learn is friction, and friction is what makes people keep a copy of the mail somewhere else.
from:,
has:attachment,
filename:,
before:,
larger:, quoted phrases and
negation — over a MySQL full-text index, across five years of mail.
Screenshots are of the live system with correspondent names, addresses, subjects and IP addresses replaced by invented equivalents. Layout, counts and behaviour are untouched.
PHP 8 and MariaDB on shared hosting, with no
framework and no package manager — every dependency is written out, because the deployment
target offers no Composer and no build step. Google is reached through two separate service
accounts with domain-wide delegation: one authorised for gmail.readonly,
the other for gmail.send. Neither can do the other's job, and that was
verified by offering each the other's scope and watching Google refuse.
The legacy mailbox is read over IMAP in resumable slices, three concurrent workers at a time — a fourth connection earns an immediate rate-limit refusal from the provider. Attachments are content-addressed by SHA-256 so an identical drawing sent to eight people is stored once, then pushed to Cloudflare R2 with a Signature V4 implementation written by hand over curl.
Sender HTML never reaches a browser unmodified. A sealing pass rewrites every remote reference out of the markup before it is served, because the host's CDN discards Content-Security-Policy headers and substitutes its own — measured, not assumed. The message body then renders inside a sandboxed frame, and any image the reader chooses to see travels inside the page as bytes rather than as a link the frame's opaque origin would refuse.
The old mailbox was larger than the Workspace account meant to receive it. Splitting it solved the arithmetic — bodies and headers into MySQL, 19.5 GB of attachments into object storage with no egress fee — while the portal presents both halves as one mailbox.
The message list stopped counting at 5,000 and displayed "many", on the assumption that an exact count would be expensive. Measured, the exact count took 16 ms against the ceiling's 37 ms — the optimisation was slower than the thing it replaced, and told the reader less.
Reply-rate analysis written as a
correlated subquery re-scanned the table once per message, each pass an unindexable
LIKE over recipient lists. Reading both sides once and walking
them together with a moving cursor produced identical answers in 30 ms.
Each import worker treated a pass as complete only when it stored and skipped nothing — a state a finished slice never reaches, because what remains in it is promotional mail that is skipped on every pass. Three workers cycled indefinitely, holding the connections the live sync waited for, so the archive looked busy while new mail quietly stopped arriving.
Message bodies render in a sandboxed
frame, which the browser gives an opaque origin — and under an opaque origin
img-src 'self' matches nothing, so same-origin image URLs were
refused before any request was made. Embedding the bytes in the page fixed it without
weakening the sandbox that keeps hostile mail away from the session.
Five years of correspondence — back to July 2021, across 1,343 correspondents — is now searchable in one place and cannot be removed from it. Two mailboxes on entirely different platforms behave as one, and mail can be answered from either address without leaving the portal.
The dashboard surfaces what is genuinely waiting on a reply rather than everything unanswered, which turned a four-thousand-row backlog into a list somebody works through in a morning. Replies are answered within a day 92% of the time, measured across the last year of threads.