Workspace Model

An afmail workspace is a local, file-first mailbox workspace. Files are the read interface; the CLI is the effect interface.

Layout

account-workspace/
  AGENTS.md
  triage/
    message_<id>.md
  cases/
    <group>/
      <case_uid>-<name>/
        case.md
        notes.md
        data/
          case.json
          messages.json
          drafts.json
        views/
          messages/
            <message_id>.md
        drafts/
        files/
  archive/
    cases/
      <case_uid>-<name>/
        case.md
        notes.md
        data/
          case.json
          messages.json
          drafts.json
        views/
          messages/
            <message_id>.md
        drafts/
        files/
    notifications/
      <archive_uid>-<name>/
        archive.md
        notes.md
        data/
          archive.json
        views/
          messages/
            <message_id>.md
  messages/
    <message_id>.json
  .afmail/
    DO_NOT_EDIT.txt
    config.json
    workspace.lock
    logs/events.jsonl
    transactions/
    push/
    messages/
      <message_id>.eml
      <message_id>.state.json
      <message_id>.remote.json
      <message_id>.files/
    templates/
      en-US/
      zh-CN/
    deleted/

triage/ and cases/ are active attention surfaces. archive/cases/ contains archived case workspaces addressed by case UID. archive/notifications/<archive_uid>-<name>/ contains direct archived messages in one archive category.

Case roots contain only user-facing Markdown entry points (case.md and notes.md) plus working directories. Direct archive roots contain archive.md and notes.md. Canonical local object state lives under data/; generated, rebuildable Markdown detail views live under views/. drafts/ and files/ are user-visible working materials.

case.md, archive.md, triage/*.md, and views/**/*.md are generated read views. They are safe to rebuild with afmail render refresh; use notes.md for durable notes instead of generated views. Case and archive message links point to views/messages/<message_id>.md.

.afmail/DO_NOT_EDIT.txt is a warning sentinel. The rest of .afmail/ is machine-managed evidence, remote state, push queue, and audit history; use the CLI for effects instead of editing it by hand. .afmail/templates/ is the only user-editable exception under .afmail/.

Persisted JSON state documents identify their on-disk format with schema_name and schema_version. CLI stdout, diagnostics, errors, and .afmail/logs/events.jsonl audit events remain Agent-First Data protocol messages and use code.

The managed .gitignore intentionally does not ignore .afmail/messages/ or .afmail/push/: raw mail evidence, local disposition sidecars, remote metadata, and pending push operations are durable local state. Tracking those files in git means the repository contains private mail bodies and attachment bytes. The managed ignore block covers rebuildable/runtime files such as messages/*.json, triage/*.md, generated object Markdown views, .afmail/logs/, .afmail/transactions/, and .afmail/workspace.lock.

Message State

Message evidence lives in .afmail/messages/<message_id>.eml. Local disposition lives in .afmail/messages/<message_id>.state.json; remote mailbox metadata lives in .afmail/messages/<message_id>.remote.json; parsed messages/<message_id>.json files are rebuildable cache. Triage, case, and archive views are generated from that evidence and workspace state. Inbound attachments belong to the message. Attachment metadata is stored on the message record; afmail message attachment fetch MESSAGE_ID [PART_ID] materializes files under .afmail/messages/<message_id>.files/ and refreshes generated read views so fetched paths appear in message renderings.

A message can be referenced by multiple active or archived cases. A message can belong to at most one direct-message archive category. If a message needs multiple contexts, create or use cases instead of multi-archiving the direct message.

Cases

Active cases live at cases/<group>/<case_uid>-<name>/. Archived cases live at archive/cases/<case_uid>-<name>/. case_uid is globally unique and stable across active and archived cases. rename --name updates data/case.json and the readable directory suffix.

Case refs must start with cYYYYMMDDNNN; archive refs must start with aYYYYMMDDNNN. A ref may include a readable suffix after one dash, so c20260521001-anything and c20260521001 are equivalent. Names alone are not looked up. Group and tag values are local path-segment identifiers. Human names may use Unicode such as Chinese, for example 应用反馈-肥料登记 or 服务通知. Do not use path separators (/ or \) or the dot-only segments ./...

Case metadata is canonical in data/case.json. Active cases use status: "active"; archived cases use status: "archived" plus archived_rfc3339. Case membership is canonical in data/messages.json. Archived cases do not use direct-message archive categories.

Case-local data/drafts.json files are afmail-managed machine state. They record the last validated and composed hashes for drafts/*.md files so afmail can detect edits after validation or compose. Humans and agents should edit draft Markdown, not data/drafts.json.

Drafts And Case Files

Draft Markdown lives under a case drafts/ directory. Outbound attachments belong to the draft/case, not to inbound message evidence. Use afmail case draft attach REF DRAFT_NAME PATH to add one: external files are copied into the case files/ directory with a safe filename, and files already inside the case are recorded as case-relative paths without another copy.

The draft frontmatter attachments: list contains case-relative paths such as files/screenshot.png. Validation and compose check that each path is relative, safe, and points to an existing file under the case workspace. Adding or editing attachments changes the draft, so run draft validate and compose again before pushing outbound effects.

Direct Message Archive Categories

The canonical membership file for a direct-message archive category is archive/notifications/<archive_uid>-<name>/data/archive.json:

{
  "schema_name": "archive_messages",
  "schema_version": 1,
  "archive_uid": "a20260521001",
  "archive_name": "服务通知",
  "items": [
    {
      "message_id": "message_20260415_4e218374a33cbdc5",
      "summary": "Contacts Permissions policy update; review if app uses contacts.",
      "archived_rfc3339": "2026-06-01T17:30:00Z"
    }
  ]
}

summary is optional. Generated archive.md renders a Markdown list using archive.message_index config. The built-in archive templates display the message subject when a summary field is empty. Generated message views live under archive/notifications/<archive_uid>-<name>/views/messages/<message_id>.md.

Generated Read-View Templates

Built-in MiniJinja Markdown templates render generated read views and human-facing scaffolds. A workspace can override language-specific templates under .afmail/templates/<language>/; generic .afmail/templates/<key> files are ignored.

Common generated-view template keys include:

Run afmail render templates to export built-ins, then afmail render refresh to rebuild generated Markdown after template edits.

Deleted Remote Messages

When a remote message disappears and has no case/archive reference, afmail moves its evidence to .afmail/deleted/ and removes the materialized messages/<message_id>.json cache. When a referenced remote message disappears, afmail keeps a tombstone message under .afmail/messages/ with remote locations marked missing so existing case/archive state stays resolvable.

Notes

notes.md files are plain Markdown with no frontmatter. They exist for active cases, archived cases, and direct-message archive categories. They are the only durable local notes surface inside those objects; generated views are disposable.