Skip to content

Commit c2814d9

Browse files
authored
Merge pull request #2 from neilgfoster/feat/msgraph-mail-rules-implement
feat: Outlook mail read + safe message-rule authoring
2 parents 2eab328 + 33a078b commit c2814d9

13 files changed

Lines changed: 829 additions & 260 deletions

File tree

README.md

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ It gives an agent two capabilities, with safety built into the *structure*, not
1010
`messageRule`s, so deterministic mail organisation lives *in Outlook* (runs even when nothing
1111
else is, visible and editable in Outlook's own UI, reversible by deleting one rule).
1212

13+
## Verbs (skills)
14+
15+
Each skill is a thin wrapper over the stdlib kernel; the runtime catalog is the source of truth
16+
(`python3 -m msgraph.client describe`). Names are prefixed `msgraph-` when invoked as slash commands.
17+
18+
| Skill | Scope it needs | What it does |
19+
|---|---|---|
20+
| `auth-login` | `Mail.Read + MailboxSettings.Read` (read, default) or `+ MailboxSettings.ReadWrite` (`--mode rules`) | Device-code sign-in; caches the token at the XDG path and refreshes it silently. **Run first.** |
21+
| `mail-list` | `Mail.Read` | List recent inbox messages (concise/detailed, pagination default 25). |
22+
| `mail-get` | `Mail.Read` | Fetch one message incl. its internet headers (e.g. `List-Unsubscribe`). |
23+
| `rule-list` | `MailboxSettings.Read` | Enumerate existing inbox rules agent-legibly. |
24+
| `rule-verify` | `Mail.Read` | Compute a candidate rule's **read-only catch-set** and record the verification gate. |
25+
| `rule-create` | `MailboxSettings.ReadWrite` | Install a verified move-to-folder rule; refuses unverified criteria. |
26+
| `rule-remove` | `MailboxSettings.ReadWrite` | Delete a rule by id (the reversibility primitive). |
27+
1328
## Why stdlib / zero-backend
1429

1530
The Microsoft Graph + Outlook plugin space is crowded — but it is almost entirely Node MCP servers
@@ -47,15 +62,40 @@ CLAUDE.md # grounding + build plan for a Claude C
4762

4863
## Prerequisite (one-time, free)
4964

50-
An **Azure AD app registration** (public client, device-code flow enabled). Add the delegated
51-
permission `Mail.Read` (and `MailboxSettings.ReadWrite` only if you want rule authoring). No cost,
52-
no admin consent for personal accounts. Set the resulting client/tenant IDs via environment before
53-
first auth — see `CLAUDE.md`.
65+
An **Azure AD app registration** (public client, device-code / public-client flow **enabled**). Add
66+
the delegated permissions `Mail.Read` + `MailboxSettings.Read` (read mail and list rules), plus
67+
`MailboxSettings.ReadWrite` only if you want rule authoring. No cost, no admin consent for personal
68+
accounts. Then export the resulting identifiers before first sign-in (read from the environment,
69+
never hardcoded):
70+
71+
```bash
72+
export MSGRAPH_CLIENT_ID="<application (client) id>"
73+
export MSGRAPH_TENANT_ID="consumers" # or "common" for work/school + personal accounts
74+
```
75+
76+
See `plugin/skills/auth-login/SKILL.md` for the full walkthrough.
77+
78+
## Quick start
79+
80+
```bash
81+
python3 -m msgraph.client describe # discover every verb + schema
82+
python3 -m msgraph.client auth-login # read-only sign-in (device code)
83+
python3 -m msgraph.client mail-list --limit 10 # triage the inbox
84+
python3 -m msgraph.client rule-verify --header_contains "List-Unsubscribe" # preview, read-only
85+
python3 -m msgraph.client auth-login --mode rules # escalate (separate consent)
86+
python3 -m msgraph.client rule-create --name "Newsletters" \
87+
--header_contains "List-Unsubscribe" --move_to_folder "Newsletters"
88+
```
89+
90+
(Run from `plugin/src/`, or set `PYTHONPATH=plugin/src`. Inside an installed plugin the skills use
91+
`${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py`.)
5492

5593
## Status
5694

57-
Scaffolded; skills built spec-first (`/speckit-specify``clarify``plan``tasks`
58-
`implement`). See `DEFINITION_OF_DONE.md` for the target and `CLAUDE.md` for the build plan.
95+
v0.1 implemented spec-first (`/speckit-specify``clarify``plan``tasks``implement`): the
96+
seven verbs above, the runtime `describe` catalog, and offline unit tests (Graph HTTP boundary
97+
mocked). Live auth/Graph behaviour requires the one-time Azure app registration above. See
98+
`DEFINITION_OF_DONE.md` for the target and `CLAUDE.md` for the build plan.
5999

60100
## License
61101

plugin/hooks/hooks.json

Lines changed: 0 additions & 18 deletions
This file was deleted.

plugin/skills/auth-login/SKILL.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
---
2+
name: "auth-login"
3+
description: "Sign in to Microsoft Graph via the OAuth device-code flow so the other msgraph verbs can reach the mailbox. Run this FIRST, before any mail-* or rule-* command. Default mode is read-only (Mail.Read + MailboxSettings.Read): read mail and list existing rules, with NO capability to change anything. Pass --mode rules only when you need to author rules — it consents to MailboxSettings.ReadWrite as a separate, deliberate escalation. Caches the token outside the repo (0600) and refreshes it silently. Use when a command reports 'not signed in' or 'escalate'. Requires MSGRAPH_CLIENT_ID / MSGRAPH_TENANT_ID in the environment (one-time Azure app registration; see below)."
4+
argument-hint: "[--mode read|rules] (default read)"
5+
user-invocable: true
6+
disable-model-invocation: false
7+
annotations:
8+
readOnlyHint: false # writes the local token cache (not the mailbox)
9+
destructiveHint: false
10+
idempotentHint: false # each login mints a fresh token
11+
openWorldHint: true # talks to the Microsoft identity platform
12+
---
13+
14+
## What this does
15+
16+
Performs the OAuth 2.0 **device authorization grant** by hand (stdlib `urllib` only — no `msal`, no
17+
SDK): it prints a short user code + a `microsoft.com/devicelogin` URL, you authorise in a browser,
18+
and the kernel polls until consent and caches the resulting token at
19+
`${XDG_STATE_HOME:-~/.local/state}/msgraph-stdlib/token.json` (`0600`, outside the repo, never
20+
committed). The refresh token (`offline_access`) is used to renew silently on later calls.
21+
22+
**The two modes are the safety ratchet — choose the smallest that fits the task:**
23+
24+
| Mode | Scopes granted | Lets you | Cannot |
25+
|---|---|---|---|
26+
| `read` (default) | `Mail.Read` + `MailboxSettings.Read` | `mail-list`, `mail-get`, `rule-list`, `rule-verify` | create/remove rules, mutate any mail |
27+
| `rules` | `Mail.Read` + `MailboxSettings.ReadWrite` | the above **plus** `rule-create`, `rule-remove` | mutate individual messages (no verb does) |
28+
29+
A read-only token *structurally* carries no write grant, so even a bug cannot change the mailbox.
30+
Escalating to `--mode rules` is a separate browser consent — the OAuth grant is the audit record.
31+
Stay in `read` until you actually need to install or remove a rule.
32+
33+
## One-time prerequisite (free, human)
34+
35+
Register a free **Azure AD app** (public client, device-code/public-client flow **enabled**) with
36+
delegated permissions `Mail.Read` + `MailboxSettings.Read` (+ `MailboxSettings.ReadWrite` for rule
37+
authoring). Personal Microsoft accounts need no admin consent. Then export, before first sign-in:
38+
39+
```bash
40+
export MSGRAPH_CLIENT_ID="<application (client) id>"
41+
export MSGRAPH_TENANT_ID="consumers" # or "common" for work/school + personal
42+
```
43+
44+
These are read from the environment and never hardcoded. If `MSGRAPH_CLIENT_ID` is unset, the verb
45+
returns a steering error explaining this.
46+
47+
## Discoverability
48+
49+
The kernel's `TOOLS` catalog is the single source of truth for every verb's arguments — don't rely
50+
on this doc staying in sync, ask at runtime:
51+
52+
```bash
53+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe # all verbs
54+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name auth-login # this verb
55+
```
56+
57+
## How it runs
58+
59+
```bash
60+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" auth-login # read-only (default)
61+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" auth-login --mode rules # escalate to write
62+
# or, as a module: python3 -m msgraph.client auth-login [--mode rules]
63+
```
64+
65+
## Output
66+
67+
The device code + verification URL are printed to stderr; on success a one-line confirmation names
68+
the mode and granted scopes. The access token itself is never printed.
69+
70+
## Errors steer the agent
71+
72+
```
73+
error: MSGRAPH_CLIENT_ID is not set. Register a free Azure AD public client … then export
74+
MSGRAPH_CLIENT_ID and MSGRAPH_TENANT_ID.
75+
error: Device-code sign-in timed out before authorisation. Run /msgraph-auth-login again.
76+
```

plugin/skills/example-subject-verb/SKILL.md

Lines changed: 0 additions & 85 deletions
This file was deleted.

plugin/skills/mail-get/SKILL.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
name: "mail-get"
3+
description: "Fetch ONE Outlook message by id, including its full internet headers. Read-only (Mail.Read). Use when you need a single message's content or — crucially — its raw headers, e.g. to inspect List-Unsubscribe, Sender, or List-Id before proposing a header-based rule with rule-verify/rule-create. Get the message_id from mail-list --format detailed. concise (default) prints subject/sender/received plus the header list; detailed returns the full JSON. Requires a prior read sign-in (run /msgraph-auth-login)."
4+
argument-hint: "--message_id <id> [--format concise|detailed]"
5+
user-invocable: true
6+
disable-model-invocation: false
7+
annotations:
8+
readOnlyHint: true
9+
destructiveHint: false
10+
idempotentHint: true
11+
openWorldHint: true
12+
---
13+
14+
## What this does
15+
16+
Fetches a single message via `GET /me/messages/{id}` with `internetMessageHeaders` selected, so you
17+
can read the raw headers Outlook rules match against. Read-only (`Mail.Read`) — reaches no write
18+
endpoint.
19+
20+
This is the natural precursor to **rule-verify**: inspect a representative message's headers here to
21+
choose the right `header_contains` substrings, then verify the catch-set read-only before installing
22+
anything.
23+
24+
## Discoverability
25+
26+
```bash
27+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name mail-get
28+
```
29+
30+
## How it runs
31+
32+
```bash
33+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" mail-get --message_id <id> --format concise
34+
# or: python3 -m msgraph.client mail-get --message_id <id> --format detailed
35+
```
36+
37+
Get `<id>` from `mail-list --format detailed`.
38+
39+
## Output (agent-legible)
40+
41+
```
42+
Subject: Weekly Newsletter
43+
From: news@example.com
44+
Received: 2026-06-19T08:01:00Z
45+
46+
Internet headers (12):
47+
List-Unsubscribe: <mailto:unsubscribe@example.com>
48+
List-Id: Example Newsletter <news.example.com>
49+
...
50+
```
51+
52+
`--format detailed` returns the full message JSON.
53+
54+
## Errors steer the agent
55+
56+
```
57+
error: not signed in — run /msgraph-auth-login first, then retry.
58+
```

plugin/skills/mail-list/SKILL.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
name: "mail-list"
3+
description: "List recent inbox messages from Outlook, agent-legibly, for triage or to find a message to inspect. Read-only (Mail.Read). Use when you need an overview of what's in the inbox — e.g. before proposing a rule, or to locate a message id for mail-get. concise (default) returns readable summaries (subject, sender, received time); detailed adds Graph ids needed for follow-up calls. Bounded by --limit (default 25) so it never dumps the whole mailbox. Requires a prior read sign-in (run /msgraph-auth-login)."
4+
argument-hint: "[--limit N] [--format concise|detailed]"
5+
user-invocable: true
6+
disable-model-invocation: false
7+
annotations:
8+
readOnlyHint: true
9+
destructiveHint: false
10+
idempotentHint: true
11+
openWorldHint: true
12+
---
13+
14+
## What this does
15+
16+
Fetches recent inbox messages via `GET /me/messages` (newest first), resolving the sender to a
17+
readable address and shaping the output for an agent. It reads only — it holds `Mail.Read` and
18+
reaches no write endpoint, so it cannot move, archive, or delete anything.
19+
20+
Reach for this for **triage/overview**. When you need one message's full content or its internet
21+
headers (e.g. to inspect `List-Unsubscribe` before proposing a rule), follow up with **mail-get**
22+
using an id from `--format detailed`.
23+
24+
## Discoverability
25+
26+
The kernel's `TOOLS` catalog is the source of truth for arguments — ask at runtime rather than
27+
trusting this doc:
28+
29+
```bash
30+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" describe --name mail-list
31+
```
32+
33+
## How it runs
34+
35+
```bash
36+
python3 "${CLAUDE_PLUGIN_ROOT}/src/msgraph/client.py" mail-list --limit 10 --format concise
37+
# or: python3 -m msgraph.client mail-list ...
38+
```
39+
40+
## Output (agent-legible)
41+
42+
`concise` returns readable summaries; pass `--format detailed` only when you need ids for a
43+
follow-up call (`mail-get`):
44+
45+
```
46+
- "Weekly Newsletter" from news@example.com (received: 2026-06-19T08:01:00Z)
47+
- "Receipt #4471" from billing@example.com (received: 2026-06-18T22:14:00Z)
48+
2 message(s). Pass --format detailed for IDs needed by follow-up commands.
49+
```
50+
51+
## Errors steer the agent
52+
53+
```
54+
error: not signed in — run /msgraph-auth-login first, then retry.
55+
```

0 commit comments

Comments
 (0)