pmo: FINAL — self-hosted teardown; loop sole worker; 0900-ready

This commit is contained in:
2026-08-29 18:44:19 -05:00
parent 7debb52c97
commit 74ca1ed4a2
13 changed files with 469 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
# REPORT — 2026-08-29 17:00 — Crush: mopac-imap-go (Redmine 501)
- task: new repo `ukrrs/mopac-imap-go` — stdlib-only Go IMAP client
(CLI + library): RFC 2177 IDLE watch, fetch+parse (headers, text
body, attachments to spool), mark seen, SMTP passthrough send.
Env `IMAP_HOST/IMAP_USER/IMAP_PASS`. Fake-server tests. Feeds the
fleet's email-driven intake.
- result: DONE — 12 atomic conventional commits pushed to
`origin/main` (https://git.knownelement.com/ukrrs/mopac-imap-go,
private, UKRRS org), HEAD `66a9135`, branch in sync, tests green,
smoke PASS. NO Redmine writes performed.
## 1. What shipped
Repo created via `tea` (UKRRS/mopac-imap-go, private). Sibling-layout
to `mopac-redmine-go` at `~/projects/meta/MOPAC/mopac-imap-go`:
`Makefile``dev.sh` → digest-pinned `golang:1.26-bookworm` builder
(`golang@sha256:e8c859f5…`) — every build/vet/test ran in Docker; the
host never ran a Go toolchain.
First commit ships full AGPLv3 LICENSE (661 lines, verbatim from the
sibling corpus). Module: `git.knownelement.com/ukrrs/mopac-imap-go`,
go 1.26, zero third-party dependencies.
### Library packages
- `imap/` — IMAP4rev1 client: implicit-TLS or plain dial with greeting
validation; quoted-string LOGIN (password only ever inside the
command, never in errors); SELECT parsing
EXISTS/RECENT/UNSEEN/UIDVALIDITY; FETCH `(UID FLAGS BODY.PEEK[])`
with a literal-inlining wire reader (payloads containing `{N}\r\n`-
looking bytes parse correctly — pinned by test); STORE
`+FLAGS.SILENT (\Seen)`; RFC 2177 IDLE with typed events
(exists/recent/expunge/tick/other), tick-driven re-idle, and a Stop
that consumes the tagged DONE completion so the stream stays clean
(a subtle readLoop/select race was found by `-race`-adjacent flake
hunting and fixed: after stop, readLoop keeps reading until the
tagged line). Sentinel errors: unreachable/protocol/auth/mailbox/
timeout, one-line messages.
- `mail/` — RFC 5322 + MIME parse: RFC 2047 header decoding, QP/base64
transfer decoding, recursive multipart (mixed/alternative nesting),
attachments via Content-Disposition filename or Content-Type name.
Non-UTF-8 charsets pass through raw (no x/text in this repo).
`Spool()` writes `SPOOL_DIR/<sha256-12>/` containing `raw.eml`,
`meta.json` (seq, ids, decoded headers, attachment index) and
`attachments/<filename>` — content-hashed dirs make re-spooling
idempotent across seq renumbering; hostile filenames reduced to
basename (escape attempt pinned by test); 0700 dirs / 0600 files.
- `mailer/` — SMTP passthrough: raw bytes on the wire unmodified;
STARTTLS when offered, AUTH PLAIN when credentials set; sentinels
unreachable/auth/rejected.
### CLI (`mimap`) + config
- `internal/config`: `IMAP_HOST/IMAP_USER/IMAP_PASS` required (env
wins over an optional 0600 `KEY=VALUE` env file, looser-than-0600
refused before read); defaults 993/TLS/INBOX, `SPOOL_DIR=spool`,
SMTP set optional with `SMTP_FROM` falling back to the IMAP user.
Errors name keys, never values.
- `internal/cli`: `watch` (backlog fetch → IDLE; per message: spool →
JSON line on stdout → mark `\Seen`, so a crash re-delivers
idempotently instead of dropping; IDLE stopped before any FETCH;
ticks re-SELECT so a missed EXISTS self-heals; SIGINT/SIGTERM →
clean LOGOUT, exit 0), `check` (mailbox state as JSON), `send`
(`--to` repeatable, `--file PATH|-` → SMTP passthrough). Exit
codes 0/1/2 (ok/usage/protocol) matching the mred convention.
## 2. Verification
- TDD throughout: each feature commit landed test-first against
protocol-correct fakes (`internal/fakeimap` self-test pins every
wire exchange incl. literal FETCH bodies and IDLE push;
`internal/fakesmtp` pins AUTH/530/dot-unstuffing).
- `./dev.sh check` (build+vet+test) green; `go test -race -count=3
./...` green after fixing two real concurrency findings (DONE-
completion race in idle.go; unsynchronized test buffer); imap
package hammered `-count=10`.
- `./dev.sh smoke`: builds the real `bin/mimap` in the pinned builder
and drives it across process boundaries against the fakes —
`check` reports backlog, `send` relays, `watch` produces backlog +
live-IDLE-push JSON lines, 2 spool dirs, 2×`\Seen`, exit 0 on
SIGINT. Output: `smoke: PASS`.
## 3. Commits (all on origin/main)
66a9135 docs: README with CLI semantics, spool/intake contract, layout
019be0d test: end-to-end smoke of the real binary against fakes
ef2aec2 feat(cli): mimap watch/check/send over the library
d0a38b5 feat(config): env + 0600 env-file connection settings
4f5a43a feat(mailer): SMTP passthrough send with fake-server tests
2865718 feat(mail): spool messages and attachments to disk
dd1a462 feat(mail): parse RFC 5322 messages with MIME walk
444f297 feat(imap): RFC 2177 IDLE with clean Stop and re-idle
9d4ae78 feat(imap): fetch raw messages and mark seen
435fd81 feat(imap): client core — connect, login, select, logout
8552c9a test: in-process fake IMAP4rev1 server for client tests
f6332bf chore: bootstrap module (AGPLv3, docker dev harness)
## 4. Notes / limitations
- Charset conversion beyond UTF-8/ASCII is raw passthrough (stdlib
only); documented in package doc and README.
- Fake IMAP is one-mailbox (INBOX), no EXPUNGE push; the client still
handles EXPUNGE events defensively via re-SELECT.
- No Redmine API calls were made from this task, per instructions.