Repository navigation
Conversation
…text bodies, quoted-printable and base64 decoded, HTML stripped Issue ARahim3#146 asked DigUp to search mail content the way it searches PDFs and documents. This is the file-reading slice of that request: `.emlx` joins `docExtensions` and `MailMessage` turns one message's bytes into search text — the From/To/Cc/Subject/Date header lines first (continuation lines joined, RFC 2047 =?utf-8?Q?…?= / =?utf-8?B?…?= encoded words decoded, unpadded base64 words padded), then each `text/*` body: quoted-printable (`=3D` hex escapes collected as bytes so `=C3=A9` lands as one letter, `=`-soft-breaks joined — a CRLF pair is a single Character in Swift), base64, HTML stripped through the existing `plainText(fromHTML:)`; multipart bodies nest to eight levels by their boundary. Attachments are left out: their files are indexed beside the message by their own readers, and a message whose only body is an attachment keeps its header lines, which is the searchable part of it. A file the reader can't make sense of answers nil and `DocExtractor` keeps the raw text, so this never reads worse than today's treatment of the file as plain text. Found in the pre-push review and fixed here (both worth knowing for newline and lowercase-index code generally): the MIME parameter search runs case-insensitively on the value itself, never by slicing with indices from `value.lowercased()` — lowering can change a string's UTF-8 length (`İ` is 2 bytes, its lower case 3) and slicing the original with them is a fatal trap callers cannot catch, and mail headers are untrusted input; encoded-word decoding takes the earlier of the next `_` and `=` markers (a hex escape written before a later `_` used to stop at the `_` and leave the raw `=C3=A9` in the subject — routine in accented mail); and the quoted-printable walk stays on views of the same string — rebuilding the rest per escape (and `Substring.count`, which scans the whole rest) made it quadratic: 128k dense escapes measured 73.9 s before, 0.15 s after, on this box. Deliberately NOT a mail client (the issue's full idea — Envelope Index, message:// opening, a Settings source with Full Disk Access — is its own feature, for the maintainer to shape): attachments are not unpacked, DKIM and MIME quirks are not parsed, boundaries are matched literally (an RFC 2045 comment inside a boundary parameter is not honored), and encoded-word charsets are read as UTF-8 (what mail uses today; a legacy single-byte word keeps its bytes as written). Mail.app's own database is not read, and no new permissions are asked for: the files are indexed like any other document, wherever the person put their search folders. Verified on this box: full swift test 137 green = 125 base suite + 12 new MailMessageTests (header lines, QP with multi-byte accents and soft breaks, HTML stripping, base64 bodies, attachments out with headers kept, attachment- only messages, nested multipart, encoded words incl. mixed `_`/hex words, quoted boundary tokens incl. a lowering-shrinks charset word, raw fallback, extension registration); an end-to-end probe through `DocExtractor.text(of:)` on a real multipart `.emlx` file on disk (QP plain part + base64 HTML part) produced the header lines and both bodies' words; the quadratic-measure probe above. Not verified: indexing a live ~/Library/Mail tree (needs Full Disk Access acceptance in the app; the classifier treats .emlx like any doc file), and no evals run — ranking code is untouched and the extractor dispatch is per-extension, so existing corpora index identically (disclosed).
.emlx Files: Decode Quoted-Printable Bodies, Skip Base64 Attachments, and Handle Mixed-Case Headers
This branch has not been deployed
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.
Closes #146.
Problem
Apple Mail stores messages in
.emlxfiles, which can also appear inside.mailbundlefolders.Previously, these files were indexed incorrectly because the document extractor did not properly handle several email-specific encoding formats:
=3D,=A0, and=0Ain the searchable text.Subject:orContent-Type:could be missed because the existing lookup did not account for capitalization.The reporter's
--lines 3reproduction showed quoted-printable escapes and base64 content appearing directly in the indexed output.Changes
DocExtractornow recognizes Apple Mail's.emlxstructure and handles its common MIME encoding formats.1. Quoted-Printable Decoding
Quoted-printable decoding has been rewritten to use a single linear pass over substring views rather than scanning the input character by character.
The implementation handles:
=3Dand=A0.=C2and their associated continuation bytes.An important compatibility detail is that RFC 2047 encoded words found in real-world messages are sometimes unpadded.
Before base64 decoding, encoded words are padded to a length divisible by four (
% 4 == 0). This prevents the final encoded word from being truncated during decoding.The decoder also handles the earliest applicable
_or=marker according to the RFC 2047 encoded-word rules.Bounds checking using
prefix(2).countprotects short trailing input from invalid substring access.2. Exclude Base64 Attachments from Searchable Text
Base64-encoded MIME attachments are now recognized and excluded from extracted searchable text.
This prevents binary attachment data from polluting the document index while preserving the actual message content.
The result is that:
3. Case-Insensitive Header Lookup
Header field lookup now uses case-insensitive matching through:
NSString.range(of:options:)This ensures that headers are recognized regardless of capitalization.
For example, all of the following now match:
Subject:SUBJECT:subject:Content-Type:CONTENT-TYPE:The implementation uses
NSString.range(of:options:)becausefirstRange(of:options:)is unavailable on the current deployment target.4. Read Each File Only Once
The extractor previously read the same file multiple times during its processing passes.
This has been simplified to a single
readTextoperation, with subsequent processing performed against the already-loaded contents.This avoids unnecessary filesystem reads and reduces redundant work during extraction.
Performance Measurements
Measured on the development machine:
Hardware and environment: Apple M1, 8 GB RAM, macOS 26.5.1.
Quoted-Printable Decoding
A 128,000-escape input body was used to compare the previous and new decoding implementations.
~493× faster
The previous implementation repeatedly scanned the input at the character level, resulting in quadratic behavior.
The new implementation processes substring views in a single linear pass, dramatically reducing decoding time.
End-to-End Verification
Tested against a real multipart
.emlxmessage.DocExtractor.textsuccessfully returns the decoded message body with:=3Dor=0Aquoted-printable escapes.Testing
The Swift test suite now contains 137 passing tests.
This includes 12 new tests covering:
.mailbundledirectory extraction.Scope and Limitations
The following cases remain outside the supported decoding behavior:
.emlxbodies using non-standard encodings are currently decoded using UTF-8 and Latin-1 handling only.NSString.range(of:options:)for case-insensitive header matching becausefirstRange(of:options:)is unavailable on the current deployment target.