InboxUnpack

How it works

Everything below happens on your device. This page is the technical account of what that means — useful if you are deciding whether to trust it with correspondence, and useful if you are wondering why a particular message renders the way it does.

Choosing a file starts everything; loading a page starts nothing

A page here is HTML, CSS and a small amount of JavaScript. No worker is created, no PDF library is imported and no font is fetched until you pick a file. That is not incidental — it is asserted by a test that loads every page, scrolls, waits, and fails if any heavy asset was requested.

Work happens in Web Workers

Parsing, mailbox indexing, base64 decoding, SHA-256 hashing, PDF layout and ZIP building all run in workers, so the interface stays responsive and the cancel button keeps working while a gigabyte of mailbox is being scanned. How many workers run at once is decided from the device: the logical core count with one held back for the interface, capped at four; one on a phone; one on Safari.

Safari gets a sequential queue because WebKit’s per-tab memory ceiling is substantially lower than Chromium’s, and two workers each holding a several-hundred- megabyte buffer is the reliable way to have the tab killed mid-batch. The reason is shown in the queue panel rather than left as an unexplained slowdown.

MIME is parsed over bytes, never over strings

The parser works on Uint8Array from beginning to end and never converts a body to a JavaScript string on the way in. A 40 MB base64 attachment turned into a string first costs 80 MB of UTF-16 before a single byte is decoded, and doing that twice inside a hundred-message batch is how a tab dies. Only header blocks — small and bounded — become text.

Boundary detection requires the delimiter to start at a line boundary, which is what stops a boundary string appearing inside a base64 blob from splitting a message. Nesting is capped, part count is capped, and every recoverable problem — a missing boundary, an unterminated final part, a multipart with no boundary parameter, invalid base64 — is recorded as a note on the message rather than thrown away or thrown at you.

.msg is a filesystem, not a message

An Outlook .msg is a Microsoft Compound File: a FAT, a mini-FAT for small streams, a red-black directory tree, and a mini-stream. The reader here implements that from the specification, and every chain walk in it has a visited set and a hard cap — because those bytes came from a file somebody sent you, and a self-referential FAT entry in a two-sector file is otherwise an infinite loop running on attacker-chosen input. The test corpus includes three fuzzer-minimised files from Apache POI precisely to exercise that.

On top of the container sit MAPI properties. Text properties come in Unicode and non-Unicode flavours, and the non-Unicode ones are in a code page that the file may or may not name correctly — the corpus contains a message declaring UTF-8 for properties that are plainly windows-1252. Resolution goes: the message code page if stated; the internet code page unless it is UTF-8, which cannot describe an 8-bit property; the ANSI code page implied by the message locale; then windows-1252 with a warning.

When a .msg has no HTML property, its rich body is a compressed RTF stream. That is decompressed with LZFu — a 4 KB ring-buffer dictionary preloaded with 207 bytes the specification prescribes exactly — and, when the RTF is an HTML encapsulation, the original markup is reassembled from the \htmltag destinations and \htmlrtf toggles. That path is why messages other readers show as blank have a body here. When the RTF is genuine rich text there is no original HTML to recover, and the viewer says so instead of inventing one.

MBOX is indexed, not loaded

A mailbox is scanned once for message boundaries. A line beginning From is only accepted as a separator if a header block follows it, which is the difference between three hundred messages and seven hundred fragments. Then the header block — and only the header block — of each message is read to build the list. Bodies and attachments are parsed one at a time, when you open one.

From-line escaping differs between the mboxo and mboxrd conventions, and unescaping the wrong one silently eats a >from every quoted reply in the file. The convention is detected from the file’s own contents and reported in the mailbox notes; when it genuinely cannot be determined, the file is read as mboxrd, which leaves unescaped text unchanged, and the ambiguity is stated.

Message HTML: four layers

  1. Sanitisation. DOMPurify with script, form, iframe, object, embed, meta, base, link, svg and more forbidden outright; every on* attribute stripped; every URL scheme other than cid:, data:image/, mailto: and https: on anchors rejected. CSS is filtered separately, in both <style> blocks and inline attributes: url() and @import removed — they are the only way CSS can cause a fetch — along with expression(), behavior, -moz-binding and position: fixed.
  2. The sandbox. The result is rendered in an iframe whose sandbox attribute is empty — the most restrictive value there is. No scripting. No same-origin access. Links cannot be followed from inside it.
  3. The frame’s own policy. The frame document carries a Content-Security-Policy meta tag with default-src ‘none’ and img-src data:.
  4. The site policy.A srcdoc frame inherits its parent’s CSP, so the site-wide img-src ‘self’ data: blob: and connect-src ‘self’ apply inside it too.

Inline images referenced by cid:are resolved only from the message’s own attachment bytes and embedded as data: URIs — a blob:URL would be unreadable from the frame’s opaque origin, and granting the frame same-origin access to avoid a base64 copy would be a poor trade. Embedding is budgeted at 2 MB per image and 12 MB per message; anything larger is listed and downloadable instead.

PDF

The PDF is written with pdf-lib and an embedded, subsetted Noto Sans. It carries the message’s text — the header fields you chose, the body with reply-chain indentation preserved, and an attachment index with SHA-256 hashes — laid out on real pages with real page breaks.

It is not a picture of the message’s HTML, and that is deliberate. Rendering arbitrary message HTML faithfully means running a layout engine over markup the sender controls; in a browser that means either screenshotting an iframe, which cannot paginate, or shipping a second HTML engine, which cannot exist here. The text extraction used by the PDF path is a DOM-free tokeniser whose output contains no markup at all, so active content cannot reach the writer by construction.

The embedded font covers Latin, Greek, Cyrillic and Vietnamese. Characters outside it are drawn as ▯ and counted, and the count is reported — rather than producing a page of empty boxes and letting you discover it later.

Exports carry a manifest

Every ZIP this site produces contains a manifest.jsonrecording, for each file, its SHA-256, its size and type, the message it came from with that message’s subject, date and Message-ID, and whether its filename came from the message or was generated here because the message supplied none. Anything that could not be exported is listed with the reason, so a shorter archive is never silently shorter.

Attachment names are reduced to a single safe path segment on the way out: directory separators removed, right-to-left override characters stripped, Windows reserved names prefixed, duplicates numbered. A message with a filename of ../../../etc/passwd cannot write outside the archive.

Formats and limits lists what is supported and what is not. Privacy covers the enforcement side in more detail.