@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 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 invitations
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`, `create-event`, `manage-event` (update/decline/cancel/delete) |
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`, `find-meeting-rooms` |
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 | Requires `Mail.Read.Shared` |
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.11.1
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 -- npx @littlebearapps/outlook-assistant
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
- Then set environment variables in your `.env` or shell.
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. Start the auth server: `outlook-assistant-auth` (or `npx @littlebearapps/outlook-assistant-auth`)
264
- 2. In your AI assistant, use the `auth` tool with `action=authenticate` to get an OAuth URL
265
- 3. Open the URL, sign in with your Microsoft account, and grant permissions
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
- > **Note**: The auth server needs `OUTLOOK_CLIENT_ID` and `OUTLOOK_CLIENT_SECRET` environment variables. Your MCP client's `"env"` config only applies to the MCP server process — when running the auth server separately, ensure these are set in a `.env` file or exported in your shell.
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.0.0 or higher
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
- Create a `.env` file from the example:
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
- This starts a local server on port 3333 to handle the OAuth callback.
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 (7 tools)
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.11.2, v3.8.x, v3.12.0+) and recent releases |
528
- | [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
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` permission and a work/school account.
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 `savePath` or `outputDir` to specify a different location.
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 { DEFAULT_TIMEZONE } = require('../config');
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
- * Access shared mailbox handler
46
- * Requires Mail.Read.Shared permission
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 = `users/${sharedMailbox}/mailFolders/${mailFolder}/messages`;
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', `me/messages/${id}`, {
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', `me/messages/${id}`, {
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
- 'List emails 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. 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 a folder path scoped to the shared mailbox.',
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: 'Folder to read from (default: inbox)',
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,