4.0 KiB
4.0 KiB
Phase 2 verification — read mail (IMAP) on .13
Status: done and deployed. AUD-67, commit df3b6c9.
What Phase 2 added
internal/mailfetch: a dependency-free, read-only IMAP client. It speaksLOGIN,SELECT,UID SEARCH SINCE, andUID FETCH (UID X-GM-MSGID X-GM-THRID BODY.PEEK[]), and it dialstcp4only — IPv6 to Gmail is broken from.13and previously produced a socket EOF (AGENTS.md §9).- MIME → text extraction (
text/plainpreferred, HTML stripped as a fallback, quoted-printable/base64 decoded) and RFC2047 header decoding. - FR-2 source-address resolution:
X-Pm-Original-To→To→Received … for <addr>→Return-Path.Delivered-Tois never consulted. The matched header is recorded in the fetch action log for audit. - SQLite storage via
UpsertMessage(idempotent on Gmail'sX-GM-MSGID), plus schema migration v2 addingimap_uidandimap_uidvalidity. - Dashboard and
/partials/inboxrender real mail, grouped by category, with a recommended action per row. Phase 3 (JEV) supplies real categories; until then everything is grouped under Unclassified with the action Review. POST /actions/fetch(CSRF-checked) backs the dashboard Check mail button.email-agent -fetchruns one scan and exits — the n8n/CLI entry point (FR-1).
Configuration
Secrets stay in ~/.config/environment.d/10-secrets.conf and are passed into the
container by compose. New names (all in .env.example):
| Variable | Default |
|---|---|
GMAIL_USER |
(required for sync) |
GMAIL_APP_PASSWORD |
(required for sync) |
EMAIL_AGENT_IMAP_HOST |
imap.gmail.com |
EMAIL_AGENT_IMAP_PORT |
993 |
EMAIL_AGENT_IMAP_LOOKBACK_DAYS |
14 |
EMAIL_AGENT_IMAP_MAX_MESSAGES |
100 |
Deployment (executed 2026-10-11, AUD-67)
On .13, from the repo, with the host secrets in the environment:
set -a; . ~/.config/environment.d/10-secrets.conf; set +a
docker compose build
docker compose up -d
docker compose ps
The Phase 1 volume email_agent_data was kept; migration v2 added the IMAP
columns in place. The container was recreated so it received GMAIL_USER and
GMAIL_APP_PASSWORD.
Verification results
| Check | Result |
|---|---|
docker compose ps |
email-agent up, healthy, 0.0.0.0:8790->8790/tcp |
GET /health (public HTTPS) |
200 {"status":"ok","env":"production","database":"ok"} |
email-agent -fetch (in container) |
fetched=100 stored=100 uidvalidity=3 since=2026-09-27 |
GET / after login (public HTTPS) |
200; nav shows All mail / categories, Check mail button |
GET /partials/inbox |
200, 100 message rows, grouped under Unclassified |
| FR-2 on the real sample | favrolfe@gmail.com → Hello@where-woof.com stored with source_address = Hello@where-woof.com from X-Pm-Original-To |
| Source address coverage | 100/100 messages have a non-empty source_address; 4 distinct addresses |
| Fetch action log | actions_log: fetch … uidvalidity=3 since=2026-09-27 fetched=100 stored=100 source_headers=To,X-Pm-Original-To |
POST /actions/fetch without CSRF |
403 |
POST /actions/fetch with CSRF |
200, note Scanned 100 message(s); 100 stored or refreshed. |
| Idempotency | a second scan reported 100 stored and left 100 rows |
IPv4 was verified indirectly by success: the client only ever calls
DialContext(ctx, "tcp4", …), and Gmail over IPv6 from .13 fails.
Phase 2 Definition of Done (AGENTS.md §11)
- The inbox shows real mail in the UI.
- FR-2 source address is read from
X-Pm-Original-To(fallbacksTo,Received … for <addr>,Return-Path;Delivered-Tonever used).
Rollback
docker compose downon.13(keepsemail_agent_data).git revert df3b6c9(or check out the previous image) anddocker compose up -d --build.- To roll back only the credential change, remove
GMAIL_*from the host environment and recreate; the service still starts and the UI shows the empty state.