# Email Reference

Full reference for the email plugin. Load with `plugin-read` when detailed configuration or behaviour reference is needed.

## Setup

Setup is two steps in plain chat, in this order. **Do the provider lookup before asking for any password** — most providers reject the normal login password and require a credential the operator has to mint in the provider's own settings, so asking for "the password" first sends them down a path that cannot work.

**Step 1 — look up the provider.** As soon as you have the email address, call `email-provider-info` with it. The tool returns one of three outcomes:

- **Known provider** — it gives the IMAP/SMTP hosts (already inferred, no need to ask) and the provider's specific requirement: an app-specific password, a trusted-device/IMAP toggle, enabling IMAP on a paid plan, etc. Relay that requirement to the operator and wait for them to create the credential in their provider's settings. Name the provider and the step it needs — that is the whole point of this tool.
- **Unsupported provider** — some providers (e.g. Tuta/Tutanota) deliberately offer no IMAP/SMTP at all. The tool says so; tell the operator this account cannot be connected here, and stop.
- **Unknown provider** — no host mapping. Ask the operator for the IMAP host/port and SMTP host/port (their provider's "mail client" / "IMAP/SMTP" help page lists these) along with the password.

**Step 2 — collect the credential and connect.** Once the operator has the right password (the app-specific one for known providers, or the account password where no app step is required), call `email-setup`. For a known provider, pass only `password` (and `agentAddress` if it differs) — the hosts are inferred. For an unknown provider, pass the hosts the operator gave you as well.

`email-setup` accepts these fields:

| name | required | notes |
|------|----------|-------|
| `email` | yes | the address being set up |
| `password` | yes | the app-specific password where the provider requires one, otherwise the account password |
| `imapHost` / `imapPort` / `imapSecurity` | only for unknown providers | inferred for known providers |
| `smtpHost` / `smtpPort` / `smtpSecurity` | only for unknown providers | inferred for known providers |
| `agentAddress` | no | for catchall/alias setups where the auth email differs from the address the agent operates as; leave empty when they're the same |

On success, confirm "credentials stored" and the inbox count. On failure, relay the diagnostic — the tool returns specific guidance (wrong password, connection refused, TLS mismatch); an authentication failure on a known provider usually means the operator used the login password instead of the app-specific one.

The agent never displays stored passwords.

## Reading and Searching

`email-read` and `email-search` return message metadata only: UID, sender, subject, and date. Body content is not included — present results as a message list without commenting on missing bodies.

### Pagination

Both tools return up to 50 messages per request (default 20). When more messages exist beyond the returned set, the response includes a `next_before_uid` value. Pass this value as the `before_uid` parameter on the next call to retrieve the next page of older messages. When `next_before_uid` is absent, all matching messages have been returned.

### Folders

Both tools accept a `folder` parameter: `"inbox"` (default) or `"sent"`. The Sent folder is discovered automatically via the IMAP server's special-use flags — no folder name guessing required.

### Filtering

Both tools support filtering by:
- `sender` — sender address or domain
- `to` — recipient address
- `since` / `before` — ISO date range
- `email-read` additionally supports `subjectPattern` (regex)
- `email-search` additionally supports `subject` (text) and `body` (keyword)

## Replying

`email-reply` sends a reply to an existing email, threaded correctly in the recipient's mail client.

The tool accepts the `messageId` of the email being replied to (visible in `email-read` output as the `Message-ID` field). It looks up the original email in the graph, sets `In-Reply-To` and `References` headers, derives the recipient from the original sender, and prepends `Re:` to the subject (stripping duplicate prefixes).

- **Reply to sender:** Default behaviour — replies to the original sender only.
- **Reply all:** Set `replyAll: true` to include all original recipients (TO + CC). The agent's own address is automatically excluded from the recipient list.
- **Threading:** The reply appears in the same conversation thread in Gmail, Apple Mail, Outlook, and other RFC 2822-compliant clients.
- **Prerequisite:** The original email must exist in the graph (ingested via the operator-initiated `email-ingest` flow). If the message hasn't been ingested yet, the tool returns a clear error.

Use `email-reply` when responding to a received email. Use `email-send` for new outbound messages.

## Attachments

Both `email-send` and `email-reply` accept an optional `attachments` array of absolute file paths. Each path must point to a regular file inside the account directory and be 25 MB or smaller; the path is validated before the message is sent, so an invalid, missing, oversized, or out-of-account path fails the call with a clear error and no email dispatched. Validated files ride the message as real SMTP attachments, and the success message names each attached file.

## Alias Support

When `agentAddress` is set and differs from the auth email:

- **Sending:** emails are sent with the agent address as the From header. SMTP authentication still uses the auth email.
- **Reading/searching:** only emails whose TO header contains the agent address are returned. The agent ignores emails to other addresses on the same mailbox.
- **OTP extraction:** only OTPs addressed to the agent address are found.
- **Status:** reports both the agent address and the auth email.

When `agentAddress` is not set or matches the auth email, all tools behave as before — no filtering, From header uses the auth email.

## Email Persistence

Emails enter the graph as `:ConversationArchive {source:'email'}` thread archives (with `:Section` chunks bounded by gap-hour sessionisation) via the **operator-initiated ingestion flow** (see `skills/email-ingest/SKILL.md`). There is no background poll, cron, or auto-classifier — new messages are written only when the operator says "check my emails" and approves each one.

The two ingestion tools:

- **`email-fetch`** — lists new IMAP messages since the stored high-water mark (`lastFetchedUid` on the `:EmailAccount` node). Returns sender, subject, date, Message-ID, UID, and a body preview. Writes nothing to the graph; stages the batch in-memory for the follow-up `email-ingest` call.
- **`email-ingest`** — takes a `decisions` array (`messageId` + `ingest` or `discard` per message). Writes only the `ingest` subset to the graph, advances `lastFetchedUid` to the batch's max UID, and consumes the staged batch. Refuses if any pending Message-ID lacks a decision.

Properties of the persistence layer:

- **Operator-initiated only.** No `setInterval`, cron, or systemd timer ever invokes `email-fetch`. The operator drives every cycle.
- **Deduplication:** Each archive is keyed by `conversationIdentity` (sha256 over `accountId + sortedParticipantElementIds`). Re-ingesting the same thread delta-appends new sections rather than duplicating.
- **Alias filtering:** Only emails addressed TO the agent's `agentAddress` are listed and ingested.
- **Semantic search:** Sections are vector-indexed via `section_embedding`, enabling natural-language search via `email-graph-query` that ranks parent archives by their best-matching section.
- **Isolation:** Each agent sees only archives whose participants resolve to its own account scope, even when sharing a catchall mailbox.

## Email Threading

Ingested messages are grouped into one `:ConversationArchive {source:'email'}` per thread, with operator-confirmed `:Person`/`:AdminUser` participants attached via `:PARTICIPANT_IN` edges. Thread grouping (`In-Reply-To`/`References` walk to the root Message-ID) happens in the email-plugin dispatcher before the conversation-archive pipeline sees the batch.

- **Out-of-order arrival:** If a reply is ingested before its parent, the dispatcher treats the reply as its own thread root; once the parent arrives the dispatcher re-groups under the older root and the conversation-archive writer delta-appends into the shared archive.
- **Per-message context:** Per-message envelopes are not stored in the graph. `email-read` and `email-search` surface a best-effort `Archive` / `Archive-Section` hint when the IMAP message's `receivedAt` falls inside a stored section's `firstMessageAt..lastMessageAt` window; this is a UI hint, not a primary key.
- **Scope:** Thread linkage never crosses account boundaries — every archive carries `accountId` and the writer refuses cross-account participants.

### Agent-Email Binding

Each email address is bound to exactly one agent. Each agent can have at most one email address. This is enforced during `email-setup`:

- The `agentSlug` parameter identifies which agent owns this email address (defaults to `"admin"`)
- If the address is already bound to a different agent, setup fails with a clear error naming the conflicting agent
- The binding is on `agentAddress` (the identity the agent presents), not the auth email

### Email Retrieval Paths

Three retrieval paths exist for email data — each scoped to a different purpose:

- **Live inbox** (`email-read`, `email-search`) — queries IMAP directly. Use for checking the inbox, reading recent messages, browsing folders, and real-time inbox state. Requires IMAP connectivity.
- **Graph list/search** (`email-graph-query`) — queries stored Email nodes in Neo4j. Use for email history, recall, and semantic search ("what emails are in memory?", "find emails about cabbages", "what did Sarah send about the contract?"). Works without IMAP. Supports date/sender/direction filters and natural-language semantic matching.
- **General knowledge** (`memory-search`) — searches the entire knowledge graph across all node types. Use when the user's question spans all memory, not just emails ("what do you know about contract renewals?").

When the user asks about stored emails, email history, or email recall — use `email-graph-query`. When they ask to check the inbox or read specific recent messages — use the IMAP tools. When the question is broader than emails — use `memory-search`.

## Inbound Email Screening

There is no automated screener. The operator screens by eye during the `email-ingest` skill flow: `email-fetch` lists the new messages with sender, subject, and a body preview; the operator picks which ones to ingest and which to discard. A `screening` property of `clean` is stamped on every operator-approved row as a provenance marker (operator-approved), and discarded messages never enter the graph.

The previous Haiku classifier was removed in Task 424; the auto-respond binary that depended on it was removed alongside the screener. Both are intentionally absent — the doctrine is that every LLM-bearing pipeline is operator-initiated.

## Security

- Credentials stored as a file with restricted permissions — never in conversation
- IMAP and SMTP connections require TLS or STARTTLS — plaintext is rejected
- Only the agent's own dedicated account is accessed — never the user's personal email
- Email nodes have `scope: "admin"` — never visible to public agents
- Inbound messages enter the graph only when the operator approves them by eye via `email-ingest`

## OTP Integration

Other skills call `email-otp-extract` during service authentication (Anthropic OAuth, Cloudflare setup) with:
- `sender` — expected sender domain
- `subject_pattern` — optional regex for subject line
- `timeout` — how long to poll (default 60s)

The tool polls at short intervals, extracts the verification code, and returns it. On timeout, it returns a clear failure so the calling skill can ask the user to check manually.
