feat(pg-js): one MIME reader, shared by both add-ins - #157
Merged
Conversation
apps/outlook-addon carried 225 lines of hand-rolled multipart parsing and apps/tb-addon carried its own detection, so the two hosts agreed on the shape they exchange only by coincidence. Both now read the envelope through the SDK. Moved into packages/pg-js and exported: parseDecryptedMime, readMimeHeader, bodyFromMime, isMultipart, detectPostGuard, extractArmoredCiphertext, looksLikeArmoredPostGuard, POSTGUARD_ENCRYPTED_FILENAME, and the ParsedMessage / ParsedAttachment / DetectPostGuardInput types. Additive: the API report diff is 44 insertions and no removals. The armor asymmetry is resolved deliberately rather than by deleting it. extract.ts said body-armor extraction was "no longer supported" while Outlook still shipped extractArmoredCiphertext and used it as a live fallback in tryFindCiphertext. The comment described the EMITTER — pg-js >= 1.1 stopped writing armor, which is what broke tb#85 — but stated it as a property of the reader. COMPATIBILITY.md's archival guarantee is unconditional, so armor reading is promoted into the SDK as an explicitly archival export instead. The JSDoc on EmailHelpers.extractCiphertext claimed "attachment or armored body", which the implementation has never done; corrected. Two bugs surfaced by the move: - Quoted-printable decoded every `=XX` through String.fromCharCode, one character per octet, so `caf=C3=A9` read back as `café`. Any accented character in a quoted-printable body was mojibake. The byte path then UTF-8-encoded that string, doubling every octet above 0x7f and silently corrupting quoted-printable attachments. Octets are now collected and decoded once, and both halves are pinned by tests. - tb-addon hardcoded "postguard.encrypted" separately from the SDK's own literal. It is one exported constant now, used by the emitter and every reader. What stayed host-specific: guessContentType is Office.js glue (it exists because Office.js hands back a name and no Content-Type), and detection.ts keeps its WebExtension calls, checking attachments before pulling the body so a tier-1/2 message is still decided without a getFull. The moved parser arrived with no tests — it had none in the add-on, which is part of why the two copies could drift. 25 added, including a buildMime → parseDecryptedMime round-trip that holds the two halves of the format together. pnpm -r typecheck, build and test green; 330 pg-js, 39 outlook, 212 tb-addon, 20 website. Envelope gate green. Part of #129.
This was referenced Jul 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #129 (the second of its three parts).
apps/outlook-addoncarried 225 lines of hand-rolled multipart parsing andapps/tb-addoncarried its own detection, so the two hosts agreed on the shape they exchange only by coincidence. Both now read the envelope through the SDK.Lands on top of the envelope-compat gate (#153) deliberately: the gate is live for every commit here, which is the whole point of that ordering.
Moved into pg-js
parseDecryptedMime,readMimeHeader,bodyFromMime,isMultipart,detectPostGuard,extractArmoredCiphertext,looksLikeArmoredPostGuard,POSTGUARD_ENCRYPTED_FILENAME, plus theParsedMessage/ParsedAttachment/DetectPostGuardInputtypes.Additive only — the API report diff is 44 insertions, no removals, so the changeset is
minor.The armor asymmetry, resolved rather than deleted
extract.tssaid body-armor extraction was "no longer supported" while Outlook still shippedextractArmoredCiphertextand used it as a live fallback intryFindCiphertext. Both were right about different things: the comment described the emitter (pg-js >= 1.1 stopped writing armor — the change behind postguard-tb-addon#85) but stated it as a property of the reader.COMPATIBILITY.md's archival guarantee is unconditional — "read support for stored artifacts is not part of this window; it never drops" — so deleting Outlook's copy would have been the tb#85 change again in the other direction. Armor reading is promoted into the SDK as an explicitly archival export instead, with a comment saying not to add a caller that produces it.Related:
EmailHelpers.extractCiphertext's JSDoc claimed it read "attachment or armored body". It has only ever read the attachment. Corrected.Two bugs the move surfaced
Quoted-printable was decoded one character per octet.
decodeQuotedPrintablemapped each=XXthroughString.fromCharCode, socaf=C3=A9came back ascafé— every accented character in a quoted-printable body was mojibake. The byte path then UTF-8-encoded that string, doubling every octet above0x7fand silently corrupting quoted-printable attachments. Octets are now collected and decoded once, and both halves are pinned by tests:Caught by the round-trip test, not by reading the code — it was moved verbatim first, and the test failed.
postguard.encryptedwas a literal in five places, including one intb-addonindependent of the SDK's own. One exported constant now, used by the emitter and every reader.What stayed host-specific
guessContentTypeis Office.js glue: it exists because Office.js hands back an attachment name and no Content-Type. No other host has that gap.detection.tskeeps its WebExtension calls, and still checks attachments before pulling the body — folding both into onedetectPostGuardcall would fetch the full message every time, and would turn agetFullfailure on a message that does carry the attachment into "not encrypted".Tests
The moved parser had no tests in the add-on, which is part of how the two copies were free to drift. 25 added, including a
buildMime→parseDecryptedMimeround-trip that holds the two halves of the format together, nested-multipart recursion, and a corrupt-base64 case asserting the readable part of a message survives an unreadable attachment.pnpm -r typecheck,buildandtestgreen: 330 pg-js (was 305), 39 outlook, 212 tb-addon, 20 website. Envelope gate green, API report matches.Still open in #129
Part 3, the
x-postguardsemantics. Four producers disagree and detection is a bare presence check, so no test can arbitrate it until the meaning is decided — untouched here.