---
name: email-composition
description: "Conversational wrapper around the email plugin tools. Activate when the operator asks to reply to an email, draft a new email, follow up on a thread, triage outstanding inbox items, or delete messages (e.g. 'reply to Sarah about X', 'email Joel about Y', 'what's outstanding in my inbox', 'delete the npm emails'). Orchestrates `email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`, `email-draft`, `email-draft-edit`, `email-draft-send`, `email-delete`. Every send and every delete is operator-approved — never auto-sends, never deletes on inference; delete moves to Trash (recoverable), never expunges. When the operator wants the email left unsent for their own review, the approved draft is saved via email-draft (and revised later via email-draft-edit), never dispatched until an explicit send."
---

# Email composition

Invoked from `specialists:personal-assistant`.

Conversational wrapper around the existing `email-*` tools. Drafting, threading, triage. **Human-in-the-loop only — every send is explicitly approved by the operator.** No drip sequences, no scheduled sends, no automatic categorisation.

## When to activate

Activate when the operator asks for any of:

- **Reply to a known thread.** "Reply to Sarah's email about the lease." "Draft a reply to that thread from Joel."
- **New outbound email.** "Email Joel about the viewing." "Send a note to the team about Friday."
- **Follow-up on a topic.** "Follow up with everyone from yesterday's viewings."
- **Inbox triage.** "What's outstanding in my inbox?" "Anything I need to deal with?"

Do **not** activate for:

- Listing or searching emails as the end-goal (no draft intent) — let `email-search` / `email-graph-query` answer directly.
- Setup, status checks, OTP extraction — those are direct tool calls. (For Google/Workspace setup the agent must require an app password, never the regular password — see the email plugin's `references/email-reference.md` § Setup → Google, opened with the `plugin-read` tool (`file="references/email-reference.md"`), which resolves it at the plugin root; a plain `Read` against this skill's base directory misses. For Microsoft / Microsoft 365 addresses there is no email-plugin path at all — register via the `outlook` Graph plugin's `outlook-account-register`, not `email-setup` — see § Setup → Microsoft.)
- Any automated send loop. If the operator asks for drip / scheduled / broadcast sends, decline and explain that this skill is human-in-the-loop only.

## Flow — reply to an existing thread

### 1. Identify the thread

Pick the right retrieval path:

- **Graph history first** (`email-graph-query`) — for "reply to Sarah's email about the lease": the operator is naming a known correspondent and topic. Fast, semantic, covers the operator's full email history.
- **Live IMAP** (`email-search` or `email-read`) — for "that email that just came in" or "today's reply from Joel": the operator is naming a recent message that may not yet be in the graph.

If the search returns **multiple plausible threads**, do not guess. Surface 3-5 candidates with sender, subject, date, and a one-line summary, and ask which one. Emit narration `[email-composition] ambiguous-thread candidates=<n>`.

If the search returns **zero matches**, ask the operator to refine (different correspondent? different subject phrasing? wider date range?). Do not invent a thread.

Once the thread is locked, emit narration `[email-composition] thread-identified messageId=<id> subject="<subject>"`.

### 2. Load thread context

Pull the full thread — the current message plus parent messages — via `email-read` or `email-graph-query`. Reply to the actual position of the conversation, not a stripped-down summary. Quote-stripped one-line summaries miss the question the operator is actually being asked.

If the thread has **attachments** that the draft will reference, list the attachment names in the draft presentation and ask whether to forward them. The five existing tools do not currently forward attachments inline — surface this so the operator can manually re-attach if needed.

### 3. Detect skip conditions

Before drafting, check the inbound for:

- **Out-of-office / vacation autoreply.** Skip. Flag to the operator: "this looks like an OOO autoreply — no draft." Do not propose a reply.
- **Unsubscribe / list-management mail** (List-Unsubscribe header, "to unsubscribe click here" body). Skip. Flag to the operator.
- **Hostile or grief tone.** Draft cautiously and flag the tone in the presentation: "this reads as hostile / grief-related — please review carefully before sending." Never auto-soften without surfacing the tone judgement.

### 4. Retrieve voice exemplars (conditional)

Call `voice-retrieve-conditioning` with `format: "email"`, `length: "short"`, and `topic` set to the inbound thread's subject (or, for a new outbound, the topic the operator named). The tool returns `{styleCard, exemplars[]}` — when `styleCard` is non-null, inject both the style card and the exemplar bodies into the drafting prompt before composing.

If the tool returns `{styleCard: null, exemplars: []}` (the operator has no `:VoiceProfile` yet — the `voice-mirror` plugin is core and present on every brand, so absence means no profile, not a missing plugin), draft in a **neutral British business register** — polite, concise, no slang, no transatlantic spellings — and state explicitly in the presentation: "drafted in neutral British business voice (no voice profile on file)."

Emit narration `[email-composition] voice-mirror styleCardBytes=<n> exemplars=<k>` when conditioning was applied. Absence of the line confirms the fallback path was taken.

### 5. Draft

Compose the reply. Anchor to:

- The specific question or position in the parent message.
- The operator's voice (from voice-mirror) or the neutral British business register.
- Any decisions the operator has already stated in this conversation (e.g. "say yes to the price, no to the timeline").

Keep drafts tight. Email is not a wall-of-text medium.

### 5b. Resolve the recipient and read their contact record

Before presenting any draft (reply or new outbound), resolve each named recipient to their `:Person` with `contact-lookup` (by email when you have it, else by name). When the contact resolves, read its properties and preferences: if the record carries routing (an always-CC, an alternate send-to address), apply it to the draft To/Cc and surface it in the presentation for the operator to approve. An explicit operator instruction on this send always overrides the stored routing. When the contact does not resolve, or carries no routing, leave Cc empty and draft to the operator-named address only. Never invent a recipient, and never create a `:Person` here.

Emit narration `[email-composition] contact-lookup recipient=<addr> resolved=<bool> prefsFound=<n> applied=<n>` for each resolved recipient, before the draft is presented.

### 6. Present

Show the draft inline with:

- **To** — recipient address (the original sender, unless the operator asked for `replyAll`).
- **Cc / Bcc** — empty unless the operator explicitly named one, or the recipient's contact record carries routing read in step 5b (an always-CC or alternate address), which is applied here and shown for approval. Never copy people forward from the original thread without being asked.
- **Subject** — the threaded subject (the original with `Re:` if not already prefixed; `email-reply` handles this).
- **Body** — the draft.
- **Notes** — any flags from step 3 (tone, attachments, OOO skip-already-applied).

Emit narration `[email-composition] draft-presented recipient=<addr> subjectPrefix="<first-60-chars>"`.

**Do not call `email-reply` at this step.** The send is not yet approved.

### 7. Edit loop

The operator may:

- **Approve** ("send it", "looks good", "yes"). Proceed to step 8.
- **Edit verbally** ("make it shorter", "drop the second paragraph", "say yes to the price but not the timeline"). Apply the edit and re-present. Stay in the loop until approval or abandonment.
- **Abandon** ("never mind", "I'll do it myself"). Emit `[email-composition] send-aborted reason=operator-abandoned`. Do not send.

Every operator edit on the draft (before final approval) is downstream feedback for `voice-mirror`. After the operator approves the final send, call `voice-record-feedback` with the original agent draft and the final edited version (`skill: "email-composition"`, `brief` set to the topic/subject). The call is fire-and-forget — failure is logged but does not block the send. The `voice-mirror` plugin is core (present on every brand), so this feedback call always runs.

### 8. Send

If the operator asked for a draft rather than a send, follow "Flow — save to Drafts instead of sending" — call `email-draft`, not `email-reply`/`email-send`.

On **explicit** approval, call `email-reply` with:

- `messageId` — the parent message ID from step 1. This is the threading anchor; `email-reply` populates `In-Reply-To` and `References` from it so the reply lands inside the existing thread, not as a new conversation.
- `replyAll` — only if the operator asked for it. Default is false (reply to sender only).
- Subject and body from the approved draft.

Emit narration `[email-composition] send-confirmed`.

Confirm to the operator that the send happened by quoting the `email-reply` tool's success line (it includes the sent `messageId` and the resolved `In-Reply-To`).

If `email-reply` errors, surface the error verbatim; do not silently retry. The operator decides whether to fix and retry or abandon.

## Flow — new outbound email

Same as the reply flow with two differences:

1. **No thread context load** (step 2 skipped). Instead, ask the operator for the recipient (if not already named), the topic, and any constraints (formal vs casual, length, things to include or exclude).
2. **Send via `email-send`**, not `email-reply`. There is no parent `messageId` because this is a new conversation.

Multi-recipient drafting: if the operator names a single person, use that address. If the operator names a group ("email the team", "everyone from yesterday's viewings"), ask which addresses to include — list candidates if you can resolve them from the graph, otherwise ask the operator to enumerate. Do not invent recipients.

## Flow — save to Drafts instead of sending

When the operator's stated outcome is a reviewable, unsent email — signalled by intent like "draft it", "leave it in drafts", "don't send yet", "the same as last time but leave it in the draft box", "let me review before sending" — the approval step changes target: the approved draft goes to `email-draft`, not `email-send`/`email-reply`. Everything upstream (thread identification, voice conditioning, the present-and-edit loop) is identical.

The outcome that matters: nothing is dispatched, and the operator finds the message in their mailbox's Drafts folder. Confirm by quoting the `email-draft` success line (it names the Drafts folder and the APPEND UID). If the server advertises no Drafts folder, `email-draft` fails naming the mailbox — surface that verbatim; do not fall back to sending.

A draft-intent request is still human-in-the-loop: present the draft and run the edit loop before saving, exactly as for a send. For a threaded draft-reply (a draft that should land inside an existing thread when the operator sends it by hand), pass the parent `messageId` to `email-draft`; it stamps `In-Reply-To`/`References` and a `Re:` subject from the parent, exactly like `email-reply`. The subject is taken from the thread, so do not pass one.

## Flow — revise or send a stored draft

A saved draft carries a Drafts UID (the `email-draft` / `email-draft-edit` confirmation line names it). Use that UID to revise or send the draft later.

- **Revise** ("change the draft I have", "update that draft", "fix the second paragraph in the draft"): run the present-and-edit loop on the new content, then call `email-draft-edit` with `targetUid` set to the draft's UID and the full revised fields (same shape as `email-draft`, plus `messageId` if it is a threaded draft). The tool appends the replacement before deleting the old copy, so the draft is never lost mid-revision. Confirm by quoting the success line — it names the folder and the old and new UIDs. Nothing is sent. Emit narration `[email-composition] draft-edited targetUid=<old> newUid=<new>`.
- **Send** ("send the draft", "send draft <uid>", "go ahead and send the one in drafts"): on **explicit** approval, call `email-draft-send` with the draft's `uid`. Recipients come from the draft's own To/Cc/Bcc headers. The draft leaves the Drafts folder only after SMTP accepts; quote the success line (sent recipients, Message-ID, removal confirmation). If it errors, surface the error verbatim and leave the draft in place — do not retry silently. Emit narration `[email-composition] draft-sent uid=<uid>`.

Sending a stored draft is a real send: human-in-the-loop, on explicit operator approval, exactly like `email-reply`/`email-send`.

## Flow — triage variant

When the operator asks "what's outstanding in my inbox" / "anything I need to deal with" / "what's unread":

1. Call `email-read` for the last N unread messages (default N = 10; operator can specify "last 25", "today's", etc.).
2. For each message, return a one-line summary with a recommended action:
   - **Reply** — needs a response.
   - **File** — informational, can be archived.
   - **Ignore** — autoreply, newsletter, list mail, OOO.
3. Emit narration `[email-composition] triage-returned n=<count>`.
4. Wait for the operator to pick one. When they do ("reply to #3", "draft a response to the Joel one"), drop into the reply flow at step 1 (thread already identified — use the `messageId` from the triage list).

The triage variant **never auto-drafts**. It returns the list and waits.

## Flow — delete (move to Trash)

When the operator asks to delete messages ("delete the npm emails", "bin those newsletters", "get rid of #2 and #4"), route the intent through `email-delete`, which moves the named messages to the mailbox Trash. This is recoverable — nothing is expunged — but it is still a per-action operator-approved step, exactly like a send: never delete on inference.

1. **Identify the exact messages.** Delete targets explicit `uids` from a prior `email-read`/`email-search`, scoped to the source `folder` (`inbox` or `sent`). If you do not already have the UIDs in hand, run `email-read`/`email-search` first and show what matched. Never delete by a search description alone.
2. **Confirm before deleting.** Present the messages you are about to move (sender, subject, UID) and get explicit operator approval. If more than the operator asked for matched, surface the difference and ask — never widen the set silently.
3. **Call `email-delete`** with the approved `uids` and `folder`. Emit narration `[email-composition] delete-confirmed uids=<n> folder=<src>`.
4. **Quote the result line** verbatim (`Moved N of M message(s) to <Trash>`). When `N < M`, some UIDs were already gone — say so plainly; it is not an error. If the mailbox has no Trash folder the call errors and nothing moves — surface the error verbatim and do not retry.

Delete is move-to-Trash only. There is no permanent/hard delete, no delete-by-criteria, and no restore-from-Trash in this skill — permanent removal is the operator's own mail client or the provider's Trash auto-purge.

## Edge cases

| Situation | Behaviour |
|-----------|-----------|
| Multiple threads match the operator's description | Surface 3-5 candidates and ask. Never guess. |
| CC / BCC in the original thread | Reply to sender only by default. Ask explicitly before copying others. |
| Recipient's contact record carries an always-CC or alternate address | Apply it to To/Cc in the draft and show it for approval. Explicit operator instruction on this send overrides it. |
| `replyAll` requested | Pass `replyAll: true` to `email-reply`. Otherwise omit (defaults false). |
| Original thread has attachments referenced in the draft | List attachment names; ask whether to forward. |
| Inbound is an out-of-office autoreply | Skip. Flag to operator. Do not draft. |
| Inbound is a list / unsubscribe mail | Skip. Flag to operator. Do not draft. |
| Inbound is hostile or grief-related | Draft cautiously; flag tone in the presentation. Operator must review explicitly. |
| Operator has no voice profile yet (`voice-retrieve-conditioning` returns no data) | Draft in neutral British business register; state the absence in the presentation. |
| `email-reply` returns an error after approval | Surface the error verbatim. Operator decides whether to fix and retry or abandon. Do not silent-retry. |
| Delete request without UIDs in hand | Run `email-read`/`email-search` first, show what matched, confirm, then `email-delete`. Never delete by description alone. |
| `email-delete` matched more than the operator named | Surface the difference and ask before deleting. Never widen the set silently. |
| `email-delete` reports `Moved N of M` with N < M | Some UIDs were already gone. State it plainly — not an error. |
| Mailbox has no Trash folder | `email-delete` errors and moves nothing. Surface verbatim; there is no expunge fallback. |

## Out of scope

Hard boundaries — never offer these in this skill, even if asked:

- **Drip sequences** — multi-step automated send chains. Decline; refer to a future task.
- **Scheduled / delayed sends** — there is no "send at 9am tomorrow" path. The operator triggers the send when they approve.
- **List broadcasts** — bulk sends to managed subscriber lists. Not this skill.
- **Inbox auto-categorisation / labelling** — triage is on demand only.
- **Calendar follow-up auto-writes** — if a reply confirms a meeting, mention it and let the operator decide whether to write a calendar entry separately.
- **Permanent / hard delete** — `email-delete` only moves to Trash. There is no expunge, no delete-by-criteria, and no restore-from-Trash here. Permanent removal is the operator's own mail client or the provider's Trash auto-purge.
- **New MCP tools** — this skill orchestrates the existing email tools (`email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`, `email-draft`, `email-draft-edit`, `email-draft-send`, `email-delete`). Nothing else.

## Observability

The skill emits narration at each major step so a session log makes the full draft → approval → send chain greppable:

| Step | Narration |
|------|-----------|
| Thread locked | `[email-composition] thread-identified messageId=<id> subject="<subject>"` |
| Multiple matches | `[email-composition] ambiguous-thread candidates=<n>` |
| Draft shown | `[email-composition] draft-presented recipient=<addr> subjectPrefix="<first-60-chars>"` |
| Contact resolved | `[email-composition] contact-lookup recipient=<addr> resolved=<bool> prefsFound=<n> applied=<n>` |
| Send approved | `[email-composition] send-confirmed` |
| Draft saved (unsent) | `[email-composition] draft-saved folder=<drafts-folder> uid=<uid>` |
| Draft revised | `[email-composition] draft-edited targetUid=<old> newUid=<new>` |
| Draft sent from Drafts | `[email-composition] draft-sent uid=<uid>` |
| Delete confirmed | `[email-composition] delete-confirmed uids=<n> folder=<src>` |
| Send abandoned | `[email-composition] send-aborted reason=<why>` |
| Triage list returned | `[email-composition] triage-returned n=<count>` |

The underlying `[email-send]` / `[email-reply]` / `[email-search]` / `[email-graph-query]` / `[email-read]` tool logs confirm the actual tool calls. Absence of an expected tool tag after `draft-presented` + operator approval is the failure signal — the skill said it sent but didn't.

Diagnostic on Pi:

```
grep -E '\[email-(composition|reply|send|draft|search|graph-query|read)\]|op=delete' <session-log>
```

returns the full chain for any thread.

## Dependencies

- **Required:** `email` plugin (`email-graph-query`, `email-search`, `email-read`, `email-send`, `email-reply`, `email-draft`, `email-draft-edit`, `email-draft-send`, `email-delete`).
- **Core:** `voice-mirror` plugin (present on every brand). When the operator has a style card, drafts use it and edits are captured as feedback; with no profile yet, drafts fall back to a neutral British business register.
