v1.0.0 to v1.0.0
81 added, 85 removed. Audit A to A.
---
name: email-manual-actions-and-storage
description: >
- Focused Email reference for action inputs, folders, check filters, delivery
- side effects, self-send/time capsules, contacts, and mailbox persistence.
- Read after email-manual when an operation needs more than the first-call map.
+ Focused Email reference for action inputs, folders, check filters, delivery,
+ self-send/time capsules, contacts, attachments, and mailbox persistence.
+ Read when the schema and first-call map are not enough.
version: 1.0.0
tags: [lingtai, email, actions, mailbox, storage]
- last_changed_at: "2026-09-06T00:00:00Z"
+ last_changed_at: "2026-09-09T10:17:00Z"
related_files:
- src/lingtai/tools/email/manual/SKILL.md
- src/lingtai/tools/email/_family_schema.py
- src/lingtai/tools/email/manager.py
- src/lingtai/tools/email/primitives.py
- src/lingtai/tools/email/settings.py
- src/lingtai/tools/email/CONTRACT.md
maintenance: |
Tracks Email action behavior, input details, mailbox layout, and persistence guidance; update when action contracts or storage ownership changes.
---
# Email actions and storage
- The root manual is the first-call router. This page supplies action-level detail;
- input keys remain closed to the action that owns them. Omitted optional values
- use the action defaults. An explicit `null` is treated as omitted at the family
+ The root manual is the first-call router and the schema is authoritative. This
+ page covers action details that need care; action input objects remain closed.
+ Omitted optional values use defaults, and explicit `null` is omitted at the family
boundary.
- ## Sending
+ ## Send
```python
email(action="send", input={
"address": "peer", "subject": "status", "message": "ready",
"cc": ["human"], "bcc": [], "attachments": [], "delay": 0,
+ "mode": "peer", "type": "normal",
}, reasoning="report status")
```
- `address` is a bare peer name (or an authorized absolute path in `abs` mode).
- `cc` is visible to recipients; `bcc` is stored in the sender's copy but hidden
- from recipients. `attachments` contains source file paths. Each recipient
- adapter validates every path before creating that recipient's inbox entry; a
- missing or non-file path leaves no partial recipient inbox entry. Accepted files
- are copied into the message's recipient-local `attachments/` directory before
- atomic publication, and `message.json` records those snapshot paths. Duplicate
- basenames receive `-1`, `-2`, ... suffixes before the extension. Email enforces
- no attachment-size or source-root-containment limit, so callers must select only
- explicitly authorized files. This is separate from `message`, which is capped at
- 50,000 Unicode characters at send time; oversize bodies are rejected with the
- limit and actual size rather than truncated.
-
- A send writes sender-side outbox/sent state synchronously, then starts one daemon
- mailman thread per recipient. A successful `{status: "sent"}` receipt means the
- attempt was scheduled, not necessarily delivered. `delay` is a non-negative
- number of seconds before the one delivery attempt; the outbox record exists while
- the thread waits. See [Notifications and delivery](../notifications-and-delivery/SKILL.md)
- for liveness and bounce behavior. Identical consecutive sends are guarded as a
- loop; the installed pass count is in [Settings reference](../settings-reference/SKILL.md).
+ `address` is a bare peer name, or an explicitly authorized absolute path with
+ `mode="abs"`. CC is visible to recipients; BCC is stored only in the sender's
+ copy. `attachments` are source paths: for non-self POSIX delivery the adapter
+ validates every path before publishing that recipient's inbox entry, copies files
+ into recipient-local `attachments/`, and suffixes duplicate basenames. Source
+ paths are resolved/read at delivery time, so preserve their bytes until delivery;
+ there is no send-time frozen snapshot. No attachment-size or source-root-containment
+ limit is enforced: select only files you are authorized to share. A self-send bypasses this adapter
+ and does not validate or copy attachments; its inbox note can retain a missing
+ source path. Do not treat self-mail as an attachment backup. The message body is capped at 50,000 Unicode characters and is rejected,
+ not truncated, when oversized.
- ## Listing and filtering
+ Each recipient's outbox is written before its thread starts; the unified sent
+ record follows all thread starts. `status="sent"` is scheduling, not acceptance.
+ Use a non-negative integer `delay` for one future attempt, not tool execution;
+ the schema does not enforce a non-negative bound. For partial failures and restart
+ limits read [Notifications and delivery](../notifications-and-delivery/SKILL.md).
+ The duplicate guard compares recipient/body, not the whole message; see
+ [Settings reference](../settings-reference/SKILL.md#duplicate-send-loop-guard).
- `check` lists newest-first by default from `inbox`; `folder` can be `inbox`,
- `sent`, or `archive`, and `n` defaults to 10. Its optional `filter` is owned by
- `check` alone:
+ ## Check and search
- ```python
- email(action="check", input={
- "folder": "inbox", "n": 20,
- "filter": {
- "sort": "newest", "from": "peer", "subject": "status",
- "contains": "blocker", "after": "2026-04-01T00:00:00Z",
- "before": "2026-05-01T00:00:00Z", "unread_only": True,
- "has_attachments": False, "truncate": 500,
- },
- }, reasoning="find unread status mail")
- ```
+ `check` lists newest first from `inbox`; `folder` may be `inbox`, `sent`, or
+ `archive`, and `n` defaults to 10. Its only filter object supports:
- `sort` is `newest` (default) or `oldest`; `from`, `subject`, and `contains`
- are case-insensitive substring filters; `after`/`before` accept ISO 8601
- timestamps; the two boolean filters select unread or attachment-bearing mail.
- `truncate` controls preview characters (default 500); `0` requests the full
- body. A check result can be trimmed by Email's token budget and reports that
- fact. Prefer filtering in the call rather than retrieving a large mailbox and
- post-filtering mentally.
+ - `sort`: `newest` (default) or `oldest`;
+ - `from`, `subject`, `contains`: case-insensitive substrings;
+ - `after`, `before`: ISO 8601 timestamps;
+ - `unread_only`, `has_attachments`: boolean selectors;
+ - `truncate`: preview characters, default 500; `0` requests the full body.
- `search` takes a regular-expression `query` and optional `folder`; it searches
- sender, subject, and body and rejects invalid regexes. It does not accept
- `filter` or `n`.
+ Email may trim a large `check` result to its token budget and reports that fact.
+ `search` takes a regex `query` and optional `folder`, searches sender/subject/body,
+ and rejects invalid regex; it accepts neither `filter` nor `n`. With no folder,
+ `search` covers inbox + sent. Folder-less `read` searches inbox, sent, then archive;
+ only inbox records are marked read.
- ## Reading, mutating, and contacts
+ ## Read, mutate, and contacts
- - `read` takes a list of mailbox IDs, returns source records (including
- attachments), and marks inbox IDs read.
- - `dismiss` takes a list of IDs and marks handled inbox mail read without
- returning bodies. Prefer it after handling a body already in persistent
- notification context.
- - `reply` and `reply_all` take one ID and a message and route from the original
- mail. Recipient selection, subject derivation, and the absence of persistent
- thread metadata are owned by [Addressing and replies](../addressing-and-replies/SKILL.md#same-channel-reply-discipline).
- Both obey the root manual's same-channel rule.
- - `archive` moves inbox IDs to `mailbox/archive`; it removes them from the read
- set as part of the move. `delete` permanently removes IDs from inbox or
- archive and refuses `sent`.
- - `contacts` lists the private address book. `add_contact` upserts by address;
- `remove_contact` deletes by address; `edit_contact` changes supplied name or
- note fields. Contact writes use a temporary file and atomic replacement.
+ - `read` takes mailbox IDs, returns source records including attachments, and marks
+ inbox IDs read. `dismiss` marks handled inbox IDs read without returning bodies.
+ - `reply`/`reply_all` take one ID and a message; routing and subject derivation are
+ in [Addressing and replies](../addressing-and-replies/SKILL.md).
+ - `archive` moves inbox IDs to `mailbox/archive` and removes them from the read set.
+ `delete` permanently removes inbox/archive IDs and refuses `sent`.
+ - `contacts` lists the private book; `add_contact` upserts; `remove_contact` deletes;
+ `edit_contact` changes only supplied name/note fields. Contact writes use a
+ temporary file and atomic replacement.
- All IDs must come from the current agent's own notification or mailbox result.
- Stale IDs produce a not-found hint; an ID from another working directory has no
- meaning here.
+ IDs must come from this agent's current notification or mailbox result. Stale or
+ foreign-working-directory IDs have no meaning and produce a not-found hint.
## Self-send and time capsules
- Sending to your own bare address creates an ordinary unread inbox message that
- survives molt, can be searched later, and remains in the unread lane until
- `dismiss`, `read`, `archive`, or `delete`. Add `delay` to make a one-shot time
- capsule: the outbox is written immediately, and the daemon mailman thread waits
- until the deadline. A delayed self-send is a future nudge, not delayed tool
- execution. Recurring sends are not supported; use a host scheduler through
- `shell-manual` for recurring work.
+ An already-delivered self-note survives molt; `dismiss`/`read` clear unread status
+ but retain the message, `archive` moves it, and `delete` removes it. Before delivery,
+ a delayed self-note depends on the current mailman daemon thread staying alive;
+ outbox persistence does not make that timer survive process exit. Use it as a
+ one-shot time capsule, not delayed tool execution or a durable scheduler.
+ Recurring or restart-resilient work belongs to `shell-manual`.
- ## Mailbox layout
+ ## Mailbox layout and retention
Paths are relative to the agent working directory:
```text
mailbox/inbox/<id>/message.json received mail
- mailbox/sent/<id>/message.json sent copy (one record per send call)
+ mailbox/sent/<id>/message.json one sent record per call
mailbox/archive/<id>/message.json archived inbox mail
mailbox/outbox/<id>/message.json pending/delayed send
mailbox/read.json read-ID set
mailbox/contacts.json private contact book
.notification/email.json producer-owned unread mirror
```
- Message JSON uses UTF-8. Mailbox message files are direct writes; contact writes
- are the atomic exception. BCC data is not exposed to recipients. The unread
- mirror is refreshed by every read-state mutation; its payload and delivery
- semantics are in the notifications reference.
+ Message JSON is UTF-8. Non-self POSIX recipient entries use staging + atomic
+ publication; self-inbox, outbox and unified sent records use direct writes.
+ `read.json` and contacts use temporary-file replacement. BCC is not exposed in
+ recipient payloads. Do not infer crash durability from these different write paths.
+
+ ## Cleanup / Footprint
+
+ Email leaves the paths above, recipient attachment snapshots, and ordinary Agent
+ logs/bounce events. Before mailbox retirement or when attachment growth matters,
+ inspect only this Agent's `mailbox/` and `.notification/email.json` with the
+ shared read-only count/byte recipe in `skills-manual` →
+ `reference/cleanup-footprint-contract.md#shared-footprint-check-recipe`.
+ Inspect metadata without broadcasting private bodies/addresses. Keep sole copies
+ of decisions, handoffs, attachments, contacts and pending/evidence records.
+
+ Show a dry-run report listing what would stay or go, obtain explicit human consent,
+ then prefer authorized `archive`/`delete` actions over filesystem removal. Do not
+ clean active outbox state. Cleanup is optional; without consent stop at the report.
+ If recording the audit, separately opt into the shared `logs/cleanup.jsonl` append
+ (timestamp, Email label, dry-run/apply, count, bytes, non-secret path summary,
+ approval). Inspection alone writes nothing; approved apply records actual results.