@littlebearapps/outlook-assistant 3.11.1 → 3.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +12 -0
- package/README.md +46 -28
- package/advanced/index.js +239 -10
- package/auth/device-code.js +100 -3
- package/auth/token-storage.js +44 -2
- package/auth/tools.js +196 -14
- package/calendar/index.js +19 -1
- package/calendar/list.js +154 -2
- package/categories/index.js +17 -3
- package/config.js +73 -17
- package/email/attachments.js +79 -5
- package/email/conversations.js +29 -15
- package/email/delta.js +94 -4
- package/email/export.js +164 -54
- package/email/folder-utils.js +29 -6
- package/email/headers.js +5 -1
- package/email/index.js +68 -13
- package/email/list.js +8 -1
- package/email/mark-as-read.js +3 -1
- package/email/mime.js +4 -1
- package/email/read.js +5 -1
- package/email/search.js +14 -5
- package/folder/create.js +11 -4
- package/folder/delete.js +9 -1
- package/folder/index.js +11 -1
- package/folder/list.js +61 -27
- package/folder/move.js +32 -7
- package/folder/resolve.js +62 -23
- package/folder/stats.js +11 -5
- package/llms-install.md +28 -9
- package/llms.txt +12 -8
- package/package.json +5 -5
- package/utils/graph-api.js +109 -3
- package/utils/mailbox.js +77 -0
- package/utils/response-formatter.js +44 -10
package/.env.example
CHANGED
|
@@ -46,6 +46,18 @@ USE_TEST_MODE=false
|
|
|
46
46
|
# Example: OUTLOOK_AUTH_AUDIENCE=consumers (for personal-account-only apps)
|
|
47
47
|
# OUTLOOK_AUTH_AUDIENCE=common
|
|
48
48
|
|
|
49
|
+
# Optional: Shared-mailbox support (OPT-IN, work/school accounts only).
|
|
50
|
+
# Unset = off: sign-in requests the standard scopes only, exactly as before.
|
|
51
|
+
# read — adds Mail.Read.Shared (read/search/export shared mailboxes)
|
|
52
|
+
# true|readwrite — adds Mail.Read.Shared + Mail.ReadWrite.Shared (also move,
|
|
53
|
+
# flag, categorise, mark read, manage folders)
|
|
54
|
+
# After enabling it, restart the server and re-authenticate:
|
|
55
|
+
# auth action=authenticate force=true
|
|
56
|
+
# Personal Outlook.com accounts can't be granted these scopes — leave it unset.
|
|
57
|
+
# The browser flow (npm run auth-server) has no fallback for accounts that
|
|
58
|
+
# reject them; use device-code auth if you enable this.
|
|
59
|
+
# OUTLOOK_SHARED_MAILBOX=read
|
|
60
|
+
|
|
49
61
|
# Optional: Default timezone for calendar events when not explicitly
|
|
50
62
|
# specified by the caller. Use any IANA timezone identifier.
|
|
51
63
|
# Default: Australia/Melbourne
|
package/README.md
CHANGED
|
@@ -36,14 +36,14 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
36
36
|
- 📨 **Search and read emails** — find messages by sender, subject, date, or keywords; read full threads with conversation grouping; batch flag, move, export, or categorise multiple emails at once
|
|
37
37
|
- 🛡️ **Send emails with safety controls** — dry-run preview, pre-send mail tips (out-of-office, mailbox full, delivery restrictions), session rate limiting, and recipient allowlist to prevent mistakes
|
|
38
38
|
- ✏️ **Draft emails for review** — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
|
|
39
|
-
- 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel
|
|
39
|
+
- 📅 **Manage your calendar** — view upcoming events, look back at past ones or find them by date range and subject, schedule meetings with attendees, update, decline or cancel events
|
|
40
40
|
- 📦 **Export emails** — save individual messages to Markdown, EML, JSON, or CSV; export full conversation threads to MBOX or HTML; bulk-export search results in one call
|
|
41
41
|
- 🔍 **Investigate email headers** — full raw header access (DKIM, SPF, DMARC, delivery chain, X-Mailer, X-Originating-IP) for phishing investigation and compliance review
|
|
42
42
|
- 🗂️ **Organise your inbox** — create nested folders (addressable by path), set up inbox rules, colour-code with categories, manage Focused Inbox — all work together for complete inbox automation
|
|
43
43
|
- 🔄 **Track inbox changes** — delta sync detects new, modified, and deleted emails since your last check, with tokens for incremental polling
|
|
44
44
|
- 👥 **Manage contacts** — search your contact book and organisational directory, create and update contact records
|
|
45
45
|
- ⚙️ **Configure settings** — set out-of-office auto-replies, working hours, and time zone
|
|
46
|
-
- 📬 **Access shared mailboxes** — read team inboxes and service accounts (Microsoft 365)
|
|
46
|
+
- 📬 **Access shared mailboxes** — read and organise team inboxes and service accounts, including custom subfolders and nested folder paths; enumerate, read, and search the folder tree (best-effort — listings flag any branches skipped due to depth limits or per-folder errors), move/flag/categorise messages, and manage folders (work/school Microsoft 365 accounts; opt-in via `OUTLOOK_SHARED_MAILBOX`). Sending, drafts, replies, and forwards from a shared mailbox are not supported — those operations always act on the signed-in user's own mailbox
|
|
47
47
|
- 🏢 **Find meeting rooms** — search by building, floor, capacity, AV equipment, and wheelchair accessibility (Microsoft 365)
|
|
48
48
|
|
|
49
49
|
### Why Outlook Assistant?
|
|
@@ -62,15 +62,15 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
62
62
|
|
|
63
63
|
| Module | Tools | What You Can Do |
|
|
64
64
|
|--------|------:|-----------------|
|
|
65
|
-
| **Email** | 8 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `draft` (create/update/send/delete/reply/forward), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
|
|
66
|
-
| **Calendar** | 3 | `list-events
|
|
65
|
+
| **Email** | 8 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `draft` (create/update/send/delete/reply/reply-all/forward), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
|
|
66
|
+
| **Calendar** | 3 | `list-events` (upcoming by default; `startAfter`/`startBefore`/`subject` filters), `create-event`, `manage-event` (update/decline/cancel/delete) |
|
|
67
67
|
| **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
|
|
68
68
|
| **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
|
|
69
69
|
| **Settings** | 1 | `mailbox-settings` (get/set auto-replies/set working hours) |
|
|
70
70
|
| **Folder** | 1 | `folders` (list/create/move/stats/delete) — nested folders addressable by path (`Parent/Child`) or ID |
|
|
71
71
|
| **Rules** | 1 | `manage-rules` (list/create/update/reorder/delete) |
|
|
72
|
-
| **Advanced** | 2 | `access-shared-mailbox
|
|
73
|
-
| **Auth** | 1 | `auth` (status/authenticate/about) |
|
|
72
|
+
| **Advanced** | 2 | `access-shared-mailbox` (messages or folder tree), `find-meeting-rooms` |
|
|
73
|
+
| **Auth** | 1 | `auth` (status/authenticate/device-code-complete/about) |
|
|
74
74
|
|
|
75
75
|
**22 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
|
|
76
76
|
|
|
@@ -104,7 +104,7 @@ Outlook Assistant works with both personal and work/school Microsoft accounts, b
|
|
|
104
104
|
| Categories | Full support | Full support |
|
|
105
105
|
| Mailbox settings | Full support | Full support |
|
|
106
106
|
| Focused Inbox | API works (overrides stored) but mail routing not affected | Full support |
|
|
107
|
-
| Shared mailboxes | Not available |
|
|
107
|
+
| Shared mailboxes | Not available | Opt-in (`OUTLOOK_SHARED_MAILBOX`). Read + organise only. Read: `Mail.Read.Shared`; organise (move/categorise/flag/create folders): `Mail.ReadWrite.Shared`. No sending/drafts/replies/forwards |
|
|
108
108
|
| Meeting room search | Not available | Requires `Place.Read.All` + admin consent |
|
|
109
109
|
|
|
110
110
|
> **Note**: On personal accounts, Microsoft's `$search` API has limited support for free-text queries. Outlook Assistant handles this automatically with progressive search — if your query returns no results, it falls back through OData filters, boolean filters, and recent message listing to find your emails. For the most direct results on personal accounts, use the structured filter parameters (`from`, `subject`, `to`, `receivedAfter`).
|
|
@@ -141,6 +141,8 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
|
|
|
141
141
|
> }
|
|
142
142
|
> ```
|
|
143
143
|
|
|
144
|
+
**Input and file hardening** — IDs containing `.` or `..` path segments are refused before any request is made, continuation links (`deltaToken`) must point at `graph.microsoft.com`, and attachment downloads and exports write sanitised filenames inside the output directory without overwriting existing files or following symlinks.
|
|
145
|
+
|
|
144
146
|
**Draft protections** — The `draft` tool shares `send-email` safety controls: dry-run preview, recipient allowlist, mail-tips validation, and rate limiting. The `send` action shares the `send-email` rate limit counter, preventing circumvention via the draft-then-send pathway.
|
|
145
147
|
|
|
146
148
|
**Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 22 tools instead of 55 reduces per-turn overhead by ~11,000 tokens (~64%), keeping more of the AI's context window available for your actual conversation. Fewer tools also means the AI selects the right tool more accurately — research shows tool selection degrades beyond ~40 tools.
|
|
@@ -164,7 +166,7 @@ npx @littlebearapps/outlook-assistant
|
|
|
164
166
|
To check which version you have, or to see the available options:
|
|
165
167
|
|
|
166
168
|
```bash
|
|
167
|
-
outlook-assistant --version # prints e.g. 3.
|
|
169
|
+
outlook-assistant --version # prints e.g. 3.12.0
|
|
168
170
|
outlook-assistant --help # usage, options and key environment variables
|
|
169
171
|
```
|
|
170
172
|
|
|
@@ -210,10 +212,13 @@ Add to your MCP client config:
|
|
|
210
212
|
<summary><strong>Claude Code</strong> (CLI)</summary>
|
|
211
213
|
|
|
212
214
|
```bash
|
|
213
|
-
claude mcp add outlook
|
|
215
|
+
claude mcp add outlook \
|
|
216
|
+
-e OUTLOOK_CLIENT_ID=your-application-client-id \
|
|
217
|
+
-e OUTLOOK_CLIENT_SECRET=your-client-secret-VALUE \
|
|
218
|
+
-- npx -y @littlebearapps/outlook-assistant
|
|
214
219
|
```
|
|
215
220
|
|
|
216
|
-
|
|
221
|
+
The MCP server reads its settings from the environment your client passes it; it doesn't load a `.env` file.
|
|
217
222
|
</details>
|
|
218
223
|
|
|
219
224
|
<details>
|
|
@@ -260,18 +265,18 @@ Or add manually to `.cursor/mcp.json`:
|
|
|
260
265
|
|
|
261
266
|
### 4. Authenticate
|
|
262
267
|
|
|
263
|
-
1.
|
|
264
|
-
2.
|
|
265
|
-
3.
|
|
268
|
+
1. Ask your AI assistant to connect to Outlook — it calls the `auth` tool with `action=authenticate` and returns a short code and the URL `microsoft.com/devicelogin`
|
|
269
|
+
2. Open the URL on any device (a private/incognito window avoids cached sessions), enter the code, sign in and grant permissions
|
|
270
|
+
3. Tell your assistant you're done — it calls `auth` with `action=device-code-complete`
|
|
266
271
|
4. Tokens are saved locally and refresh automatically
|
|
267
272
|
|
|
268
|
-
|
|
273
|
+
No auth server is needed for this default device-code flow. If you'd rather use the browser redirect flow, see [Authentication Flow](#authentication-flow) below.
|
|
269
274
|
|
|
270
275
|
## Installation
|
|
271
276
|
|
|
272
277
|
### Prerequisites
|
|
273
278
|
|
|
274
|
-
- **Node.js** 18.
|
|
279
|
+
- **Node.js** 18.18.0 or higher (contributors: the dev tooling needs 22.22.1 or higher)
|
|
275
280
|
- **npm** (included with Node.js)
|
|
276
281
|
- **Azure account** for app registration ([free tier works](https://azure.microsoft.com/free/))
|
|
277
282
|
|
|
@@ -327,7 +332,8 @@ a server that would ignore it.
|
|
|
327
332
|
- `MailboxSettings.ReadWrite` — settings, auto-replies, categories
|
|
328
333
|
- `People.Read` — people search
|
|
329
334
|
3. Optionally add **org-only** permissions (work/school accounts only):
|
|
330
|
-
- `Mail.Read.Shared` — shared mailbox access
|
|
335
|
+
- `Mail.Read.Shared` — shared mailbox read access (requested only when `OUTLOOK_SHARED_MAILBOX=read` or `=true`)
|
|
336
|
+
- `Mail.ReadWrite.Shared` — shared mailbox writes (move/categorise/flag/mark-read; requested only when `OUTLOOK_SHARED_MAILBOX=true`)
|
|
331
337
|
- `Place.Read.All` — meeting room search (requires admin consent)
|
|
332
338
|
4. Click **Add permissions**
|
|
333
339
|
|
|
@@ -342,7 +348,7 @@ a server that would ignore it.
|
|
|
342
348
|
|
|
343
349
|
### Environment Variables
|
|
344
350
|
|
|
345
|
-
|
|
351
|
+
Set these in your MCP client's `"env"` block (see [Quick Start](#3-configure-your-mcp-client)). The MCP server doesn't load `.env` files; the browser-flow auth server (`npm run auth-server`) does, so when running from source you can also keep a `.env` for it:
|
|
346
352
|
|
|
347
353
|
```bash
|
|
348
354
|
cp .env.example .env
|
|
@@ -366,6 +372,7 @@ USE_TEST_MODE=false
|
|
|
366
372
|
| `OUTLOOK_DEFAULT_TIMEZONE` | IANA timezone applied to calendar events when callers don't pass one (e.g. `Europe/London`, `America/New_York`). | `Australia/Melbourne` |
|
|
367
373
|
| `OUTLOOK_MAX_EMAILS_PER_SESSION` | Cap on `send-email` + `draft send` per MCP server lifetime. | unlimited |
|
|
368
374
|
| `OUTLOOK_ALLOWED_RECIPIENTS` | Comma-separated allowlist of domains/addresses for sends, drafts, and rule forwards. | unrestricted |
|
|
375
|
+
| `OUTLOOK_SHARED_MAILBOX` | Opt-in shared-mailbox support (work/school only). `read` requests `Mail.Read.Shared`; `true` (or `readwrite`/`1`) also requests `Mail.ReadWrite.Shared`. Unset leaves sign-in unchanged. After enabling, restart and run `auth action=authenticate force=true`. | unset (off) |
|
|
369
376
|
| `OUTLOOK_SEARCH_SCAN_LIMIT` | How many recent messages the client-side search fallback scans. Personal accounts match `to` locally within this window, so the default caps how far back a `to` search reaches. Max 5000. | `500` |
|
|
370
377
|
|
|
371
378
|
### MCP Client Configuration
|
|
@@ -407,19 +414,27 @@ No auth server needed. Works everywhere, including remote/headless environments.
|
|
|
407
414
|
|
|
408
415
|
### Browser Redirect Flow (Alternative)
|
|
409
416
|
|
|
410
|
-
For localhost development or if you prefer the traditional OAuth flow:
|
|
417
|
+
For localhost development or if you prefer the traditional OAuth flow, start the auth server. From a source checkout:
|
|
411
418
|
|
|
412
419
|
```bash
|
|
413
420
|
npm run auth-server
|
|
414
421
|
```
|
|
415
422
|
|
|
416
|
-
|
|
423
|
+
From a global npm install:
|
|
424
|
+
|
|
425
|
+
```bash
|
|
426
|
+
node "$(npm root -g)/@littlebearapps/outlook-assistant/outlook-auth-server.js"
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
This starts a local server on port 3333 to handle the OAuth callback. (The `outlook-assistant` command itself only accepts `--version` and `--help`; any other argument exits with an error.)
|
|
417
430
|
|
|
418
431
|
1. In your AI assistant, use the `auth` tool with `action=authenticate, method=browser`
|
|
419
432
|
2. Open the provided URL in your browser
|
|
420
433
|
3. Sign in and grant permissions — tokens are saved automatically
|
|
421
434
|
|
|
422
|
-
> **Note**: The auth server reads `OUTLOOK_CLIENT_ID` and `OUTLOOK_CLIENT_SECRET` from environment variables. Your MCP client's `"env"` config only applies to the MCP server process, not a separately-started auth server.
|
|
435
|
+
> **Note**: The auth server reads `OUTLOOK_CLIENT_ID` and `OUTLOOK_CLIENT_SECRET` from environment variables or a `.env` file in the directory you start it from. Your MCP client's `"env"` config only applies to the MCP server process, not a separately-started auth server.
|
|
436
|
+
>
|
|
437
|
+
> **Shared mailboxes**: the browser flow requests the configured scopes with no fallback. If you enable `OUTLOOK_SHARED_MAILBOX`, sign in with the device-code flow.
|
|
423
438
|
|
|
424
439
|
## Directory Structure
|
|
425
440
|
|
|
@@ -429,22 +444,23 @@ outlook-assistant/
|
|
|
429
444
|
├── config.js # Configuration settings
|
|
430
445
|
├── outlook-auth-server.js # OAuth server (port 3333)
|
|
431
446
|
├── auth/ # Authentication module (1 tool)
|
|
432
|
-
├── email/ # Email module (
|
|
447
|
+
├── email/ # Email module (8 tools)
|
|
433
448
|
│ ├── mail-tips.js # Pre-send recipient validation
|
|
434
449
|
│ ├── headers.js # Email header retrieval
|
|
435
450
|
│ ├── mime.js # Raw MIME/EML content
|
|
436
451
|
│ ├── conversations.js # Thread listing/export
|
|
437
452
|
│ ├── attachments.js # Attachment operations
|
|
438
453
|
│ └── ...
|
|
439
|
-
├── calendar/ # Calendar module (3 tools)
|
|
454
|
+
├── calendar/ # Calendar module (3 tools; list.js builds list-events filters)
|
|
440
455
|
├── contacts/ # Contacts module (2 tools)
|
|
441
456
|
├── categories/ # Categories module (3 tools)
|
|
442
457
|
├── settings/ # Settings module (1 tool)
|
|
443
|
-
├── folder/ # Folder module (1 tool)
|
|
458
|
+
├── folder/ # Folder module (1 tool; resolve.js resolves paths/IDs)
|
|
444
459
|
├── rules/ # Rules module (1 tool)
|
|
445
460
|
├── advanced/ # Advanced module (2 tools)
|
|
446
461
|
└── utils/
|
|
447
|
-
├── graph-api.js # Microsoft Graph API client (includes $batch)
|
|
462
|
+
├── graph-api.js # Microsoft Graph API client (includes $batch, path guards)
|
|
463
|
+
├── mailbox.js # me vs users/{sharedMailbox} prefix, shared-mailbox opt-in
|
|
448
464
|
├── safety.js # Rate limiting, recipient allowlist, dry-run
|
|
449
465
|
├── odata-helpers.js # OData query building
|
|
450
466
|
├── field-presets.js # Token-efficient field selections
|
|
@@ -524,8 +540,9 @@ USE_TEST_MODE=true npm start
|
|
|
524
540
|
| [Getting Started](docs/how-to/getting-started/connect-outlook-to-claude.md) | Install, configure, and authenticate — start here |
|
|
525
541
|
| [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
|
|
526
542
|
| [How-To Guides](docs/how-to/index.md) | 29 practical guides for email, calendar, contacts, and settings |
|
|
527
|
-
| [Roadmap](ROADMAP.md) | Active milestones (v3.
|
|
528
|
-
| [Troubleshooting
|
|
543
|
+
| [Roadmap](ROADMAP.md) | Active milestones (v3.12.x, v3.8.x, v3.13.0+) and recent releases |
|
|
544
|
+
| [Troubleshooting](docs/troubleshooting.md) | Known errors and fixes, including auth, search, export and shared mailboxes |
|
|
545
|
+
| [FAQ](docs/faq/faq.md) | Install, accounts, permissions, tokens, updates, uninstall |
|
|
529
546
|
| [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
|
|
530
547
|
| [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
|
|
531
548
|
|
|
@@ -536,9 +553,10 @@ Full documentation: [docs/](docs/README.md)
|
|
|
536
553
|
- **Personal account search**: Free-text `query` and the raw `searchExpression` (formerly `kqlQuery`) rely on Microsoft's `$search` API, which has limited support on personal Outlook.com accounts. `query` mitigates this with progressive fallback (OData filters, boolean filters, then a client-side scan). Field-scoped `$search` (e.g. `subject:"…"`) is rejected outright there; since v3.10.0 `from:`/`to:`/`subject:` expressions are translated into the closest equivalent OData filters and retried, but boolean operators, grouping, wildcards and other field prefixes are not — those still terminate with an explicit no-results rather than a silent broader search. Structured filters (`from`, `subject`, `to`, `receivedAfter`) remain the most direct route. Cross-folder search (`searchAllFolders: true`) returns a superset of inbox-only results. Note that `query` and `searchExpression` are not interchangeable there: `searchExpression` goes to `$search`, which matches the whole message including the body and ranks by relevance rather than date, while `query` falls back to a subject substring match that never reads bodies.
|
|
537
554
|
- **`to` search depth on personal accounts**: the server-side recipient filter is rejected, so `to` is matched locally over the 500 most recent messages (`OUTLOOK_SEARCH_SCAN_LIMIT`, max 5000). On a large archive that excludes older mail — pair `to` with `receivedAfter`/`receivedBefore`. Since v3.11.1 the response says so whenever the scan was truncated, whether or not it matched.
|
|
538
555
|
- **Focused Inbox**: Only available on work/school Microsoft 365 accounts.
|
|
539
|
-
- **Shared mailboxes**: Require `Mail.Read.Shared`
|
|
556
|
+
- **Shared mailboxes**: Require a work/school account and are **opt-in**: set `OUTLOOK_SHARED_MAILBOX=read` (read) or `=true` (read and organise), restart the server, then re-authenticate with `auth action=authenticate force=true`. Until then, `sharedMailbox` calls are refused with setup guidance (`access-shared-mailbox` keeps its previous well-known-folder behaviour). `auth action=about` shows whether the shared scopes were actually granted. Support covers reading and organising only. Reading needs `Mail.Read.Shared`; organising (move/categorise/flag/mark-read/create folders via `sharedMailbox`) needs `Mail.ReadWrite.Shared` — add it in Azure and re-authenticate (until then, shared-scoped writes fail with 403; they never fall back to your own mailbox). Custom subfolders are supported — pass `folder` as a display name or nested path (e.g. `Inbox/Vendors/Acme`), a raw `folderId`, or use `listFolders: true` (or `folders action=list, sharedMailbox: …`) to discover them. **Sending, drafts, replies, and forwards from a shared mailbox are not supported** — `send-email` and `draft` (including reply/reply-all/forward) always act on the signed-in user's own mailbox, and `Mail.Send.Shared` is not requested.
|
|
540
557
|
- **Meeting room search**: Requires `Place.Read.All` permission with admin consent (work/school accounts only).
|
|
541
|
-
- **Export default path**: Exports save to the system temp directory by default. Use `
|
|
558
|
+
- **Export default path**: Exports and attachment downloads save to the system temp directory by default. Use `outputDir` (or `savePath`) to choose a different location.
|
|
559
|
+
- **`list-events` date filters**: `startAfter`/`startBefore` must include `Z` or a ±hh:mm offset; zone-less and date-only values are rejected rather than guessed.
|
|
542
560
|
|
|
543
561
|
## Contributing
|
|
544
562
|
|
package/advanced/index.js
CHANGED
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
const { callGraphAPI } = require('../utils/graph-api');
|
|
12
12
|
const { ensureAuthenticated } = require('../auth');
|
|
13
13
|
const { FIELD_PRESETS } = require('../utils/field-presets');
|
|
14
|
-
const
|
|
14
|
+
const config = require('../config');
|
|
15
|
+
const { DEFAULT_TIMEZONE } = config;
|
|
16
|
+
const {
|
|
17
|
+
buildMailboxPrefix,
|
|
18
|
+
validateMailboxPrefix,
|
|
19
|
+
SHARED_MAILBOX_DISABLED_MESSAGE,
|
|
20
|
+
} = require('../utils/mailbox');
|
|
21
|
+
const { resolveFolder } = require('../folder/resolve');
|
|
22
|
+
const { getAllFoldersHierarchy } = require('../folder/list');
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Format an email for display (simplified)
|
|
@@ -42,13 +50,25 @@ function formatEmail(email, verbosity = 'standard') {
|
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
53
|
+
* Shared-mailbox hint appended to access errors while OUTLOOK_SHARED_MAILBOX
|
|
54
|
+
* is off (the token then carries no `.Shared` scope).
|
|
55
|
+
*/
|
|
56
|
+
const ENABLE_SHARED_HINT =
|
|
57
|
+
'\n\nShared-mailbox scopes are not enabled. Reading a mailbox other than your own normally needs `Mail.Read.Shared`: set OUTLOOK_SHARED_MAILBOX=read (work/school accounts only), restart the server, and run `auth action=authenticate force=true`.';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Access shared mailbox handler.
|
|
61
|
+
*
|
|
62
|
+
* With OUTLOOK_SHARED_MAILBOX off this behaves exactly as before the opt-in
|
|
63
|
+
* flag existed: `folder` (or `folderId`) is used as given — a well-known name
|
|
64
|
+
* or a folder ID — with whatever scopes the token already has. With the flag
|
|
65
|
+
* on, custom/localized names and nested paths are resolved, and
|
|
66
|
+
* `listFolders` is available.
|
|
47
67
|
*/
|
|
48
68
|
async function handleAccessSharedMailbox(args) {
|
|
49
69
|
// F-46: accept `email` as alias for `sharedMailbox`. The original
|
|
50
70
|
// param name is awkward; most callers reach for `email` first.
|
|
51
|
-
const { folder, count, outputVerbosity } = args;
|
|
71
|
+
const { folder, folderId, count, outputVerbosity, listFolders } = args;
|
|
52
72
|
const sharedMailbox = args.sharedMailbox || args.email;
|
|
53
73
|
|
|
54
74
|
if (!sharedMailbox) {
|
|
@@ -62,6 +82,40 @@ async function handleAccessSharedMailbox(args) {
|
|
|
62
82
|
};
|
|
63
83
|
}
|
|
64
84
|
|
|
85
|
+
// Validate up front, through the same helper every other tool uses. The
|
|
86
|
+
// ungated variant keeps this tool's pre-opt-in behaviour when
|
|
87
|
+
// OUTLOOK_SHARED_MAILBOX is off; with it on, buildMailboxPrefix and
|
|
88
|
+
// validateMailboxPrefix agree.
|
|
89
|
+
let mailboxPrefix;
|
|
90
|
+
try {
|
|
91
|
+
mailboxPrefix = validateMailboxPrefix(sharedMailbox);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
return { content: [{ type: 'text', text: error.message }] };
|
|
94
|
+
}
|
|
95
|
+
if (mailboxPrefix === 'me') {
|
|
96
|
+
return {
|
|
97
|
+
content: [
|
|
98
|
+
{
|
|
99
|
+
type: 'text',
|
|
100
|
+
text: 'access-shared-mailbox reads another mailbox — pass its email address. To read your own mailbox, use `search-emails`.',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// listFolders mode: enumerate the shared mailbox's folder tree so callers
|
|
107
|
+
// can discover custom subfolder names/IDs to read from.
|
|
108
|
+
const sharedEnabled = config.SHARED_MAILBOX_MODE !== 'off';
|
|
109
|
+
|
|
110
|
+
if (listFolders) {
|
|
111
|
+
if (!sharedEnabled) {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: 'text', text: SHARED_MAILBOX_DISABLED_MESSAGE }],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return handleListSharedMailboxFolders(sharedMailbox, args);
|
|
117
|
+
}
|
|
118
|
+
|
|
65
119
|
const mailFolder = folder || 'inbox';
|
|
66
120
|
const pageSize = Math.min(count || 25, 50);
|
|
67
121
|
const verbosity = outputVerbosity || 'standard';
|
|
@@ -69,8 +123,45 @@ async function handleAccessSharedMailbox(args) {
|
|
|
69
123
|
try {
|
|
70
124
|
const accessToken = await ensureAuthenticated();
|
|
71
125
|
|
|
126
|
+
// Resolve the requested folder to an ID/well-known segment scoped to the
|
|
127
|
+
// shared mailbox. A raw `folderId` (from `listFolders`) is used as-is;
|
|
128
|
+
// otherwise custom and localized folder names are resolved via the tree.
|
|
129
|
+
let resolvedFolder;
|
|
130
|
+
if (!sharedEnabled) {
|
|
131
|
+
// Pre-opt-in behaviour: no folder resolution.
|
|
132
|
+
resolvedFolder = folderId || mailFolder;
|
|
133
|
+
} else {
|
|
134
|
+
try {
|
|
135
|
+
const resolved = await resolveFolder(accessToken, {
|
|
136
|
+
id: folderId,
|
|
137
|
+
name: mailFolder,
|
|
138
|
+
mailbox: sharedMailbox,
|
|
139
|
+
});
|
|
140
|
+
resolvedFolder = resolved.id;
|
|
141
|
+
} catch (resolveError) {
|
|
142
|
+
// Only resolution failures get the discovery hint — a Graph error
|
|
143
|
+
// (access denied, 5xx) must fall through to the generic handler below
|
|
144
|
+
// rather than masquerading as "folder not found".
|
|
145
|
+
if (!/not found|ambiguous/i.test(resolveError.message)) {
|
|
146
|
+
throw resolveError;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
content: [
|
|
150
|
+
{
|
|
151
|
+
type: 'text',
|
|
152
|
+
text:
|
|
153
|
+
`${resolveError.message}\n\n` +
|
|
154
|
+
`Searched in ${sharedMailbox}. List its folders first to get exact names/IDs:\n` +
|
|
155
|
+
'- `access-shared-mailbox` with `listFolders: true`, or\n' +
|
|
156
|
+
`- \`folders\` tool with \`action: list\`, \`sharedMailbox: "${sharedMailbox}"\``,
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
72
163
|
// Build endpoint for shared mailbox
|
|
73
|
-
const endpoint =
|
|
164
|
+
const endpoint = `${mailboxPrefix}/mailFolders/${resolvedFolder}/messages`;
|
|
74
165
|
const fieldSet = verbosity === 'full' ? 'read' : 'list';
|
|
75
166
|
const queryParams = {
|
|
76
167
|
$top: pageSize.toString(),
|
|
@@ -165,7 +256,7 @@ async function handleAccessSharedMailbox(args) {
|
|
|
165
256
|
content: [
|
|
166
257
|
{
|
|
167
258
|
type: 'text',
|
|
168
|
-
text: `Access denied to shared mailbox "${sharedMailbox}".\n\n**Possible causes:**\n- You don't have access to this shared mailbox\n- The Mail.Read.Shared permission is not granted\n- The shared mailbox address is incorrect`,
|
|
259
|
+
text: `Access denied to shared mailbox "${sharedMailbox}".\n\n**Possible causes:**\n- You don't have access to this shared mailbox\n- The Mail.Read.Shared permission is not granted\n- The shared mailbox address is incorrect${sharedEnabled ? '' : ENABLE_SHARED_HINT}`,
|
|
169
260
|
},
|
|
170
261
|
],
|
|
171
262
|
};
|
|
@@ -193,6 +284,130 @@ async function handleAccessSharedMailbox(args) {
|
|
|
193
284
|
}
|
|
194
285
|
}
|
|
195
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Enumerate a shared mailbox's folder hierarchy.
|
|
289
|
+
*
|
|
290
|
+
* Lists the full folder tree (recursively) of a shared/delegated mailbox so
|
|
291
|
+
* callers can discover custom subfolder names, paths, and IDs — the values
|
|
292
|
+
* needed to read or search those folders.
|
|
293
|
+
* @param {string} sharedMailbox - Shared mailbox email address
|
|
294
|
+
* @param {object} args - Tool arguments (outputVerbosity)
|
|
295
|
+
* @returns {object} - MCP response
|
|
296
|
+
*/
|
|
297
|
+
async function handleListSharedMailboxFolders(sharedMailbox, args) {
|
|
298
|
+
const verbosity = args.outputVerbosity || 'standard';
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const accessToken = await ensureAuthenticated();
|
|
302
|
+
|
|
303
|
+
const { folders, warnings } = await getAllFoldersHierarchy(
|
|
304
|
+
accessToken,
|
|
305
|
+
true,
|
|
306
|
+
sharedMailbox
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
if (!folders || folders.length === 0) {
|
|
310
|
+
return {
|
|
311
|
+
content: [
|
|
312
|
+
{
|
|
313
|
+
type: 'text',
|
|
314
|
+
text: `No folders found in ${sharedMailbox}.\n\nNote: Make sure you have delegate access to this shared mailbox and the Mail.Read.Shared permission is granted.`,
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const output = [];
|
|
321
|
+
output.push(`# Shared Mailbox Folders: ${sharedMailbox}`);
|
|
322
|
+
output.push(
|
|
323
|
+
`**Folders**: ${folders.length}${warnings.length > 0 ? ' (partial)' : ''}\n`
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
folders.forEach((f) => {
|
|
327
|
+
const depth = f.path ? f.path.split('/').length - 1 : 0;
|
|
328
|
+
const indent = ' '.repeat(depth);
|
|
329
|
+
let line = `${indent}- ${f.displayName}`;
|
|
330
|
+
const total = f.totalItemCount || 0;
|
|
331
|
+
const unread = f.unreadItemCount || 0;
|
|
332
|
+
line += ` (${total} items${unread > 0 ? `, ${unread} unread` : ''})`;
|
|
333
|
+
output.push(line);
|
|
334
|
+
if (verbosity === 'full') {
|
|
335
|
+
output.push(`${indent} path: \`${f.path}\``);
|
|
336
|
+
output.push(`${indent} id: \`${f.id}\``);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
if (warnings.length > 0) {
|
|
341
|
+
output.push(
|
|
342
|
+
`\n**Partial listing — ${warnings.length} branch(es) incomplete:**`
|
|
343
|
+
);
|
|
344
|
+
warnings.forEach((w) => output.push(`- ${w}`));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
output.push(
|
|
348
|
+
'\nRead a folder with `access-shared-mailbox` using its name, ' +
|
|
349
|
+
'path (e.g. `Inbox/Subfolder`), or `folderId`.'
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
content: [
|
|
354
|
+
{
|
|
355
|
+
type: 'text',
|
|
356
|
+
text: output.join('\n'),
|
|
357
|
+
},
|
|
358
|
+
],
|
|
359
|
+
_meta: {
|
|
360
|
+
sharedMailbox,
|
|
361
|
+
folderCount: folders.length,
|
|
362
|
+
partial: warnings.length > 0,
|
|
363
|
+
warnings,
|
|
364
|
+
folders: folders.map((f) => ({
|
|
365
|
+
id: f.id,
|
|
366
|
+
displayName: f.displayName,
|
|
367
|
+
folderPath: f.path,
|
|
368
|
+
parentFolderId: f.parentFolderId,
|
|
369
|
+
totalItemCount: f.totalItemCount,
|
|
370
|
+
unreadItemCount: f.unreadItemCount,
|
|
371
|
+
})),
|
|
372
|
+
},
|
|
373
|
+
};
|
|
374
|
+
} catch (error) {
|
|
375
|
+
if (error.message === 'Authentication required') {
|
|
376
|
+
return {
|
|
377
|
+
content: [
|
|
378
|
+
{
|
|
379
|
+
type: 'text',
|
|
380
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
error.message.includes('Access is denied') ||
|
|
388
|
+
error.message.includes('403')
|
|
389
|
+
) {
|
|
390
|
+
return {
|
|
391
|
+
content: [
|
|
392
|
+
{
|
|
393
|
+
type: 'text',
|
|
394
|
+
text: `Access denied to shared mailbox "${sharedMailbox}".\n\n**Possible causes:**\n- You don't have delegate access to this shared mailbox\n- The Mail.Read.Shared permission is not granted\n- The shared mailbox address is incorrect`,
|
|
395
|
+
},
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
content: [
|
|
402
|
+
{
|
|
403
|
+
type: 'text',
|
|
404
|
+
text: `Error listing shared mailbox folders: ${error.message}`,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
196
411
|
/**
|
|
197
412
|
* Set message flag handler
|
|
198
413
|
*/
|
|
@@ -204,6 +419,7 @@ async function handleSetMessageFlag(args) {
|
|
|
204
419
|
startDateTime,
|
|
205
420
|
reminderDateTime: _reminderDateTime,
|
|
206
421
|
} = args;
|
|
422
|
+
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
207
423
|
|
|
208
424
|
// Support single ID or array
|
|
209
425
|
const ids = messageIds || (messageId ? [messageId] : []);
|
|
@@ -263,7 +479,7 @@ async function handleSetMessageFlag(args) {
|
|
|
263
479
|
|
|
264
480
|
for (const id of ids) {
|
|
265
481
|
try {
|
|
266
|
-
await callGraphAPI(accessToken, 'PATCH',
|
|
482
|
+
await callGraphAPI(accessToken, 'PATCH', `${prefix}/messages/${id}`, {
|
|
267
483
|
flag,
|
|
268
484
|
});
|
|
269
485
|
results.push({ id, success: true });
|
|
@@ -333,6 +549,7 @@ async function handleSetMessageFlag(args) {
|
|
|
333
549
|
*/
|
|
334
550
|
async function handleClearMessageFlag(args) {
|
|
335
551
|
const { messageId, messageIds, markComplete } = args;
|
|
552
|
+
const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
|
|
336
553
|
|
|
337
554
|
// Support single ID or array
|
|
338
555
|
const ids = messageIds || (messageId ? [messageId] : []);
|
|
@@ -370,7 +587,7 @@ async function handleClearMessageFlag(args) {
|
|
|
370
587
|
|
|
371
588
|
for (const id of ids) {
|
|
372
589
|
try {
|
|
373
|
-
await callGraphAPI(accessToken, 'PATCH',
|
|
590
|
+
await callGraphAPI(accessToken, 'PATCH', `${prefix}/messages/${id}`, {
|
|
374
591
|
flag,
|
|
375
592
|
});
|
|
376
593
|
results.push({ id, success: true });
|
|
@@ -607,7 +824,7 @@ const advancedTools = [
|
|
|
607
824
|
{
|
|
608
825
|
name: 'access-shared-mailbox',
|
|
609
826
|
description:
|
|
610
|
-
|
|
827
|
+
"List emails — or enumerate folders — from a shared mailbox the signed-in user has been granted access to (read-only). Returns paged messages from the named `sharedMailbox` (or alias `email`) and `folder` (default `inbox`) with id/subject/from/receivedDateTime/preview — same shape as `search-emails` list mode. `folder` accepts a well-known name (inbox, sent, archive…), a custom/localized folder display name (e.g. `Archiv`), a nested folder path (e.g. `Inbox/Vendors/Acme`), or pass a raw `folderId`. Set `listFolders: true` to enumerate the shared mailbox's full folder tree (names, paths, IDs, counts) — use this to discover custom subfolders before reading them. Requires that the shared mailbox has been delegated to the signed-in user in Exchange (admin-configured). Use `outputVerbosity` to control field count and `count` (default 25, max 50) for page size. For full search/filter capability over a shared mailbox, prefer `search-emails` with `sharedMailbox` set. Custom/localized names, nested paths and `listFolders` need the server opt-in setting OUTLOOK_SHARED_MAILBOX (work/school only); without it `folder` must be a well-known name or a folder ID, as before.",
|
|
611
828
|
annotations: {
|
|
612
829
|
title: 'Shared Mailbox',
|
|
613
830
|
readOnlyHint: true,
|
|
@@ -629,7 +846,18 @@ const advancedTools = [
|
|
|
629
846
|
},
|
|
630
847
|
folder: {
|
|
631
848
|
type: 'string',
|
|
632
|
-
description:
|
|
849
|
+
description:
|
|
850
|
+
'Folder to read from (default: inbox). Accepts a well-known name, a custom/localized display name, or a nested path like `Inbox/Subfolder`.',
|
|
851
|
+
},
|
|
852
|
+
folderId: {
|
|
853
|
+
type: 'string',
|
|
854
|
+
description:
|
|
855
|
+
'Exact Graph folder ID to read from (e.g. from `listFolders`). Skips name resolution; takes precedence over `folder`.',
|
|
856
|
+
},
|
|
857
|
+
listFolders: {
|
|
858
|
+
type: 'boolean',
|
|
859
|
+
description:
|
|
860
|
+
"Enumerate the shared mailbox's full folder tree (names, paths, IDs, item counts) instead of reading messages.",
|
|
633
861
|
},
|
|
634
862
|
count: {
|
|
635
863
|
type: 'number',
|
|
@@ -690,6 +918,7 @@ const advancedTools = [
|
|
|
690
918
|
module.exports = {
|
|
691
919
|
advancedTools,
|
|
692
920
|
handleAccessSharedMailbox,
|
|
921
|
+
handleListSharedMailboxFolders,
|
|
693
922
|
handleSetMessageFlag,
|
|
694
923
|
handleClearMessageFlag,
|
|
695
924
|
handleFindMeetingRooms,
|