[high] fix(email): give an HTML-only message a searchable body - #106
Draft
elhoim wants to merge 1 commit into
Draft
[high] fix(email): give an HTML-only message a searchable body#106elhoim wants to merge 1 commit into
elhoim wants to merge 1 commit into
Conversation
_parse_email_message set body_text from _get_body(msg, "text/plain") alone. Most phishing is sent as text/html only, so those messages carried body_text: None -- a search_term naming a phrase in the body could never match them, and none of their content reached the case DB. _get_body had two further defects. For a multipart message it walked every part, so a text/plain *attachment* was returned as the message body; and it called payload.decode(charset) unguarded, so an unrecognised charset raised LookupError and lost the body outright. Walk only leaf parts, skip attachments, fall back to UTF-8 on an unknown charset, and add _get_searchable_body: text/plain when present, otherwise text/html with the markup stripped. Script and style blocks are removed rather than indexed, since neither is evidence. A message that has a text/plain part is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
BLUF
body_text: None. Asearch_termnaming a phrase in its body could never match it, and none of its content reached the case DB.text/plainattachment was returned as the message body, and an unrecognised charset raisedLookupErrorand lost the body entirely.body_textcame from_get_body(msg, "text/plain")alone, and_get_bodywalked every MIME part including attachments while decoding without a guard.text/htmlwith markup stripped when there is no plain part.src/mulder/server/tools/email.pyonly. No new abstraction, no change to any other tool.The bug
and
Three separate consequences:
body_textisNone, so_matches_searchreceivesNone, the body is never searched, and the message is dropped from any keyword search. Its content is also absent from what gets indexed.text/plainattachment is returned as the body. The walk does not checkContent-Disposition, so for amultipart/mixedmessage whose body is HTML and whose attachment isnotes.txt, the attachment text becomesbody_text.payload.decode(charset, …)raisesLookupError— not caught anywhere — for a charset the platform does not know.errors="replace"does not help; the exception is raised before decoding starts.The fix
plus
_get_searchable_body, which preferstext/plainand falls back totext/htmlwith tags,<script>and<style>blocks removed and the common entities decoded. Script and style content is stripped rather than indexed — neither is evidence, and indexing it would pollute keyword search.A message that has a
text/plainpart is completely unaffected; that is pinned by a test.Deliberately out of scope
fix/email-parsing(closed #78) bundled this with five other independent defects in the same file — the lexicographic date comparison, RFC 2047 header decoding,_parse_recipientssplitting on a comma inside a quoted display name,Cc/Bccnever being searched, and attachments detected only viaContent-Disposition: attachment. Each is a separate root cause and is being submitted as its own PR.HTML-to-text here is deliberately a tag strip, not a full renderer: it exists so the words are searchable, not to reproduce layout. Pulling in an HTML parsing dependency for that would be a much larger change than the defect warrants.
Relationship to open PR #96 (
fix/readpst-exit-code): that one guards the case where readpst failed. This one is the success path, where messages were extracted and then parsed lossily. Different functions, no overlap; verified conflict-free withgit merge-tree.Verification
uvx pre-commit run --all-files(new test staged first, so the hooks actually see it) → ruff, ruff-format, mypy all pass.email.pyrestored toorigin/mainand the new tests kept, 4 of 7 fail:The second line is the forensic consequence stated as an assertion: a phishing message containing "wire transfer" in its body, invisible to a search for exactly that phrase. The third and fourth lines are defects 2 and 3, each reproduced directly rather than argued.
The three that pass on both trees are the narrowness tests — plain-text messages unchanged, and markup absent from the body — so they pin the fix rather than the bug.
Context
Recovered from closed PR #78, which bundled six independent defects behind one title. The rejected outcome framework and
classify_tool_exitare not reintroduced — this is a self-contained fix to body extraction, per the review on #32:Branched fresh from current
main(2e5432c); the closed branch was not revised in place.🤖 Generated with Claude Code