# Email Reference

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

## Setup

Collect email credentials via a form. Render this `form` component with `render-component`:

**Fields:**

| name | label | type | required | placeholder |
|------|-------|------|----------|-------------|
| `email` | Email address | text | yes | user@example.com |
| `password` | Password | text | yes | App password or account password |
| `imapHost` | IMAP host | text | yes | imap.example.com |
| `imapPort` | IMAP port | number | yes | 993 |
| `imapSecurity` | IMAP security | select | yes | Options: `tls` (default), `starttls` |
| `smtpHost` | SMTP host | text | yes | smtp.example.com |
| `smtpPort` | SMTP port | number | yes | 587 |
| `smtpSecurity` | SMTP security | select | yes | Options: `tls`, `starttls` (default) |
| `agentAddress` | Agent email address (if different from login) | text | no | agent@yourdomain.com |

The `agentAddress` field is for catchall/alias setups where the authentication email differs from the address the agent operates as. Leave empty when they're the same.

After form submission, pass all fields to `email-setup`. 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).

The agent never displays stored passwords. Never name or recommend specific email providers — the form collects the standard fields every provider gives its users.

## 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 (stored by the background polling job). If the email hasn't been fetched yet, the tool returns a clear error.

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

## 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 are automatically polled from IMAP and stored as `Email` nodes in the graph. This happens via a background cron job — no manual triggering needed.

- **Polling:** After `email-setup` completes, the platform polls IMAP at the configured interval (default: every 5 minutes). Only emails addressed TO the agent's `agentAddress` are stored.
- **Deduplication:** Each email is identified by its Message-ID header. Re-polling the same messages does not create duplicates, even if the agent's email address changes between poll cycles. A composite unique constraint on `(messageId, accountId)` provides database-level enforcement.
- **Semantic search:** Stored emails are vector-indexed (subject + body preview), enabling natural-language search over email history via `email-graph-query`.
- **Isolation:** Each agent sees only emails addressed to its own bound email address, even when sharing a catchall mailbox.

## Email Threading

Emails are linked into conversation threads via `REPLY_TO` graph edges. When an email has an `In-Reply-To` header, the platform looks up the parent email by `Message-ID` within the same account and creates an edge. Thread linking happens automatically during the polling cron job.

- **Out-of-order delivery:** If a reply arrives before its parent, the edge is created later when the parent is stored (orphan back-fill).
- **Thread context:** `email-read` and `email-search` include `Thread-Depth` (number of hops to the thread root) and `Thread-ID` (emailId of the root message) for any email that is part of a thread. Root emails (no parent) have no thread fields.
- **Scope:** Thread edges never cross account boundaries — parent lookups are always scoped to the same account.

## Auto-Respond Audit Trail

When auto-respond sends a reply, the outbound message is stored as an `Email` node with `direction: "outbound"` and a `REPLY_TO` edge to the original inbound email. This creates a queryable audit trail of every automated reply — what the agent said, when, and to whom.

Outbound email nodes have the same properties as inbound emails (subject, bodyPreview, fromAddress, toAddresses, timestamps) plus the `direction` property. They appear in thread traversals alongside inbound messages.

### 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

Every inbound email is classified by Claude Haiku before entering Neo4j. The classifier examines `fromAddress`, `fromName`, `subject`, and the first 1024 characters of `bodyPreview`.

### Verdicts

| Verdict | Stored? | Embedding? | Auto-respond? | Graph query? |
|---------|---------|------------|---------------|--------------|
| `clean` | Yes | Yes | Yes | Yes |
| `suspicious` | Yes (with `screeningReason`) | Yes | No | Yes |
| `discarded` | Yes (with `screeningReason`) | No | No | No (list mode) |

- **clean** — legitimate personal or business correspondence
- **suspicious** — phishing indicators (urgency + credential requests, mismatched sender), prompt injection patterns, or classification failure (safe default)
- **discarded** — obvious spam, marketing bulk mail, auto-generated notifications with no actionable content

### Prompt injection flag

When `promptInjectionFlag` is true (body contains instruction-like patterns targeting an AI agent), auto-respond skips the email entirely regardless of verdict. No reply is generated. The email is visible in Neo4j for admin review.

### Failure handling

If the Haiku API is unavailable (network, rate limit, auth failure), the email defaults to `suspicious`. The entire fetch batch is never blocked — each email is classified independently. API key absence causes all emails in the batch to default to `suspicious`.

### Logs

Classification verdicts are logged to `{accountDir}/logs/email-fetch.log` with per-email detail (fromAddress, verdict, reason, promptInjectionRisk) and batch summaries.

## 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 emails are classified by Haiku before storage — prompt injection and phishing are flagged

## Auto-Respond

When enabled, a public agent automatically replies to incoming emails. A cron job polls the inbox for new messages and generates replies via the assigned agent.

### Setup

To enable auto-respond, call `email-auto-respond-config` with `enabled: true`. The tool requires an `agentSlug` — render a `single-select` component with available public agents so the admin can choose which agent handles responses. The tool returns the agent list when called without a slug.

When called without an `agentSlug`, the tool returns available agents. Present these as a `single-select` component for the admin to choose from, then call the tool again with the selected slug.

### Configuration

- `enabled` — `true` to enable, `false` to disable
- `agentSlug` — slug of the public agent that responds (required when enabling)
- `pollIntervalMinutes` — how often to check for new emails (default: 5, range: 1–60)
- `hourlyLimit` — maximum auto-replies per clock hour UTC (default: 20, range: 1–1000)
- `dailyLimit` — maximum auto-replies per calendar day UTC (default: 100, range: 1–10000)

### Behaviour

- The cron job runs every minute. Each account's poll is skipped if the configured interval hasn't elapsed since the last poll.
- Only emails addressed TO the agent's email address are processed (alias filtering applies).
- Auto-replies, mailing list messages, and emails from the agent's own address are automatically skipped (RFC 3834 loop prevention).
- Outgoing replies include `In-Reply-To` and `References` headers for correct threading, and `Auto-Submitted: auto-replied` to prevent loops with other auto-responders.
- Every reply includes a deterministic AI disclosure footer ("This reply was generated by an AI assistant.") appended after the agent's response. This is a regulatory requirement (EU AI Act Article 50) and is not dependent on the LLM including disclosure in its generated text.
- The subject line carries a single `Re:` prefix (no doubling).

### Rate Limiting

Per-account send caps prevent runaway auto-replies from spam waves, mailing list explosions, or deliberate attacks. Two independent limits:

- **Hourly limit** (default 20) — resets at each UTC clock-hour boundary
- **Daily limit** (default 100) — resets at midnight UTC

When either limit is reached, remaining emails in the poll cycle are skipped (not replied to). Auto-respond is **not** disabled — counters reset naturally at the next boundary. A medium-priority admin Task is created to notify the admin.

Within a single poll cycle, only one auto-reply is sent per unique sender address. This prevents reply storms when an automated sender delivers multiple messages. Sender deduplication is case-insensitive.

### Circuit Breaker

After 3 consecutive failures (IMAP errors, API failures, SMTP rejections), auto-respond is automatically disabled and a high-priority Task is created for the admin. The admin sees this task at the start of their next session and can re-enable auto-respond after resolving the issue.

### Disabling

Call `email-auto-respond-config` with `enabled: false`. This clears the agent assignment and resets the failure counter.

## 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.
