email-connect/docs/mailbox-report-tutorial.md
tegwick 3497ca88bf
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 2s
EMAIL-WP-0005 T05/T06: evidence realism and harness documentation
Adds harness.deliver_raw() to inject crafted .eml fixtures through the harness,
so they pick up the real Received: headers an MTA adds, and
harness.inject_maildir() as the byte-exact offline counterpart.

tests/test_evidence_realism.py asserts all ten recognized evidence classes
after passing through a real MTA, proves MTA delivery does not change which
evidence is produced, and covers Maildir injection offline.

Documents what no local server can honestly produce -- provider-generated DSNs,
real 4xx deferral and retry, ISP feedback loops, provider suppression behavior,
MX acceptance as distinct from provider acceptance -- and names SES simulator
addresses as the staging path. That tier stays out of the test run because it
needs real credentials.

Adds docs/test-harness-tutorial.md covering the three test tiers and the
start/send/scan/assert/reset walkthrough, with an explicit
assertable/not-assertable list so the evidence ceiling is stated where tests
get written. Adds a Maildir section to the mailbox report tutorial.

Completes EMAIL-WP-0005. Suite: 85 passed with the harness up, 64 passed +
21 skipped with it down.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 02:00:17 +02:00

4.4 KiB

Mailbox Report Tutorial

This tutorial shows how to generate an email-channel evidence report from a return mailbox or from the bundled fixture mailbox.

1. Verify The Scanner

Run the tests and print the adapter descriptor:

PYTHONPATH=src python3 -m unittest discover -s tests
PYTHONPATH=src python3 -m email_connect.cli adapter-descriptor

2. Start With Fixtures

The example config uses tests/fixtures/mailbox:

PYTHONPATH=src python3 -m email_connect.cli scan-mailbox \
  --config config/mailbox.example.yml \
  --full-rescan \
  --out reports/

The scanner writes a timestamped CSV file to reports/.

3. Configure A Live IMAP Mailbox

Copy config/mailbox.example.yml and set:

mailbox:
  protocol: imap
  host: imap.example.com
  port: 993
  tls: true
  username_env: EMAIL_CONNECT_IMAP_USER
  password_env: EMAIL_CONNECT_IMAP_PASSWORD
  folder: INBOX

Then export credentials:

export EMAIL_CONNECT_IMAP_USER='mailbox@example.com'
export EMAIL_CONNECT_IMAP_PASSWORD='app-password'

IMAP scans select the folder read-only and fetch messages with BODY.PEEK[]. The scanner does not mark messages seen, move messages, or delete messages.

3b. Scan A Maildir Instead

When mail is delivered locally — an MTA writing to disk, or the test harness — point the scanner at the Maildir root instead of an IMAP server:

mailbox:
  protocol: maildir
  folder: INBOX

source:
  maildir_dir: /var/mail/returns/Maildir

The directory is the mailbox root containing new/ and cur/; both are read. No credentials are involved. Message identity is the Maildir unique name without its :2,FLAGS suffix, so a message stays the same message when the MTA or a mail client moves it from new/ to cur/.

Like IMAP, Maildir scans are read-only: mark_seen is rejected, and the scanner never moves, flags, or deletes a message. include_seen: false skips messages already flagged S.

For a local Maildir or IMAP server to test against, see docs/test-harness-tutorial.md.

4. Add Expected Recipients

Expected recipients are optional. A newline-separated file can look like:

missing@example.com
recipient@example.com

Run:

PYTHONPATH=src python3 -m email_connect.cli scan-mailbox \
  --config config/mailbox.example.yml \
  --expected-recipients recipients.txt \
  --out reports/

CSV input is also supported:

email,name
missing@example.com,Missing User
recipient@example.com,Known Recipient

Run:

PYTHONPATH=src python3 -m email_connect.cli scan-mailbox \
  --config config/mailbox.example.yml \
  --expected-recipients recipients.csv \
  --expected-recipient-column email \
  --out reports/

Invalid recipient rows are ignored and printed as warnings.

5. Limit The Time Range

Use an inclusive datetime range:

PYTHONPATH=src python3 -m email_connect.cli scan-mailbox \
  --config config/mailbox.example.yml \
  --from 2026-06-02T10:00:00Z \
  --to 2026-06-02T11:00:00Z \
  --out reports/

--since remains a compatibility alias for the lower bound. When a range is active, messages with no parseable Date header are excluded because the scanner cannot confirm that they originated inside the requested window.

6. Read The Report

Key columns:

  • known_recipient: true when the address was supplied in the expected list.
  • normalized_event_type: the email evidence or diagnostic event.
  • assessment_category and assessment_subclass: advisory interpretation.
  • affected_email_address: the endpoint the row is about.

Known recipients appear first by default so spreadsheet filtering is easy.

Expected recipients with no mailbox evidence appear as:

normalized_event_type: diagnostic.expected_recipient.no_evidence
assessment_category: undef
assessment_subclass: undef.no_signal
evidence_strength: none
known_recipient: true

That row means only that no mailbox evidence was found for the supplied address inside the inspected range. It is not evidence of delivery success, delivery failure, recipient awareness, or legal acceptance.

7. Troubleshooting

  • Empty report: check folder, time range, and whether incremental cursor state already skipped older messages. Try --full-rescan.
  • IMAP credential error: verify the environment variable names and values.
  • Missing expected rows: check the recipient file path and CSV column name.
  • Unexpected no-evidence rows: confirm that the relevant mailbox evidence is inside the configured datetime range.