Repository navigation
feat(tools/maildir): land the local-archive read side - #1811
Conversation
Kaap10
left a comment
There was a problem hiding this comment.
Thanks for working on this @sharpca! Landing the read-side for contract:mail-source closes a key gap for forensics/air-gapped triage (#304). The format sniffing, whole-thread retention across --since-days, and orphan-root handling are well-designed.
A few items to address before merge:
-
Rebase needed (Commit hygiene): The branch is based on an older commit and bundles two commits (
9f1da7d9andf432930e) that were already merged via PR #1752 and PR #1753. This makes the PR show 20 files changed across other packages. Please rebase ontomainto keep only the single commit for #304:git fetch upstream main git rebase --onto upstream/main HEAD~1 git push --force-with-lease origin feat/maildir-archive-read
-
thread_urlfile URI: Inread_archive.py:267,f"file://{root.as_posix()}#{wanted}"producesfile://C:/...on Windows (which is missing the third slash for drive letters). Consider usingf"{root.as_uri()}#{wanted}"frompathlib.Pathfor cross-platform RFC-compliant file URIs. -
CLI
FileNotFoundErrorhandling: Inthreads_mainandread_main, catchFileNotFoundErrorand print tostderrwith exit code1so missing archive paths exit cleanly rather than dumping an unhandled Python traceback. -
.emlparsing: Initer_messages(line 156), consideremail.message_from_bytes(item.read_bytes())instead ofmailbox.mboxMessage(...)since standalone.emlfiles lack mboxFromenvelope delimiters.
Once rebased and updated, this should be ready to land!
There was a problem hiding this comment.
Thanks for landing the read side — the overall shape fits the mail-source contract well. I probed read_archive.py against small synthetic mbox/Maildir archives, and a few things need fixing before this can merge; details are in the inline comments. Most are cheap to cover with tests. (Kaap10's rebase point still stands; that also takes care of the tracker-stats files, which are already on main via #1752.)
One point has no line in this diff: tools/spec-loop/specs/adapters.md (:46–52) still describes tools/maildir/ as drafting-only and points at a separate tools/mail-archive/ reader for mbox/Maildir. AGENTS.md asks for the spec update in this PR or a companion sync-specs PR.
potiuk
left a comment
There was a problem hiding this comment.
Inline versions of the points in my review above, on the lines they refer to. The spec point (tools/spec-loop/specs/adapters.md still describes tools/maildir/ as drafting-only) has no line in this diff, so it stays in the review body.
|
@sharpca, a couple of things before this can go further:
|
67cee35 to
7e609f0
Compare
|
Rebased onto Spec updated in this PR rather than in a companion one: Every review thread has a reply. Two things beyond the findings themselves:
One difference I left alone: Verification: |
57dbf21 to
ec419d1
Compare
Why: `contract:mail-source` had no working local-files backend. The mbox adapter documented the three read operations but shipped no executable, so the forensics / air-gapped path every `security-issue-import` run falls back to was unexercised code in a README. `tools/maildir/` was already named as the vendor home for that backend in its own README, and it already carries the outbound half of the vendor. This lands the inbound half there: `maildir-source-threads`, `maildir-source-read` and `maildir-source-thread-url`, over the three shapes an adopter can point at (one mbox file, a Maildir, a directory of `.eml` files), sniffed per call. Two decisions worth stating: - A thread is returned whole when any of its messages falls inside the `--since-days` window. A report filed months ago whose last reply is from yesterday is still an active thread, and trimming the older messages would hide the original report from the import skills that asked for the thread. - `thread_url` needs either an adopter-declared `--archive-template` or the archive path. It refuses to invent a public address: without a template the URL is a `file://` link to the snapshot, which is honest about being unreachable to anyone else. Thread identity is the root's RFC-5322 `Message-ID` in the bracketed form (`<root@host>`) that the IMAP backend emits under the same `rfc5322-message-id` kind, so an ID recorded against one backend is compared verbatim by the other instead of proposing a duplicate tracker. The root is the first `References` ID, else the first `In-Reply-To` ID, else the message's own `Message-ID` — the rule `imap_source` applies — and it is used even when that message is absent from the archive, so a snapshot truncated at the start of a thread keeps its replies in one thread rather than one thread per reply. A message carrying no `Message-ID` at all is keyed by a hash of its bytes under the separate `content-hash` kind, because a mailbox key is a position that shifts between snapshots. The thread-identity rule is pinned in `tools/mail-source/contract.md` (§ *Thread identity*), so a later backend cannot quietly emit the bare form. Two edges the first cut missed are fixed, each with a test: only bracketed tokens count as ancestors, so a comment in `In-Reply-To` (`(was: report) <r@h>`) cannot become a thread root and split the reply off; and the `urn:sha256:` id `list_recent_threads` emits for a message with no `Message-ID` round-trips through `read_thread` instead of coming back "not found". Closes apache#304. Generated-by: WorkBuddy AI (DeepSeek-V4.1-Flash)
ec419d1 to
a80c32d
Compare
The read side takes an archive snapshot from elsewhere, so its contents are treated as hostile: - The tag patterns could rescan to the end of the body from every `<`, so a few megabytes of `<` or of unclosed `<script>` stalled `read_thread` for minutes. Every tag pattern now stops at the next `<`, and a script or style element's end tag is searched for once, forward; an unclosed one runs to the end of the document. - A second message reusing a Message-ID was dropped in favour of whichever copy the listing yielded first, so a decoy could hide a real report. Copies are now matched on the Message-ID and the body, so a report exported twice is still counted once. - A symlinked `.eml`, or a symlink in a Maildir's `cur/` or `new/`, was followed and could put a local file's bytes into the agent's context. The Maildir is now listed by hand and neither shape follows a symlink. The docstring, the README and the contract also said the IMAP backend applies the same root rule. It does not take the `In-Reply-To` step yet, and all three now say so. Generated-by: Claude Opus 5
potiuk
left a comment
There was a problem hiding this comment.
Thanks for splitting the branch and for working through every thread: the bracketed thread ids, keying replies on an absent root, the comment in In-Reply-To, the urn:sha256: round trip and the pinned Thread identity section in the contract are all in. Reading the archive as the hostile input it is turned up four more things, so I pushed them as a fixup commit (1902210) rather than send it back:
- HTML stripping was quadratic. The tag patterns could rescan to the end of the body from every
<, so a few megabytes of<or of unclosed<script>stalledread_threadfor minutes. Every tag pattern now stops at the next<, and ascript/styleend tag is searched for once, forward; an unclosed element runs to the end of the document. - A reused Message-ID could hide a report. The second message with an id already seen was dropped, whichever copy the listing yielded first, so any sender could suppress a real report by reusing its id. Copies are now matched on the id and the body, so a report exported twice still counts once and a different message is kept.
- Symlinks were followed. A symlinked
.eml, or a symlink in a Maildir'scur/ornew/, would put a local file's bytes into the agent's context. The Maildir is now listed by hand and neither shape follows a symlink. - IMAP parity. The docstring, the README and the contract said
imap_source.searching.thread_root_ofapplies the same root rule; it does not take theIn-Reply-Tostep yet, and all three now say so. The follow-up you offered for IMAP is welcome.
The tools/spec-loop/.last-sync bump is reverted to main's value, since only adapters.md was synced. New tests cover each case, and the reused-id, symlink and unclosed-script tests fail on the previous head.
Please pull before pushing again.
This review was drafted by an AI-assisted tool and
confirmed by an Apache Magpie maintainer. The maintainer
approving this PR has read the findings and signed off. If
something feels off, please reply on the PR and a maintainer
will follow up.More on how Apache Magpie handles maintainer review:
Contributing guide.
|
Thanks again for this one. I filed the IMAP side as #1919: |
|
Thanks! I'd be happy to take it.
|
Summary
contract:mail-sourcehad no working local-files backend: the mbox adapterdocumented three read operations and shipped no executable.
tools/maildir/— already named as the vendor home for this backend in its own README, and
already carrying the outbound half — now implements them:
maildir-source-threads,maildir-source-read,maildir-source-thread-url.a directory of
.emlfiles. Read-only by construction; there is no draft orsend path here, that is
maildir-draft-create's half of the vendor.tools/maildir/README.mdgoes from "reader remains thedocumented stub" to an Operations table, and the mbox adapter's own README
drops its stub status and points at the implementation.
Type of change
tools/*/withpyproject.toml)tools/<system>/*.md)docs/,README.md,CONTRIBUTING.md)Test plan
pytest tests/test_read_archive.py: 16 passed — covers all three archiveshapes, threading by
In-Reply-Toand byReferences, theorphan-becomes-its-own-root case, whole-thread retention across the
--since-daysboundary, newest-first ordering, oldest-first threadbodies, the missing-thread exit code, and both
thread_urlbranches.mypy: no issues in 8 source filesruff checkandruff format --check: cleanmarkdownlint-cli2on both changed READMEs: 0 issuesRFC-AI-0004 compliance
archive, reads it, and returns JSON; nothing writes, sends, or drafts.
Linked issues
Closes #304
Notes for reviewers
thread is returned whole when any message is inside the window, and
thread_urlreturns afile://link rather than inventing a public addresswhen the adopter declared no template.
tools/maildir's existing tests fail on Windows because Maildir names carry:2,D, which Windows rejects. That is a platform limit, unrelated to thischange, and CI runs on Linux.
mboxrow of the capability matrix intools/mail-source/contract.mdalready claimed these three operations; it now has an implementation behind
the claim.
Generative AI disclosure
This change was authored with an AI coding agent (WorkBuddy AI, DeepSeek-V4.1-Flash)
under human review. The commit carries the
Generated-by:trailer; thecontributor has reviewed the diff and owns the change.