Open winmail.dat #205

Closed
opened 2026-09-02 06:16:40 +00:00 by jcoffey-dev · 0 comments
Owner

Outlook sending in Rich Text packs every attachment into one TNEF blob. Every other client shows a single unopenable winmail.dat, and the files inside are gone as far as the reader is concerned — which is a decoding problem rather than a mail one.

What it does

A banner offers to open it; the contents appear as ordinary attachments, named and typed. The original stays attached.

Written from the published format (MS-OXTNEF): a signature, a key, then a flat run of attributes — level byte, 32-bit id carrying its own type, length, data, checksum. Attachments are delimited by attAttachRenddata rather than named, so the parse is a small state machine.

The MAPI property stream is read for two properties: the long filename and the MIME type. attAttachTitle carries an 8.3 name, so a file that arrived as Quarterly Report Final.docx is QUARTE~1.DOC there and correct here. The stream stops at a named property (id ≥ 0x8000) rather than guessing past it — those carry a GUID before their value and nothing after one can be trusted to stay aligned.

Decisions

Decoded in the browser, on request. The server never sees the contents and has nowhere to keep a decoded copy; doing the work on sight would spend the bandwidth whether or not anyone wanted what is inside.

A blob that goes wrong part-way keeps what was read — whether it ran out or the checksum stopped matching. Half the attachments beats none: the alternative is a reader who can see the file is there and cannot have it.

The message body is deliberately not decoded. TNEF can also carry it as compressed RTF, which is a second format again (MS-OXRTFCP) for a body the reader already has in plain text or HTML nine times in ten.

Testing

16 unit tests: single and multiple attachments, the long filename in both ASCII and UTF-16, the stated MIME type winning over a guess, an attachment with no title, unknown attributes skipped, non-TNEF answering quietly, checksum desync and truncation both keeping earlier files, the named-property stop, a nonsense MAPI stream still yielding the attachment, and a titled attachment with no data being dropped.

Suite green: web 733 across 80 files, server 110, typecheck clean.

Honest note on the fixtures

The test blobs are built from the format description, not captured from Outlook. They prove the parser reads what the spec says; they cannot prove it reads what Outlook actually emits. The likeliest divergence is the MAPI stream, where real messages carry many more properties than these do.

The mock now sends a winmail.dat too, built by its own encoder rather than by the parser's fixtures — so the round trip goes through two independently written implementations of the same description, which is a better check than a fixture round-tripping against itself. It is still not Outlook.

Verified in the running app

Opened the mock's message: the banner appears, the two files decode, the first shows as Quarterly Revenue Final.csv (the MAPI long name, not the QUARTE~1.CSV title), and fetching the generated blob URL returns exactly the CSV bytes the mock encoded. The original winmail.dat remains attached.

Worth trying against one real Outlook message before this is trusted.

Merged 2026-09-01 as coffey-labs/ihasmail@c5be74fe57

Rebuilt from: git history, session transcript.

Outlook sending in Rich Text packs every attachment into one TNEF blob. Every other client shows a single unopenable `winmail.dat`, and the files inside are gone as far as the reader is concerned — which is a decoding problem rather than a mail one. ## What it does A banner offers to open it; the contents appear as ordinary attachments, named and typed. The original stays attached. Written from the published format (MS-OXTNEF): a signature, a key, then a flat run of attributes — level byte, 32-bit id carrying its own type, length, data, checksum. Attachments are delimited by `attAttachRenddata` rather than named, so the parse is a small state machine. **The MAPI property stream is read for two properties**: the long filename and the MIME type. `attAttachTitle` carries an 8.3 name, so a file that arrived as `Quarterly Report Final.docx` is `QUARTE~1.DOC` there and correct here. The stream **stops at a named property** (id ≥ 0x8000) rather than guessing past it — those carry a GUID before their value and nothing after one can be trusted to stay aligned. ## Decisions **Decoded in the browser, on request.** The server never sees the contents and has nowhere to keep a decoded copy; doing the work on sight would spend the bandwidth whether or not anyone wanted what is inside. **A blob that goes wrong part-way keeps what was read** — whether it ran out or the checksum stopped matching. Half the attachments beats none: the alternative is a reader who can see the file is there and cannot have it. **The message body is deliberately not decoded.** TNEF can also carry it as compressed RTF, which is a second format again (MS-OXRTFCP) for a body the reader already has in plain text or HTML nine times in ten. ## Testing 16 unit tests: single and multiple attachments, the long filename in both ASCII and UTF-16, the stated MIME type winning over a guess, an attachment with no title, unknown attributes skipped, non-TNEF answering quietly, checksum desync and truncation both keeping earlier files, the named-property stop, a nonsense MAPI stream still yielding the attachment, and a titled attachment with no data being dropped. Suite green: web 733 across 80 files, server 110, typecheck clean. ## Honest note on the fixtures **The test blobs are built from the format description, not captured from Outlook.** They prove the parser reads what the spec says; they cannot prove it reads what Outlook actually emits. The likeliest divergence is the MAPI stream, where real messages carry many more properties than these do. The mock now sends a `winmail.dat` too, built by **its own encoder** rather than by the parser's fixtures — so the round trip goes through two independently written implementations of the same description, which is a better check than a fixture round-tripping against itself. It is still not Outlook. ## Verified in the running app Opened the mock's message: the banner appears, the two files decode, the first shows as `Quarterly Revenue Final.csv` (the MAPI long name, not the `QUARTE~1.CSV` title), and fetching the generated blob URL returns exactly the CSV bytes the mock encoded. The original `winmail.dat` remains attached. Worth trying against one real Outlook message before this is trusted. **Merged** 2026-09-01 as coffey-labs/ihasmail@c5be74fe57b1 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.