From 45a68fbbe8da80a7bdf399b2e042f1982ca6a69d Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Thu, 10 Sep 2026 11:10:08 +0200 Subject: feat: add the report subcommand Refuses without a configured reporter identity rather than filing a report with no reply address, and turns a frozen or edited case into an error message rather than a traceback. The identity rule is EMAIL REQUIRED, NAME AND ORG OPTIONAL, not the plan's "all three required". Each [reporter] key is individually skippable at init and config drops a skipped one rather than storing "", so a partial identity is a normal shape; text_part() already renders whatever subset is present. Requiring all three would refuse a config init itself writes without complaint. The address is different in kind because it becomes the From. That closes backlog item 5: report.build() read identity["email"] directly and raised a bare KeyError on a name-without-address identity, which is exactly what skipping one init prompt while answering another produces. The check lives in the command rather than the builder, since that is what decides whether the user sees an exit code and a sentence or a traceback. Two further defects in the plan's code, both fixed here: - It called config.load() with no argument, so the global --config option was silently ignored for this subcommand alone. It now resolves the path the way _cmd_parse does. Reporting against the wrong identity is the failure that would have caused. - Its summary printed counts and no path. Nothing sends these bodies yet, so reviewing them by hand is the entire point of the command, and the case's bodies directory is now named in the output. case.py stays the only writer of the manifest: generate() returns it and case.save() writes it atomically. Bodies are written before that save, so a failed save leaves bodies the manifest does not record; the next run finds no recorded hash, treats them as regenerable rather than hand-edited and overwrites them, and nothing has been sent, so no machinery is warranted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01LByBnw83xr9YP85nskzkyE --- docs/BACKLOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'docs') diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 63ea52c..3ab642f 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -9,6 +9,7 @@ number and gains a status rather than being renumbered. | 2 | An IDN indicator resolves to no contact | S | open | | 3 | Expose kept cases so qtmaildir can tag spam | ? | open, unsized | | 4 | `Report-Type: phishing` is unverified against x-arf | XS | open | +| 5 | `report.build()` raises KeyError on an identity with no email | XS | closed | ## 1. Skip boilerplate namespace URLs @@ -137,6 +138,15 @@ expects. ## 5. `report.build()` raises KeyError on an identity with no email +**Closed** by the `report` subcommand (Task 11). `cli._cmd_report()` refuses +before building, with the not-configured exit code and a sentence naming the +file to edit, rather than letting `build()` raise. The rule it applies is +EMAIL REQUIRED, NAME AND ORG OPTIONAL: the address becomes the `From` and a +report without one cannot be answered, while the other two are individually +skippable at `init` and `text_part()` already renders whatever subset is +present. `tests/test_cli.py` covers both halves, the refusal and the +email-only identity that must still succeed. + **Observed.** `report.build(manifest, destination, identity)` reads `identity.get("name", "")` defensively but `identity["email"]` directly, so an identity carrying a name and no address raises `KeyError: 'email'` rather than -- cgit v1.2.3