Adds tests/harness/docker-compose.yml running GreenMail 2.1.12 (digest-pinned) with SMTP 3025, IMAP 3143 and the API bound to 127.0.0.1 only, plus a config/harness-imap.yml scanner profile and harness README. Auth is disabled and no users are declared, so a mailbox is created on first login and per-test users need no provisioning. GreenMail standalone offers no STARTTLS, only plaintext or implicit TLS, while SMTPProvider hardcoded starttls() -- so no send could reach it. SMTPProvider now takes a security mode via EMAIL_CONNECT_SMTP_SECURITY, defaulting to starttls. plaintext is refused for any non-loopback host, and hostnames are never resolved to decide that, so a misconfigured deployment fails at startup rather than sending credentials in the clear. Trusting GreenMail's self-signed cert was rejected as the wider risk; see DECISIONS.md. Verified end to end against the live harness: SMTPProvider.send -> GreenMail -> ImapMailboxSource, and the documented scan-mailbox CLI. Suite: 52 passed with the harness down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
12 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|
| EMAIL-WP-0005 | workplan | Test Mailbox Harness for Automated Test Environments | infotech | email-connect | proposed | claude | custodian | 2026-08-14 | 2026-08-14 | 1ffd54d3-4fdf-4a66-86ba-36a5d8202f3c |
EMAIL-WP-0005 - Test Mailbox Harness for Automated Test Environments
1. Purpose
Automated test environments need mail accounts for test users so that end-to-end flows (invitation send, verification, return-path evidence) can be exercised without a production provider and without hand-crafted fixtures for every case.
This workplan adds a test mailbox harness: a Maildir mailbox source, a
containerized SMTP/IMAP test server, deterministic test-user accounts, and
end-to-end tests that drive /v1/send into the harness and scan the resulting
mailboxes back out through the existing scanner.
The harness is test infrastructure. It does not make email-connect a mail
provider or an MTA operator, and it must not weaken the evidence discipline:
mail observed in a test server is still provider/MX-level fact, never proof of
delivery, awareness, identity, or authorization.
2. User Story
As a developer or CI job, I want to start one container, send invitation and
verification mail through email-connect to per-test mailboxes, and then scan
those mailboxes with the normal scanner path, so that I can assert on end-to-end
behavior without a live provider and without network egress.
3. In Scope
- A
maildirmailbox source alongside the existingfixtureandimapsources. - A containerized SMTP + IMAP test server for integration tests.
- Deterministic per-test-user account provisioning and reset.
- End-to-end tests: send through the transactional service, scan through the scanner, assert evidence rows.
- A documented strategy for bounce/complaint/deferral realism, which local servers cannot generate honestly.
- Documentation and a decision record for the chosen approach.
4. Out of Scope
- Operating a production or internet-facing MTA.
- Owning DNS, TLS certificates, rDNS, SPF/DKIM/DMARC, or spam filtering.
- Mailbox write-back actions (the scanner stays read-only;
mark_seenremains rejected). - A mailbox management UI or user-facing account provisioning.
- Replacing the fixture path as the default test source.
- Integrating s/qmail — see
DECISIONS.md, "Test mailbox harness: purpose-built test server over s/qmail".
5. Design Constraints
The harness must respect the existing seams rather than introduce new ones:
mailbox.protocol fixture | imap | maildir (src/email_connect/mailbox.py)
SMTP provider EMAIL_CONNECT_SMTP_HOST/PORT (src/email_connect/transactional.py)
Pointing tests at the harness must require configuration only, not code branching inside the scanner or the transactional application. No test-only code paths may exist in the production send or scan logic.
The fixture source remains the default for unit tests: deterministic, offline, no container required. The harness is the integration tier.
6. Evidence Semantics
Mail retrieved from a test server carries exactly the same evidence ceiling as mail retrieved from a real mailbox. Specifically, the harness must not be used to justify any of:
message was delivered to a real inbox
recipient became aware of the message
recipient identity was verified
recipient was authorized
A test-server acceptance is provider_accepted and nothing more. Tests that
assert stronger claims are defects in the test, not features of the harness.
7. Work Packages
T01 - Maildir mailbox source
id: EMAIL-WP-0005-T01
status: done
priority: high
state_hub_task_id: "28856bdc-7ea2-44ce-8c06-88bf006e17fd"
Tasks:
Add protocol: maildir to config and AppConfig validation
Implement MaildirMailboxSource reading new/ and cur/ per Maildir spec
Derive stable message identity from Maildir filename plus existing dedup keys
Support incremental scans via scan_cursors for maildir sources
Preserve raw_message_ref as a maildir:// reference
Reject mark_seen for maildir the same way IMAP does
Wire the source into source_for_config
Add unit tests over a temporary Maildir tree
Acceptance:
The scanner reads a Maildir directory, classifies messages, and produces the
same evidence rows as the equivalent fixture directory, with deduplication and
incremental cursors working across repeated scans.
Done 2026-08-14:
MaildirMailboxSourcereadsnew/andcur/, skips dotfiles, preferscur/when a base name appears in both, and emitsmaildir://<root>/<subdir>/<file>refs.mark_seenand a missing directory both raise, matching the read-only IMAP contract.- Identity is the Maildir unique name minus the
:2,FLAGSsuffix, so it is stable across thenew/→cur/move. It is carried on a newMailboxSourceMessage.dedup_uidand appended to the message dedup key only when set, leaving existing fixture and IMAP state-store keys byte-identical. - Cursor ordering parses the Maildir delivery time rather than comparing names lexically, so cursors stay correct as the time field changes width.
- Fixed alongside: the parse-failure path keyed identity on
raw_message_ref, which anew/→cur/move rewrites, so an unparseable message re-registered as new on every rescan. It now prefers the source uid when one exists. Behavior for fixture and IMAP sources is unchanged. source.maildir_diradded to config and the example file;source_for_configvalidates it.include_seen: falseskipsS-flagged messages.- Tests:
tests/test_maildir.py, 11 cases including maildir-vs-fixture evidence parity. Full suite 38 passed.
T02 - Containerized SMTP/IMAP test server
id: EMAIL-WP-0005-T02
status: done
priority: high
state_hub_task_id: "967d2bab-43e3-4240-8bd5-c657ba4178ff"
Tasks:
Select and pin a test mail server image providing SMTP and IMAP
Add a compose or container definition under a test harness directory
Expose SMTP and IMAP on fixed local ports with non-secret test credentials
Document startup, teardown, and health check
Add a config profile pointing mailbox.protocol imap at the harness
Add a config profile pointing the SMTP provider env vars at the harness
Ensure the harness never binds a routable interface by default
Acceptance:
One documented command starts a local mail server; the scanner can complete an
IMAP scan against it and the transactional service can complete a send to it,
using committed non-secret test credentials only.
Notes:
Preferred candidate is GreenMail, which serves SMTP and IMAP and creates
accounts on demand. Mailpit is the fallback for send-side-only inspection and
does not exercise the IMAP source. Record the choice in DECISIONS.md.
Done 2026-08-14:
tests/harness/docker-compose.ymlruns GreenMail2.1.12, digest-pinned, SMTP 3025 / IMAP 3143 / API 8080, all bound to127.0.0.1only. The image ships no curl/wget/nc, so the healthcheck probes both mail ports with bash/dev/tcp. Verified healthy.greenmail.auth.disabledwith no declared users: any login is accepted and the mailbox is created on first use, so T03 needs no provisioning step. Declaring users and enabling auth-disabled conflicts — GreenMail tries to auto-create the login and collides with the declared address.config/harness-imap.ymlscanner profile; env-var recipe for the send side intests/harness/README.md.- GreenMail standalone has no STARTTLS support (confirmed against the shipped
jar's property builder — only plain and implicit-TLS setups exist), and
SMTPProviderhardcodedstarttls(), so no send could reach it. Added anEMAIL_CONNECT_SMTP_SECURITYmode defaulting tostarttls;plaintextis refused for any non-loopback host and hostnames are never resolved to decide it. See DECISIONS.md. - Verified end to end:
SMTPProvider.send→ GreenMail →ImapMailboxSourcefetch, then the documented CLI (scan-mailbox --config config/harness-imap.yml) against the live harness — 1 message seen, parsed, 1 evidence event. - Tests: 14 offline cases for the transport-security guard in
tests/test_transactional.py. Full suite 52 passed, still container-free.
T03 - Test-user account provisioning and reset
id: EMAIL-WP-0005-T03
status: todo
priority: high
state_hub_task_id: "c761d26d-a8b0-4b66-ba18-8f22c6d59440"
Tasks:
Define the test-user address convention and test domain
Provide a helper to create or address per-test mailboxes deterministically
Provide per-test reset so state does not leak between tests
Keep test credentials non-secret and committed, never routed through OpenBao
Document the convention and the reset contract
Acceptance:
A test can obtain an isolated mailbox for a named test user, send to it, read
it back, and reset it, with no cross-test interference and no real credentials
involved.
Notes:
Test credentials are deliberately non-secret and belong in the repo. Per
.claude/rules/credential-routing.md, OpenBao and warden route are for real
provider material; the harness must not touch them.
T04 - End-to-end send-and-scan integration tests
id: EMAIL-WP-0005-T04
status: todo
priority: high
state_hub_task_id: "69655fad-97b8-4c17-bc54-af341aaaf38c"
Tasks:
Add an integration test tier that is skipped when the harness is unavailable
Drive /v1/send invitation and verification flows into harness mailboxes
Scan the resulting mailboxes through the normal scanner path
Assert evidence rows, evidence_ceiling, and absence of overclaiming
Cover idempotency and duplicate-request behavior end to end
Cover suppression behavior end to end
Ensure the default unit test run stays offline and container-free
Acceptance:
An integration run proves send-to-scan continuity against a live local server,
while the default test run remains offline, deterministic, and unchanged.
T05 - Bounce, complaint, and deferral realism
id: EMAIL-WP-0005-T05
status: todo
priority: medium
state_hub_task_id: "48b8eaf9-5a4b-437b-ad2c-952907340ed7"
Tasks:
Document that local servers do not generate realistic DSNs, complaints, or deferrals
Keep crafted .eml fixtures as the deterministic path for those classes
Add a helper to inject fixture messages into a harness mailbox or Maildir
Define an optional staging tier using provider simulator addresses
Document which evidence classes are only reachable in the staging tier
Acceptance:
Every evidence class the parser recognizes has a documented, reachable test
path, and the limits of the local harness are stated explicitly rather than
papered over.
T06 - Documentation and decision record
id: EMAIL-WP-0005-T06
status: todo
priority: medium
state_hub_task_id: "0599de98-c0ff-4160-a2c8-54265b31ca5d"
Tasks:
Write a test harness tutorial covering start, send, scan, assert, reset
Extend docs/mailbox-report-tutorial.md with the maildir source
Record the DECISIONS.md entry for the harness approach and the s/qmail rejection
Document the three test tiers: fixture unit, harness integration, provider staging
Document the evidence ceiling that applies to harness-observed mail
Acceptance:
A new contributor can start the harness, run the integration tier, and
correctly state what harness-observed mail does and does not prove.
8. Completion Criteria
This workplan is complete when:
mailbox.protocol: maildiris supported, tested, and documented.- A pinned containerized SMTP/IMAP test server starts from one documented command with committed non-secret credentials.
- Test users get isolated, resettable mailboxes by convention.
- Integration tests prove send-to-scan continuity while the default test run stays offline and container-free.
- Every recognized evidence class has a documented, reachable test path, with local-harness limits stated explicitly.
- Documentation and the decision record are in place, including the evidence ceiling for harness-observed mail.