agent-first-mail 0.3.0

Let your AI agent work your inbox — email pulled into plain files it reads, sorts, and drafts on your machine, with nothing sent until you confirm.
Documentation
# Code Structure

This document maps the `agent-first-mail` crate after the module split. It is a
maintenance guide for contributors; user-facing mailbox behavior is documented in
`workspace.md`, `file-formats.md`, and `cli.md`.

## Top-Level Flow

`afmail` has three main layers:

1. `src/main.rs` handles early CLI bootstrapping: version/help output, parse
   errors, output-format recovery, and process exit codes.
2. `src/cli/` defines the Clap contract and parse-time conveniences such as
   command-specific error hints.
3. `src/runner/` dispatches parsed commands to workspace operations and owns
   structured command logging/progress output.

Most behavior then lands in `src/store/Workspace`, which owns the file-first
mailbox workspace. Network-facing code stays outside the store in `imap_pull/`,
`imap_client.rs`, `smtp_send.rs`, `remote.rs`, and `push_queue/`.

## Source Map

```text
src/
  main.rs            early CLI bootstrap and exit handling
  cli/               Clap argument model, parsing, and invalid-command hints
  runner/            command dispatch, output formatting, locks, push, and purge orchestration
  config/            typed config schema, defaults, key access, and validation
  store/             workspace file model and local state transitions
  imap_pull/         read-only IMAP pull sessions, remote identity, special-use folders
  imap_client.rs     low-level IMAP session, mailbox info, and move outcomes
  smtp_send.rs       outbound message construction and SMTP sending
  remote.rs          MailRemote trait over IMAP/SMTP side effects, plus the test fake
  push_queue/        durable local push items plus preview/execute helpers
  types/             stable ids, message/case/archive/push DTOs
  mail.rs            MIME parsing into message DTOs
  markdown.rs        Markdown frontmatter split/parse/render and conversation extraction
  frontmatter.rs     draft/triage/case-message frontmatter types
  templates.rs       built-in and workspace MiniJinja template loading
  skill_admin.rs     skill install/status backing `afmail skill`
  progress.rs        workspace progress snapshot sink and status projection
  error.rs           AppError type with structured error codes and hints
  util.rs            crate-wide atomic-write, hashing, and flag helpers
  workspace_lock.rs  workspace lock-file guard
```

## Store Modules

`store/mod.rs` keeps the `Workspace` type, workspace discovery/init/status, and
high-level wiring. Sibling modules add focused `impl Workspace` blocks:

- `archive.rs` handles direct-message archive categories and archived case IO.
- `cases.rs` handles active case CRUD, membership, notes, tags, and moves.
- `contacts.rs` handles contact-card CRUD, emails/phones/tags/notes, archive and
  reopen, sender extraction, the ephemeral email→contact map, and the view
  refresh that materializes the contact link onto messages.
- `messages.rs` handles message cache materialization, reads, local status
  transitions, attachments, related-message discovery, relocation, and stamping
  each message's materialized `contact` link.
- `refs.rs` builds the case->message reference index (`CaseIndex`) used to decide
  whether a message is still referenced by any active or archived case.
- `disposition_views.rs` handles canonical local spam/trash/deleted collections
  plus generated review views.
- `remote_sync.rs` owns the `pull` entrypoint (`Workspace::pull` /
  `pull_with_progress`, dispatched for `afmail pull`), reconciles local references
  with remote IMAP locations, and exposes remote-location helpers used by push
  execution. The IMAP I/O it drives lives in `imap_pull/`.
- `triage.rs`, `render.rs`, and `view_model.rs` build generated Markdown read
  views and shared message rendering context.
- `drafts.rs` owns draft creation, validation, composition, and attachment
  bookkeeping for cases.
- `push_state.rs`, `purge.rs`, `doctor.rs`, and `transactions.rs` own narrower
  maintenance concerns.
- `util.rs` contains store-private filesystem, id, path, timestamp, and audit
  helpers shared by the modules above.

Keep new store behavior near the durable state it changes. Prefer another small
module only when a concern has its own file format, lifecycle, or command group.

## File-State Boundaries

The workspace keeps durable state in a few canonical places:

- Raw message evidence and remote sidecars live under `.afmail/messages/`.
- Rebuildable parsed message caches live under `messages/`.
- Case state lives in `cases/.../data/case.json` or
  `archive/cases/.../data/case.json`.
- Direct archive state lives in
  `archive/notifications/.../data/notification.json`.
- Local discard state lives in `spam/data/spam.json`,
  `trash/data/trash.json`, and `deleted/data/deleted.json` after the first
  message enters that disposition.
- Push items live in `.afmail/push/`; audit events live in
  `.afmail/logs/events.jsonl`.

Generated Markdown views (`triage/`, `case.md`, `archive.md`, `views/`,
`spam/*.md`, `trash/*.md`, and `deleted/*.md`) should be rebuildable from those
canonical files with `afmail render refresh`.

## Refactor Rules

- Preserve public command names, JSON schemas, stdout event codes, and on-disk
  paths unless a migration is explicitly planned.
- Add new CLI actions in `src/cli/` first, then dispatch them in `src/runner/`,
  and keep workspace mutations on `Workspace` methods.
- Use typed structs at module boundaries; reserve `serde_json::Value` for dynamic
  CLI output and template contexts.
- Keep generated view code separate from durable state mutation code where
  possible, then call refresh helpers after state changes.
- Keep helpers `pub(super)` or narrower unless another crate module already
  relies on the path.

## Validation Loop

For behavior-preserving cleanup, run from `spores/agent-first-mail/`:

```bash
cargo fmt
cargo check
cargo test
```

Use `cargo clippy --all-targets --all-features -- -D warnings` before release or
when changing shared helpers. Docker/container tests remain gated by the existing
integration-test environment flags.

## Fixture Batch, Docker E2E, and Demo

The reusable mailbox story lives under `tests/fixtures/mail-batch/`: a
`manifest.json` plus 30 realistic `.eml` files. Tests should load that manifest
instead of inventing new one-off EML strings when they need a realistic inbox.

Run the GreenMail-backed E2E locally with `--ignored`; no extra environment
gate is required:

```bash
cargo test --test container_e2e -- --ignored --nocapture
```

The same fixture batch powers a real agent-operation workspace. This prepares a
live GreenMail mailbox and configured afmail workspace, but does not pull or
process mail; the agent must actually operate the inbox:

```bash
bash scripts/demo-greenmail-fixtures.sh prepare
```

After it prints the workspace path, open your agent there and say:

```text
Check mail.
```

The prepared workspace uses `./bin/afmail`, so it works without a global
install. To install the current source globally for demo commands outside the
workspace:

```bash
cargo install --path . --bin afmail --locked --force
```

The prepared workspace also includes a small mock CRM (`./bin/crm`). The
`prepare` output suggests short demo prompts:

```text
Clean spam.
Check the refund request.
refund this order, reply.
```

The CRM helper is intentionally optional context for customer/order cases, not a
scripted requirement; the agent can use it when the email thread mentions an
order and more context would help. `AGENTS.md` only receives this CRM lookup
hint; the normal mailbox workflow instructions come from `afmail init`.

For an automated scripted demo pass, use:

```bash
bash scripts/demo-greenmail-fixtures.sh run
```