---
name: outlook
description: Microsoft 365 / Outlook.com via Microsoft Graph. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Per-account OAuth device-code flow; no client secret. One account can hold several mailboxes at once; every operational tool takes an optional mailbox selector (email or graphUserId) and refuses ambiguously when two or more are attached and none is named. Tools — outlook-account-register / outlook-account-register-poll: device-code register (start + poll); outlook-mail-list / outlook-mail-search: inbox triage (~255-char preview) with a date window (list) or KQL from:/subject: filters (search) and a nextCursor for paging beyond the newest page; outlook-mail-fetch-body: full body of one message by id; outlook-mail-attachment: list any message's attachments (received or draft), or download one file attachment's bytes to the account uploads folder; outlook-mail-reply: in-thread reply (replyAll, cc/bcc, attachments); outlook-mail-delete: move to Deleted Items (recoverable); outlook-mail-ingest: write operator-reviewed messages into the business graph as :ConversationArchive threads (per-message ingest/discard, conversationId threading, closed-set participant resolution); outlook-mail-otp-extract: poll for a one-time code; outlook-mail-send / outlook-draft / outlook-draft-edit / outlook-draft-send: compose, edit, and send, all with attachments — outlook-draft takes an optional replyToMessageId to draft a threaded reply, unsent, under the original conversation; outlook-calendar-list / outlook-calendar-event: read calendar; outlook-calendar-create / outlook-calendar-update / outlook-calendar-cancel / outlook-calendar-respond / outlook-calendar-freebusy: control calendar; outlook-contacts-list: contacts; outlook-mailbox-info: auth state + folder count for one mailbox; outlook-mailbox-list: every connected mailbox.
tools:
  - name: outlook-account-register
    publicAllowlist: false
    adminAllowlist: false
    riskClass: write_local
  - name: outlook-account-register-poll
    publicAllowlist: false
    adminAllowlist: false
    riskClass: write_local
  - name: outlook-mail-list
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mail-search
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mail-fetch-body
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mail-attachment
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mail-reply
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-mail-delete
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-mail-ingest
    publicAllowlist: false
    adminAllowlist: false
    riskClass: write_local
  - name: outlook-mail-otp-extract
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mail-send
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-draft
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-draft-send
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-draft-edit
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-calendar-list
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-calendar-event
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-calendar-create
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-calendar-update
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-calendar-cancel
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-calendar-respond
    publicAllowlist: false
    adminAllowlist: false
    riskClass: external
  - name: outlook-calendar-freebusy
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-contacts-list
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mailbox-info
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
  - name: outlook-mailbox-list
    publicAllowlist: false
    adminAllowlist: false
    riskClass: read
always: false
embed: ["admin"]
metadata: {"platform":{"optional":true}}
mcp:
  command: node
  args:
    - ${PLATFORM_ROOT}/lib/mcp-spawn-tee/dist/index.js
    - ${PLATFORM_ROOT}/plugins/outlook/mcp/dist/index.js
  env:
    MCP_SPAWN_TEE_NAME: outlook
    LOG_DIR: ${LOG_DIR}
    PLATFORM_ROOT: ${PLATFORM_ROOT}
    ACCOUNT_ID: ${ACCOUNT_ID}
    SESSION_ID: ${SESSION_ID}
mcp-manifest: auto
---

# Outlook

Microsoft Graph access for Outlook.com / Microsoft 365 mailboxes. Mail is read (list, search, full-body), reply, delete, and compose (draft, draft-edit, send); contacts are read-only; calendar is read + control. Admin agent only — public agent surface is explicitly excluded.

## Capabilities

- **Register:** `outlook-account-register` — starts the OAuth device-code flow against the brand's Entra app and returns a user code + verification URL immediately. The operator opens `microsoft.com/devicelogin` on any device, enters the code, and consents. The always-running server then completes registration on its own: a self-contained dispatcher (`dist/scripts/complete-registration.js`), armed on a 30 s interval, polls every live pending code to a terminal outcome, so a mailbox connects once the operator consents with no agent action. `outlook-account-register-poll` remains the manual path and the fallback, one check per call. Registering a second mailbox on the same account adds it alongside the first rather than replacing it; each mailbox holds its own encrypted tokens (AES-256-CBC) under a shared account key. One device-code flow runs at a time per account; starting another while one is live returns an `in-progress` signal. Observability: `devicecode-autopoll` per poll cycle and an independent `devicecode-stranded` audit for any pending code left un-harvested past its lifetime.
- **Multiple mailboxes:** one account can hold several mailboxes. Every operational tool takes an optional `mailbox` argument (email or graphUserId). With one mailbox, omit it. With two or more, name it — omitting it returns a refusal listing the connected mailboxes. `outlook-mailbox-list` shows them all.
- **Mail (read):** `outlook-mail-list` returns messages newest-first with an optional `since`/`before` date window, and `outlook-mail-search` runs a Graph `$search` (KQL) query where `from:`/`to:`/`subject:`/`received>=` filters live — both return Microsoft's short `bodyPreview` (~255 chars) for cheap triage and a `nextCursor` for paging beyond the 250-per-page window (the cursor is an opaque `@odata.nextLink`; pass it back as `cursor`). Graph forbids `$search`+`$filter`, so the strict date window is on the list tool and sender/subject filtering is on the search tool. `outlook-mail-fetch-body` reads the COMPLETE body of one message by id (HTML decoded to text, no preview cap) — the full-read path. All three read tools report `hasAttachments` per message, so the agent knows to reach for the attachment tool.
- **Mail (attachments):** `outlook-mail-attachment` lists or downloads any message's attachments — received or draft. Called with `messageId` alone it LISTS them (`id`, `name`, `contentType`, `size`, `isInline`, and a `kind` of `file`/`item`/`reference`) via `GET /me/messages/{id}/attachments` with `$select` (no bytes); a draft is a message in the same collection, so passing a draftId lists what that draft carries, and the returned `name`/`id` is the handle `outlook-draft-edit`'s `removeAttachments` takes. Called with `messageId`+`attachmentId` it DOWNLOADS one file attachment: the bytes are written to `{accountDir}/uploads/outlook/<messageHash>/<bytesHash>-<name>` at `0o600` and the saved absolute path is returned, ready for the Read tool, `SendUserFile`, or an `outlook-mail-reply` attachment. Only file attachments download — `item` (embedded message/event) and `reference` (cloud-drive link) kinds are refused, as are empty payloads and anything over the 25 MB cap; in each case nothing is written. Requires `Mail.Read`.
- **Mail (reply / delete):** `outlook-mail-reply` replies in-thread (native `conversationId` threading) with `replyAll`, added cc/bcc, and account-scoped file attachments (up to 25 MB per file — files over Graph's ~3 MB inline limit stream through a chunked upload session); the body is prepended above the quoted original. `outlook-mail-delete` moves messages to Deleted Items (recoverable — never hard-deletes). Both require `Mail.ReadWrite` (reply also needs `Mail.Send`).
- **Mail (ingest to graph):** `outlook-mail-ingest` writes operator-reviewed messages into the business graph as `:ConversationArchive {source:'email'}` threads, the analogue of the IMAP `email-ingest`. Input is `decisions`: one `{messageId, disposition:'ingest'|'discard'}` per listed message. Each `ingest` message's full body + envelope is fetched via `runMailFetchBody`, threads are grouped by Graph `conversationId`, and participant resolution is closed-set — every From/To/Cc address must already resolve to a `:Person.email` or `:AdminUser.email` for the `accountId`, or its thread is skipped and the address is returned to the operator (no `:Person` auto-create). Resolvable threads dispatch one at a time through the shared `plugins/memory/bin/conversation-archive-ingest.sh --source email` (same RFC822 thread-JSON shape the `email` normaliser consumes). This is the plugin's only Neo4j coupling (`neo4j-driver`). Human-in-the-loop: nothing enters the graph without an explicit per-message decision; the `outlook-mail-ingest` skill drives the list → summarise → ask → ingest loop. Requires `Mail.Read`.
- **Mail (OTP):** `outlook-mail-otp-extract` polls the inbox for a one-time verification code from a named sender, reading full bodies to extract it — used during service authentication.
- **Mail (send / draft):** `outlook-mail-send` sends a new message — via `POST /me/sendMail` with no attachments, or composed as a draft, attached, and sent when `attachments` is present (`/me/sendMail` carries attachments inline and cannot fit a large file); `outlook-draft` creates a draft, attaches any files, and returns its id — either a new message (`to` + `subject` required) or, with `replyToMessageId`, a threaded reply created via `POST /me/messages/{id}/createReply` that lands under the original conversation (the only way to draft a reply without sending it; `outlook-mail-reply` threads but always sends). On the threaded path Graph sets the recipients and `RE:` subject from the original and composes the body above its own quote — the same email `outlook-mail-reply` sends — so that path is plain text only and `to`, `subject`, and `isHtml: true` are refused rather than silently dropped; `replyAll` and `cc`/`bcc` work as on reply. `outlook-draft-edit` updates an existing draft in place (Graph drafts are mutable) and edits its attachments — `attachments` adds files, `removeAttachments` takes them off by name or attachment id, and both together replace a file in one call; `outlook-draft-send` sends an existing draft by id. Recipients are to/cc/bcc arrays; body is HTML or plain text via `isHtml`; `attachments` are file paths inside the account directory, capped at 25 MB per file and validated before any Graph write. Success asserted on Graph 202. Requires `Mail.Send` + `Mail.ReadWrite`; a mailbox on the old read-only consent returns an actionable "re-register to grant send" signal.
- **Calendar (read + control):** `outlook-calendar-list` and `outlook-calendar-event` read events; `outlook-calendar-create` books an event; `outlook-calendar-update` reschedules or edits one; `outlook-calendar-cancel` cancels a meeting (notifying attendees when the account organises it) or deletes a personal appointment; `outlook-calendar-respond` accepts, declines, or tentatively accepts an invite; `outlook-calendar-freebusy` reports availability for one or more addresses over a window. Writes require the `Calendars.ReadWrite` consent; a mailbox on stale read-only consent returns a distinct signal naming the scope and `outlook-account-register`.
- **Contacts (read-only):** `outlook-contacts-list` returns contacts.
- **Health:** `outlook-mailbox-info` reports auth state, refresh-window status, and top-level folder count for one mailbox (name it with `mailbox` when several are attached). `outlook-mailbox-list` lists every attached mailbox with its email, scopes, and token health, reading local stores only, never the network. It reports an `addressable` count and a per-mailbox `status` (`ok` / `blank-email` / `refresh-expired` / `unreadable`) so an authenticated, addressable mailbox is distinguishable from one that is merely present: a bare `count` of 1 is not "connected" unless that mailbox is `status:ok`. Use these to answer "did mailbox X auth?" without grepping logs.

## Out of scope

- Mail move (to arbitrary folders) / flag — write surfaces beyond reply, delete (move to Deleted Items), and compose are not implemented. Hard/permanent delete is deliberately excluded; `outlook-mail-delete` only moves to Deleted Items.
- Attachment-byte archival into the graph — `outlook-mail-ingest` lands text threads only; the IMAP path also archives attachment bytes as `:DigitalDocument`, and the Outlook equivalent is a separate concern (see out-of-scope).
- Contacts write — read-only.
- Recurring-series occurrence editing (edit-this-occurrence) beyond whole-series create/update.
- The `scheduling` (Neo4j graph) plugin and any auto-sync between it and the Outlook calendar.
- OneDrive, Files, Sites — separate plugin; different endpoints.
- Push notifications via `/subscriptions` — first cut polls.
- On-premises Exchange — Microsoft Graph deprecated hybrid REST 2023-07. The plugin detects on-prem mailboxes and routes the operator to the IMAP path (`email` plugin).
- M365 admin scopes (`User.Read.All`, `AuditLog.Read.All`) — separate plugin.

## References

| Reference | Topics |
|-----------|--------|
| [auth.md](references/auth.md) | Entra app registration, device-code flow, public-client requirement |
| [graph-surfaces.md](references/graph-surfaces.md) | Graph endpoint and response shape per tool |

## Skills

| Skill | Purpose |
|-------|---------|
| [outlook](skills/outlook/SKILL.md) | When to activate; how to register a new Outlook account |
