@littlebearapps/outlook-assistant 3.4.0 → 3.5.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 +3 -0
- package/README.md +17 -14
- package/advanced/index.js +23 -6
- package/config.js +4 -1
- package/email/export.js +2 -4
- package/email/folder-utils.js +25 -1
- package/email/index.js +43 -0
- package/email/mail-tips.js +211 -0
- package/email/send.js +44 -0
- package/llms.txt +8 -7
- package/package.json +4 -3
- package/settings/index.js +3 -1
- package/utils/graph-api.js +70 -5
package/.env.example
CHANGED
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/outlook-assistant-logo-full.
|
|
2
|
+
<img src="https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/assets/outlook-assistant-logo-full.png" height="200" alt="Outlook Assistant" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<h1 align="center">Outlook Assistant</h1>
|
|
@@ -11,13 +11,9 @@
|
|
|
11
11
|
<p align="center">
|
|
12
12
|
<a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/v/@littlebearapps/outlook-assistant" alt="npm version" /></a>
|
|
13
13
|
<a href="https://www.npmjs.com/package/@littlebearapps/outlook-assistant"><img src="https://img.shields.io/npm/dm/@littlebearapps/outlook-assistant" alt="npm downloads" /></a>
|
|
14
|
-
<a href="https://github.com/littlebearapps/outlook-assistant/stargazers"><img src="https://img.shields.io/github/stars/littlebearapps/outlook-assistant" alt="GitHub stars" /></a>
|
|
15
|
-
<a href="https://github.com/littlebearapps/outlook-assistant/commits/main"><img src="https://img.shields.io/github/last-commit/littlebearapps/outlook-assistant" alt="Last commit" /></a>
|
|
16
14
|
<a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
|
|
17
15
|
<a href="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml"><img src="https://github.com/littlebearapps/outlook-assistant/actions/workflows/codeql.yml/badge.svg" alt="CodeQL" /></a>
|
|
18
|
-
<a href="https://github.com/littlebearapps/outlook-assistant/issues"><img src="https://img.shields.io/github/issues/littlebearapps/outlook-assistant" alt="Open issues" /></a>
|
|
19
16
|
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT" /></a>
|
|
20
|
-
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen" alt="Node.js" /></a>
|
|
21
17
|
</p>
|
|
22
18
|
|
|
23
19
|
Outlook Assistant connects AI assistants to your Microsoft Outlook account through the [Model Context Protocol](https://modelcontextprotocol.io/). Ask your AI assistant to search your inbox, send emails, schedule meetings, manage contacts, and configure mailbox settings — without leaving the conversation. Works with Claude, Cursor, Windsurf, and any MCP-compatible client.
|
|
@@ -37,7 +33,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
37
33
|
### What you can do
|
|
38
34
|
|
|
39
35
|
- 📨 **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
|
|
40
|
-
- 🛡️ **Send emails with safety controls** — dry-run preview, session rate limiting, and recipient allowlist to prevent mistakes
|
|
36
|
+
- 🛡️ **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
|
|
41
37
|
- 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel invitations
|
|
42
38
|
- 📦 **Export emails** — save to Markdown, EML, MBOX, JSON, or HTML for archiving, analysis, or migration; export search results or entire threads in one call
|
|
43
39
|
- 🔍 **Investigate email headers** — check DKIM, SPF, and DMARC authentication; trace delivery chains; analyse spam scores — useful for phishing investigation and compliance
|
|
@@ -64,7 +60,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
64
60
|
|
|
65
61
|
| Module | Tools | What You Can Do |
|
|
66
62
|
|--------|------:|-----------------|
|
|
67
|
-
| **Email** |
|
|
63
|
+
| **Email** | 7 | `search-emails` (list/search/delta/conversations), `read-email` (content + forensic headers), `send-email` (with dry-run + mail tips), `update-email` (read status, flags), `attachments`, `export`, `get-mail-tips` |
|
|
68
64
|
| **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (decline/cancel/delete) |
|
|
69
65
|
| **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
|
|
70
66
|
| **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
|
|
@@ -74,7 +70,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
74
70
|
| **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
|
|
75
71
|
| **Auth** | 1 | `auth` (status/authenticate/about) |
|
|
76
72
|
|
|
77
|
-
**
|
|
73
|
+
**21 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
|
|
78
74
|
|
|
79
75
|
### Export Formats
|
|
80
76
|
|
|
@@ -115,6 +111,7 @@ Outlook Assistant works with both personal and work/school Microsoft accounts, b
|
|
|
115
111
|
- **Email forensics** — full header analysis (DKIM, SPF, DMARC, delivery chain, spam scores) built in as a first-class feature — useful for phishing investigation, compliance, and security review.
|
|
116
112
|
- **Delta sync** — incremental inbox monitoring returns only what changed since your last check, with tokens for continuous polling. Designed for agent workflows that need to watch a mailbox.
|
|
117
113
|
- **Batch operations** — flag, move, export, or categorise multiple emails in a single call. Search-driven export lets you batch-export results without collecting IDs manually.
|
|
114
|
+
- **Pre-send intelligence** — check recipients for out-of-office, full mailbox, delivery restrictions, and moderation status before sending — no other Outlook MCP server offers this.
|
|
118
115
|
- **Compound automation** — rules, categories, folders, and Focused Inbox work together. Set up complete inbox management through your AI assistant in one conversation.
|
|
119
116
|
|
|
120
117
|
## Safety & Token Efficiency
|
|
@@ -124,11 +121,12 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
|
|
|
124
121
|
**Destructive action safeguards** — Every tool carries [MCP annotations](https://modelcontextprotocol.io/docs/concepts/tools#annotations) (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so AI clients can auto-approve safe reads and prompt for confirmation on destructive operations like sending email or deleting events.
|
|
125
122
|
|
|
126
123
|
**Send-email protections** — The `send-email` tool includes:
|
|
124
|
+
- **Pre-send mail tips** (`checkRecipients: true`) — check recipients for out-of-office, mailbox full, delivery restrictions before sending
|
|
127
125
|
- **Dry-run mode** (`dryRun: true`) — preview composed emails without sending
|
|
128
126
|
- **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: unlimited)
|
|
129
127
|
- **Recipient allowlist** — restrict sending to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`
|
|
130
128
|
|
|
131
|
-
**Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach.
|
|
129
|
+
**Token-optimised architecture** — Tools are consolidated using the STRAP (Single Tool, Resource, Action Pattern) approach. 21 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.
|
|
132
130
|
|
|
133
131
|
> **Important**: These safeguards are defence-in-depth measures that reduce risk, but they are not a guarantee against unintended actions. AI-driven access to your email is inherently sensitive — always review tool calls before approving, particularly for sends and deletes. No automated guardrail is foolproof, and you remain responsible for actions taken through your mailbox.
|
|
134
132
|
|
|
@@ -191,6 +189,10 @@ Then set environment variables in your `.env` or shell.
|
|
|
191
189
|
<details>
|
|
192
190
|
<summary><strong>Cursor</strong> (<code>.cursor/mcp.json</code>)</summary>
|
|
193
191
|
|
|
192
|
+
[](cursor://anysphere.cursor-deeplink/mcp/install?name=Outlook%20Assistant&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBsaXR0bGViZWFyYXBwcy9vdXRsb29rLWFzc2lzdGFudCJdLCJlbnYiOnsiT1VUTE9PS19DTElFTlRfSUQiOiIiLCJPVVRMT09LX0NMSUVOVF9TRUNSRVQiOiIifX0=)
|
|
193
|
+
|
|
194
|
+
Or add manually to `.cursor/mcp.json`:
|
|
195
|
+
|
|
194
196
|
```json
|
|
195
197
|
{
|
|
196
198
|
"mcpServers": {
|
|
@@ -359,11 +361,12 @@ This starts a local server on port 3333 to handle the OAuth callback.
|
|
|
359
361
|
|
|
360
362
|
```
|
|
361
363
|
outlook-assistant/
|
|
362
|
-
├── index.js # Main entry point (
|
|
364
|
+
├── index.js # Main entry point (21 tools)
|
|
363
365
|
├── config.js # Configuration settings
|
|
364
366
|
├── outlook-auth-server.js # OAuth server (port 3333)
|
|
365
367
|
├── auth/ # Authentication module (1 tool)
|
|
366
|
-
├── email/ # Email module (
|
|
368
|
+
├── email/ # Email module (7 tools)
|
|
369
|
+
│ ├── mail-tips.js # Pre-send recipient validation
|
|
367
370
|
│ ├── headers.js # Email header retrieval
|
|
368
371
|
│ ├── mime.js # Raw MIME/EML content
|
|
369
372
|
│ ├── conversations.js # Thread listing/export
|
|
@@ -377,7 +380,7 @@ outlook-assistant/
|
|
|
377
380
|
├── rules/ # Rules module (1 tool)
|
|
378
381
|
├── advanced/ # Advanced module (2 tools)
|
|
379
382
|
└── utils/
|
|
380
|
-
├── graph-api.js # Microsoft Graph API client
|
|
383
|
+
├── graph-api.js # Microsoft Graph API client (includes $batch)
|
|
381
384
|
├── safety.js # Rate limiting, recipient allowlist, dry-run
|
|
382
385
|
├── odata-helpers.js # OData query building
|
|
383
386
|
├── field-presets.js # Token-efficient field selections
|
|
@@ -444,9 +447,9 @@ USE_TEST_MODE=true npm start
|
|
|
444
447
|
|-------|-------------|
|
|
445
448
|
| [Getting Started](docs/how-to/getting-started/connect-outlook-to-claude.md) | Install, configure, and authenticate — start here |
|
|
446
449
|
| [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
|
|
447
|
-
| [How-To Guides](docs/how-to/index.md) |
|
|
450
|
+
| [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
|
|
448
451
|
| [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
|
|
449
|
-
| [Tools Reference](docs/quickrefs/tools-reference.md) | All
|
|
452
|
+
| [Tools Reference](docs/quickrefs/tools-reference.md) | All 21 tools with parameters |
|
|
450
453
|
| [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
|
|
451
454
|
|
|
452
455
|
Full documentation: [docs/](docs/README.md)
|
package/advanced/index.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
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
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Format an email for display (simplified)
|
|
@@ -224,16 +225,32 @@ async function handleSetMessageFlag(args) {
|
|
|
224
225
|
};
|
|
225
226
|
|
|
226
227
|
if (dueDateTime) {
|
|
228
|
+
// Graph API expects { dateTime, timeZone } envelope without trailing Z
|
|
229
|
+
// When timeZone is specified, the dateTime value is interpreted in that zone
|
|
230
|
+
const dueDt = dueDateTime.replace(/Z$/i, '');
|
|
227
231
|
flag.dueDateTime = {
|
|
228
|
-
dateTime:
|
|
229
|
-
timeZone:
|
|
232
|
+
dateTime: dueDt,
|
|
233
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
230
234
|
};
|
|
231
|
-
}
|
|
232
235
|
|
|
233
|
-
|
|
236
|
+
// Graph API requires startDateTime when dueDateTime is set
|
|
237
|
+
// Default to start of the same day if not explicitly provided
|
|
238
|
+
if (startDateTime) {
|
|
239
|
+
flag.startDateTime = {
|
|
240
|
+
dateTime: startDateTime.replace(/Z$/i, ''),
|
|
241
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
242
|
+
};
|
|
243
|
+
} else {
|
|
244
|
+
const startOfDay = dueDt.split('T')[0] + 'T09:00:00';
|
|
245
|
+
flag.startDateTime = {
|
|
246
|
+
dateTime: startOfDay,
|
|
247
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
} else if (startDateTime) {
|
|
234
251
|
flag.startDateTime = {
|
|
235
|
-
dateTime:
|
|
236
|
-
timeZone:
|
|
252
|
+
dateTime: startDateTime.replace(/Z$/i, ''),
|
|
253
|
+
timeZone: DEFAULT_TIMEZONE,
|
|
237
254
|
};
|
|
238
255
|
}
|
|
239
256
|
|
package/config.js
CHANGED
|
@@ -26,7 +26,7 @@ if (!homeDir) {
|
|
|
26
26
|
module.exports = {
|
|
27
27
|
// Server information
|
|
28
28
|
SERVER_NAME: 'outlook-assistant',
|
|
29
|
-
SERVER_VERSION: '
|
|
29
|
+
SERVER_VERSION: require('./package.json').version,
|
|
30
30
|
|
|
31
31
|
// Test mode setting
|
|
32
32
|
USE_TEST_MODE: process.env.USE_TEST_MODE === 'true',
|
|
@@ -90,6 +90,9 @@ module.exports = {
|
|
|
90
90
|
// Search defaults (reduced for token efficiency)
|
|
91
91
|
DEFAULT_SEARCH_RESULTS: DEFAULT_LIMITS.searchEmails,
|
|
92
92
|
|
|
93
|
+
// Immutable IDs (opt-in: IDs persist through folder moves)
|
|
94
|
+
USE_IMMUTABLE_IDS: process.env.OUTLOOK_IMMUTABLE_IDS === 'true',
|
|
95
|
+
|
|
93
96
|
// Timezone
|
|
94
97
|
DEFAULT_TIMEZONE: 'Australia/Melbourne', // Updated for Nathan's timezone
|
|
95
98
|
};
|
package/email/export.js
CHANGED
|
@@ -281,10 +281,8 @@ async function handleBatchExportEmails(args) {
|
|
|
281
281
|
}
|
|
282
282
|
|
|
283
283
|
const csvContent = formatEmailsAsCSV(emails);
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
`batch_export_${new Date().toISOString().slice(0, 10)}.csv`
|
|
287
|
-
);
|
|
284
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
285
|
+
const csvPath = path.join(outputDir, `batch_export_${timestamp}.csv`);
|
|
288
286
|
fs.writeFileSync(csvPath, csvContent, 'utf8');
|
|
289
287
|
const totalBytes = Buffer.byteLength(csvContent, 'utf8');
|
|
290
288
|
|
package/email/folder-utils.js
CHANGED
|
@@ -10,15 +10,39 @@ const { callGraphAPI } = require('../utils/graph-api');
|
|
|
10
10
|
const _folderCache = {};
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Well-known folder names and their endpoints
|
|
13
|
+
* Well-known folder names and their endpoints.
|
|
14
|
+
* Includes Graph API well-known names (sentitems, deleteditems, junkemail),
|
|
15
|
+
* common display names (Sent Items, Deleted Items, Junk Email), and
|
|
16
|
+
* short aliases (sent, deleted, junk) for consistent resolution across tools.
|
|
14
17
|
*/
|
|
15
18
|
const WELL_KNOWN_FOLDERS = {
|
|
19
|
+
// Inbox
|
|
16
20
|
inbox: 'me/mailFolders/inbox/messages',
|
|
21
|
+
|
|
22
|
+
// Drafts
|
|
17
23
|
drafts: 'me/mailFolders/drafts/messages',
|
|
24
|
+
|
|
25
|
+
// Sent Items - alias, Graph API name, display name
|
|
18
26
|
sent: 'me/mailFolders/sentItems/messages',
|
|
27
|
+
sentitems: 'me/mailFolders/sentItems/messages',
|
|
28
|
+
'sent items': 'me/mailFolders/sentItems/messages',
|
|
29
|
+
|
|
30
|
+
// Deleted Items - alias, Graph API name, display name
|
|
19
31
|
deleted: 'me/mailFolders/deletedItems/messages',
|
|
32
|
+
deleteditems: 'me/mailFolders/deletedItems/messages',
|
|
33
|
+
'deleted items': 'me/mailFolders/deletedItems/messages',
|
|
34
|
+
|
|
35
|
+
// Junk Email - alias, Graph API name, display name, common alias
|
|
20
36
|
junk: 'me/mailFolders/junkemail/messages',
|
|
37
|
+
junkemail: 'me/mailFolders/junkemail/messages',
|
|
38
|
+
'junk email': 'me/mailFolders/junkemail/messages',
|
|
39
|
+
spam: 'me/mailFolders/junkemail/messages',
|
|
40
|
+
|
|
41
|
+
// Archive
|
|
21
42
|
archive: 'me/mailFolders/archive/messages',
|
|
43
|
+
|
|
44
|
+
// Outbox
|
|
45
|
+
outbox: 'me/mailFolders/outbox/messages',
|
|
22
46
|
};
|
|
23
47
|
|
|
24
48
|
/**
|
package/email/index.js
CHANGED
|
@@ -22,6 +22,7 @@ const {
|
|
|
22
22
|
handleGetConversation,
|
|
23
23
|
handleExportConversation,
|
|
24
24
|
} = require('./conversations');
|
|
25
|
+
const { handleGetMailTips } = require('./mail-tips');
|
|
25
26
|
|
|
26
27
|
// Import flag handlers from advanced module
|
|
27
28
|
const { handleSetMessageFlag, handleClearMessageFlag } = require('../advanced');
|
|
@@ -278,6 +279,11 @@ const emailTools = [
|
|
|
278
279
|
description:
|
|
279
280
|
'Preview email without sending (default: false). Returns composed email for review.',
|
|
280
281
|
},
|
|
282
|
+
checkRecipients: {
|
|
283
|
+
type: 'boolean',
|
|
284
|
+
description:
|
|
285
|
+
'Check recipients for out-of-office, mailbox full, delivery restrictions before sending (default: false). Combine with dryRun=true for pre-send review.',
|
|
286
|
+
},
|
|
281
287
|
},
|
|
282
288
|
required: ['to', 'subject', 'body'],
|
|
283
289
|
},
|
|
@@ -513,6 +519,42 @@ const emailTools = [
|
|
|
513
519
|
}
|
|
514
520
|
},
|
|
515
521
|
},
|
|
522
|
+
{
|
|
523
|
+
name: 'get-mail-tips',
|
|
524
|
+
description:
|
|
525
|
+
'Check recipients before sending: out-of-office status, mailbox full, external recipients, delivery restrictions, moderation, group member counts, and max message size. No competitor offers this.',
|
|
526
|
+
annotations: {
|
|
527
|
+
title: 'Mail Tips',
|
|
528
|
+
readOnlyHint: true,
|
|
529
|
+
openWorldHint: false,
|
|
530
|
+
},
|
|
531
|
+
inputSchema: {
|
|
532
|
+
type: 'object',
|
|
533
|
+
properties: {
|
|
534
|
+
recipients: {
|
|
535
|
+
oneOf: [
|
|
536
|
+
{
|
|
537
|
+
type: 'array',
|
|
538
|
+
items: { type: 'string' },
|
|
539
|
+
description: 'Array of email addresses to check',
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
type: 'string',
|
|
543
|
+
description: 'Comma-separated email addresses to check',
|
|
544
|
+
},
|
|
545
|
+
],
|
|
546
|
+
description: 'Email addresses to check for mail tips',
|
|
547
|
+
},
|
|
548
|
+
tipTypes: {
|
|
549
|
+
type: 'string',
|
|
550
|
+
description:
|
|
551
|
+
'Comma-separated tip types to request (default: all). Options: automaticReplies, mailboxFullStatus, customMailTip, externalMemberCount, totalMemberCount, maxMessageSize, deliveryRestriction, moderationStatus, recipientScope, recipientSuggestions',
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
required: ['recipients'],
|
|
555
|
+
},
|
|
556
|
+
handler: handleGetMailTips,
|
|
557
|
+
},
|
|
516
558
|
];
|
|
517
559
|
|
|
518
560
|
module.exports = {
|
|
@@ -534,4 +576,5 @@ module.exports = {
|
|
|
534
576
|
handleListConversations,
|
|
535
577
|
handleGetConversation,
|
|
536
578
|
handleExportConversation,
|
|
579
|
+
handleGetMailTips,
|
|
537
580
|
};
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mail tips functionality — pre-send validation via Graph API
|
|
3
|
+
*
|
|
4
|
+
* Checks recipients for out-of-office, mailbox full, external,
|
|
5
|
+
* delivery restrictions, and more before sending.
|
|
6
|
+
*/
|
|
7
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
8
|
+
const { ensureAuthenticated } = require('../auth');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* All available mail tip types from Microsoft Graph API
|
|
12
|
+
*/
|
|
13
|
+
const MAIL_TIP_TYPES = [
|
|
14
|
+
'automaticReplies',
|
|
15
|
+
'mailboxFullStatus',
|
|
16
|
+
'customMailTip',
|
|
17
|
+
'externalMemberCount',
|
|
18
|
+
'totalMemberCount',
|
|
19
|
+
'maxMessageSize',
|
|
20
|
+
'deliveryRestriction',
|
|
21
|
+
'moderationStatus',
|
|
22
|
+
'recipientScope',
|
|
23
|
+
'recipientSuggestions',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format mail tips into readable markdown
|
|
28
|
+
* @param {Array} mailTips - Array of mail tip objects from Graph API
|
|
29
|
+
* @returns {string} - Formatted markdown output
|
|
30
|
+
*/
|
|
31
|
+
function formatMailTips(mailTips) {
|
|
32
|
+
const lines = [];
|
|
33
|
+
let warningCount = 0;
|
|
34
|
+
|
|
35
|
+
for (const tip of mailTips) {
|
|
36
|
+
const email = tip.emailAddress?.address || 'Unknown';
|
|
37
|
+
const tipLines = [];
|
|
38
|
+
const warnings = [];
|
|
39
|
+
|
|
40
|
+
// Out-of-office / automatic replies
|
|
41
|
+
if (
|
|
42
|
+
tip.automaticReplies?.message &&
|
|
43
|
+
tip.automaticReplies.message.trim() !== ''
|
|
44
|
+
) {
|
|
45
|
+
warnings.push('Out of Office');
|
|
46
|
+
const reply = tip.automaticReplies;
|
|
47
|
+
tipLines.push(
|
|
48
|
+
` **Out of Office**: ${reply.message.replace(/\n/g, ' ').substring(0, 200)}`
|
|
49
|
+
);
|
|
50
|
+
if (reply.scheduledStartTime || reply.scheduledEndTime) {
|
|
51
|
+
const start = reply.scheduledStartTime?.dateTime || '';
|
|
52
|
+
const end = reply.scheduledEndTime?.dateTime || '';
|
|
53
|
+
if (start || end) {
|
|
54
|
+
tipLines.push(` *Schedule*: ${start} → ${end}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Mailbox full
|
|
60
|
+
if (tip.mailboxFullStatus) {
|
|
61
|
+
warnings.push('Mailbox Full');
|
|
62
|
+
tipLines.push(
|
|
63
|
+
` **Mailbox Full**: Recipient's mailbox is full — delivery may fail`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Custom mail tip (admin-configured)
|
|
68
|
+
if (tip.customMailTip) {
|
|
69
|
+
warnings.push('Custom Tip');
|
|
70
|
+
tipLines.push(` **Notice**: ${tip.customMailTip}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Delivery restriction
|
|
74
|
+
if (tip.deliveryRestriction) {
|
|
75
|
+
const restriction = tip.deliveryRestriction;
|
|
76
|
+
if (restriction.isDeliveryRestricted) {
|
|
77
|
+
warnings.push('Delivery Restricted');
|
|
78
|
+
tipLines.push(
|
|
79
|
+
` **Delivery Restricted**: ${restriction.message || 'Cannot deliver to this recipient'}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Moderation status
|
|
85
|
+
if (tip.moderationStatus && tip.moderationStatus !== 'notModerated') {
|
|
86
|
+
warnings.push('Moderated');
|
|
87
|
+
tipLines.push(
|
|
88
|
+
` **Moderated**: Messages to this recipient require approval`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Recipient scope (external)
|
|
93
|
+
if (tip.recipientScope === 'external') {
|
|
94
|
+
tipLines.push(` **External**: Recipient is outside your organisation`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Max message size
|
|
98
|
+
if (tip.maxMessageSize && tip.maxMessageSize > 0) {
|
|
99
|
+
const sizeMB = (tip.maxMessageSize / (1024 * 1024)).toFixed(1);
|
|
100
|
+
tipLines.push(` *Max message size*: ${sizeMB} MB`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Group member counts
|
|
104
|
+
if (tip.totalMemberCount > 0) {
|
|
105
|
+
tipLines.push(
|
|
106
|
+
` *Group members*: ${tip.totalMemberCount} total (${tip.externalMemberCount || 0} external)`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Build section for this recipient
|
|
111
|
+
const statusIcon = warnings.length > 0 ? '⚠' : '✓';
|
|
112
|
+
lines.push(`### ${statusIcon} ${email}`);
|
|
113
|
+
|
|
114
|
+
if (warnings.length > 0) {
|
|
115
|
+
lines.push(`**Warnings**: ${warnings.join(', ')}`);
|
|
116
|
+
warningCount += warnings.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (tipLines.length > 0) {
|
|
120
|
+
lines.push(tipLines.join('\n'));
|
|
121
|
+
} else {
|
|
122
|
+
lines.push(' No issues detected');
|
|
123
|
+
}
|
|
124
|
+
lines.push('');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { formatted: lines.join('\n'), warningCount };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get mail tips for specified recipients
|
|
132
|
+
* @param {object} args - Tool arguments
|
|
133
|
+
* @returns {object} - MCP response
|
|
134
|
+
*/
|
|
135
|
+
async function handleGetMailTips(args) {
|
|
136
|
+
const { recipients, tipTypes } = args;
|
|
137
|
+
|
|
138
|
+
if (!recipients || recipients.length === 0) {
|
|
139
|
+
return {
|
|
140
|
+
content: [
|
|
141
|
+
{
|
|
142
|
+
type: 'text',
|
|
143
|
+
text: 'At least one recipient email address is required.',
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const accessToken = await ensureAuthenticated();
|
|
151
|
+
|
|
152
|
+
const requestBody = {
|
|
153
|
+
EmailAddresses: Array.isArray(recipients)
|
|
154
|
+
? recipients
|
|
155
|
+
: recipients.split(',').map((e) => e.trim()),
|
|
156
|
+
MailTipsOptions: tipTypes || MAIL_TIP_TYPES.join(','),
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const response = await callGraphAPI(
|
|
160
|
+
accessToken,
|
|
161
|
+
'POST',
|
|
162
|
+
'me/getMailTips',
|
|
163
|
+
requestBody
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const mailTips = response.value || [];
|
|
167
|
+
|
|
168
|
+
if (mailTips.length === 0) {
|
|
169
|
+
return {
|
|
170
|
+
content: [
|
|
171
|
+
{
|
|
172
|
+
type: 'text',
|
|
173
|
+
text: 'No mail tips returned for the specified recipients.',
|
|
174
|
+
},
|
|
175
|
+
],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const { formatted, warningCount } = formatMailTips(mailTips);
|
|
180
|
+
|
|
181
|
+
let header = `# Mail Tips\n\n`;
|
|
182
|
+
header += `**Recipients checked**: ${mailTips.length}\n`;
|
|
183
|
+
header += `**Warnings**: ${warningCount}\n\n`;
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: 'text', text: header + formatted }],
|
|
187
|
+
_meta: {
|
|
188
|
+
recipientCount: mailTips.length,
|
|
189
|
+
warningCount,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
} catch (error) {
|
|
193
|
+
if (error.message === 'Authentication required') {
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: 'text',
|
|
198
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
content: [
|
|
205
|
+
{ type: 'text', text: `Error getting mail tips: ${error.message}` },
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { handleGetMailTips, formatMailTips, MAIL_TIP_TYPES };
|
package/email/send.js
CHANGED
|
@@ -9,6 +9,7 @@ const {
|
|
|
9
9
|
checkRecipientAllowlist,
|
|
10
10
|
formatDryRunPreview,
|
|
11
11
|
} = require('../utils/safety');
|
|
12
|
+
const { handleGetMailTips } = require('./mail-tips');
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* Send email handler
|
|
@@ -25,6 +26,7 @@ async function handleSendEmail(args) {
|
|
|
25
26
|
importance = 'normal',
|
|
26
27
|
saveToSentItems = true,
|
|
27
28
|
dryRun = false,
|
|
29
|
+
checkRecipients = false,
|
|
28
30
|
} = args;
|
|
29
31
|
|
|
30
32
|
// Validate required parameters
|
|
@@ -99,6 +101,48 @@ async function handleSendEmail(args) {
|
|
|
99
101
|
const allowlistError = checkRecipientAllowlist(allRecipients);
|
|
100
102
|
if (allowlistError) return allowlistError;
|
|
101
103
|
|
|
104
|
+
// Pre-send mail tips check
|
|
105
|
+
if (checkRecipients) {
|
|
106
|
+
const allAddresses = allRecipients.map((r) => r.emailAddress.address);
|
|
107
|
+
const tipsResult = await handleGetMailTips({
|
|
108
|
+
recipients: allAddresses,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// If there are warnings, prepend them to the response
|
|
112
|
+
if (tipsResult._meta?.warningCount > 0 && dryRun) {
|
|
113
|
+
// In dry-run mode, include mail tips in the preview
|
|
114
|
+
const tipsText = tipsResult.content[0]?.text || '';
|
|
115
|
+
const preview = formatDryRunPreview({
|
|
116
|
+
message: {
|
|
117
|
+
subject,
|
|
118
|
+
body: {
|
|
119
|
+
contentType:
|
|
120
|
+
/<(html|div|p|h[1-6]|br|table|ul|ol|li|span|a\s|img|strong|em|b|i)\b/i.test(
|
|
121
|
+
body
|
|
122
|
+
)
|
|
123
|
+
? 'html'
|
|
124
|
+
: 'text',
|
|
125
|
+
content: body,
|
|
126
|
+
},
|
|
127
|
+
toRecipients,
|
|
128
|
+
ccRecipients: ccRecipients.length > 0 ? ccRecipients : undefined,
|
|
129
|
+
bccRecipients: bccRecipients.length > 0 ? bccRecipients : undefined,
|
|
130
|
+
importance,
|
|
131
|
+
},
|
|
132
|
+
saveToSentItems,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: 'text',
|
|
138
|
+
text: tipsText + '\n\n---\n\n' + preview.content[0].text,
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
_meta: { mailTips: tipsResult._meta },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
102
146
|
// Prepare email object
|
|
103
147
|
const emailObject = {
|
|
104
148
|
message: {
|
package/llms.txt
CHANGED
|
@@ -11,12 +11,12 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
11
11
|
- **License**: MIT
|
|
12
12
|
- **Node.js**: >= 18.0.0
|
|
13
13
|
- **Authentication**: OAuth 2.0 with Microsoft Graph API (requires Azure app registration)
|
|
14
|
-
- **Tools**:
|
|
14
|
+
- **Tools**: 21 tools across 9 modules (reduced from 55 for optimal AI performance)
|
|
15
15
|
|
|
16
16
|
## Why Outlook Assistant?
|
|
17
17
|
|
|
18
18
|
- Read, search, send, and export emails directly from Claude instead of switching apps
|
|
19
|
-
-
|
|
19
|
+
- 7 email tools covering search, conversations, attachments, bulk export, and pre-send mail tips
|
|
20
20
|
- Manage calendar events, contacts, rules, categories, and mailbox settings in one place
|
|
21
21
|
- Export to multiple formats: MIME/EML, MBOX, Markdown, JSON, HTML
|
|
22
22
|
|
|
@@ -26,13 +26,14 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
26
26
|
- **Email forensics**: Built-in header analysis for DKIM, SPF, DMARC authentication, delivery chains, and spam scores — useful for phishing investigation and compliance
|
|
27
27
|
- **Delta sync**: Incremental inbox monitoring — returns only new, modified, and deleted emails since last check, with tokens for continuous polling
|
|
28
28
|
- **Batch operations**: Flag, move, export, or categorise multiple emails in a single tool call; search-driven export for batch archiving without collecting IDs
|
|
29
|
+
- **Pre-send intelligence**: Check recipients for out-of-office, mailbox full, delivery restrictions, and moderation before sending — no other Outlook MCP server offers this
|
|
29
30
|
- **Compound automation**: Rules + categories + folders + Focused Inbox for complete inbox management in one conversation
|
|
30
31
|
|
|
31
32
|
## Safety & Token Efficiency
|
|
32
33
|
|
|
33
|
-
- **MCP safety annotations** on all
|
|
34
|
-
- **Send-email protections**: dry-run preview, session rate limiting, recipient allowlist
|
|
35
|
-
- **Token-optimised**:
|
|
34
|
+
- **MCP safety annotations** on all 21 tools — AI clients auto-approve reads and prompt for destructive operations
|
|
35
|
+
- **Send-email protections**: pre-send mail tips, dry-run preview, session rate limiting, recipient allowlist
|
|
36
|
+
- **Token-optimised**: 21 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
36
37
|
- These safeguards reduce risk but are not foolproof — always review actions before approving
|
|
37
38
|
|
|
38
39
|
## Quick Start
|
|
@@ -57,7 +58,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
57
58
|
## Tool Categories
|
|
58
59
|
|
|
59
60
|
- **Authentication (1 tool)**: `auth` — OAuth flow, status, about
|
|
60
|
-
- **Email (
|
|
61
|
+
- **Email (7 tools)**: `search-emails`, `read-email`, `send-email`, `update-email`, `attachments`, `export`, `get-mail-tips`
|
|
61
62
|
- **Calendar (3 tools)**: `list-events`, `create-event`, `manage-event`
|
|
62
63
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
63
64
|
- **Folders (1 tool)**: `folders` — list, create, move, stats
|
|
@@ -69,7 +70,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
69
70
|
## Documentation
|
|
70
71
|
|
|
71
72
|
- [README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/README.md): Full documentation including setup, Azure configuration, and usage
|
|
72
|
-
- [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All
|
|
73
|
+
- [Tools Reference](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/docs/quickrefs/tools-reference.md): All 21 tools with parameters and safety annotations
|
|
73
74
|
- [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
|
|
74
75
|
- [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
|
|
75
76
|
- [CHANGELOG](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CHANGELOG.md): Version history
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@littlebearapps/outlook-assistant",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
|
-
"description": "Outlook Assistant — MCP server with
|
|
5
|
+
"description": "Outlook Assistant — MCP server with 21 tools for email, calendar, contacts, and settings via Microsoft Graph API",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"outlook-assistant": "./index.js"
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"lint:fix": "eslint . --fix",
|
|
18
18
|
"format": "prettier --write .",
|
|
19
19
|
"format:check": "prettier --check .",
|
|
20
|
-
"prepare": "husky || true"
|
|
20
|
+
"prepare": "husky || true",
|
|
21
|
+
"version": "node -e \"const s=require('./server.json');const p=require('./package.json');s.version=p.version;s.packages[0].version=p.version;require('fs').writeFileSync('./server.json',JSON.stringify(s,null,2)+'\\n')\" && git add server.json"
|
|
21
22
|
},
|
|
22
23
|
"lint-staged": {
|
|
23
24
|
"*.js": [
|
package/settings/index.js
CHANGED
|
@@ -129,7 +129,9 @@ async function handleGetMailboxSettings(args) {
|
|
|
129
129
|
output.push(`**Locale**: ${settings.locale || 'Not set'}`);
|
|
130
130
|
output.push(`**Display Name**: ${settings.displayName || 'Not set'}`);
|
|
131
131
|
} else if (section === 'timeZone') {
|
|
132
|
-
|
|
132
|
+
// Graph API returns { value: "timezone string" } for scalar properties
|
|
133
|
+
const tz = typeof settings === 'string' ? settings : settings.value;
|
|
134
|
+
output.push(`**Zone**: ${tz || 'Not set'}`);
|
|
133
135
|
} else {
|
|
134
136
|
// Fallback for unknown sections
|
|
135
137
|
output.push('```json');
|
package/utils/graph-api.js
CHANGED
|
@@ -12,6 +12,7 @@ const mockData = require('./mock-data');
|
|
|
12
12
|
* @param {string} path - API endpoint path
|
|
13
13
|
* @param {object} data - Data to send for POST/PUT requests
|
|
14
14
|
* @param {object} queryParams - Query parameters
|
|
15
|
+
* @param {object} extraHeaders - Additional headers (e.g. Prefer for immutable IDs)
|
|
15
16
|
* @returns {Promise<object>} - The API response
|
|
16
17
|
*/
|
|
17
18
|
async function callGraphAPI(
|
|
@@ -19,7 +20,8 @@ async function callGraphAPI(
|
|
|
19
20
|
method,
|
|
20
21
|
path,
|
|
21
22
|
data = null,
|
|
22
|
-
queryParams = {}
|
|
23
|
+
queryParams = {},
|
|
24
|
+
extraHeaders = {}
|
|
23
25
|
) {
|
|
24
26
|
// For test tokens, we'll simulate the API call
|
|
25
27
|
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
@@ -75,12 +77,22 @@ async function callGraphAPI(
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
return new Promise((resolve, reject) => {
|
|
80
|
+
const headers = {
|
|
81
|
+
Authorization: `Bearer ${accessToken}`,
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Add immutable IDs header when enabled globally
|
|
86
|
+
if (config.USE_IMMUTABLE_IDS) {
|
|
87
|
+
headers.Prefer = 'IdType="ImmutableId"';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Merge any extra headers (caller overrides take precedence)
|
|
91
|
+
Object.assign(headers, extraHeaders);
|
|
92
|
+
|
|
78
93
|
const options = {
|
|
79
94
|
method: method,
|
|
80
|
-
headers
|
|
81
|
-
Authorization: `Bearer ${accessToken}`,
|
|
82
|
-
'Content-Type': 'application/json',
|
|
83
|
-
},
|
|
95
|
+
headers,
|
|
84
96
|
};
|
|
85
97
|
|
|
86
98
|
const req = https.request(finalUrl, options, (res) => {
|
|
@@ -202,6 +214,58 @@ async function callGraphAPIPaginated(
|
|
|
202
214
|
}
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Sends multiple Graph API requests in a single batch call ($batch).
|
|
219
|
+
* Supports up to 20 requests per batch (Graph API limit).
|
|
220
|
+
* @param {string} accessToken - The access token for authentication
|
|
221
|
+
* @param {Array<{id: string, method: string, url: string, body?: object, headers?: object}>} requests - Batch requests
|
|
222
|
+
* @returns {Promise<Array<{id: string, status: number, body: object}>>} - Array of responses
|
|
223
|
+
*/
|
|
224
|
+
async function callGraphAPIBatch(accessToken, requests) {
|
|
225
|
+
if (!Array.isArray(requests) || requests.length === 0) {
|
|
226
|
+
throw new Error('Batch requests must be a non-empty array');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (requests.length > 20) {
|
|
230
|
+
throw new Error('Batch requests cannot exceed 20 (Graph API limit)');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Test mode
|
|
234
|
+
if (config.USE_TEST_MODE && accessToken.startsWith('test_access_token_')) {
|
|
235
|
+
return requests.map((req) => ({
|
|
236
|
+
id: req.id,
|
|
237
|
+
status: 200,
|
|
238
|
+
body: mockData.simulateGraphAPIResponse(
|
|
239
|
+
req.method,
|
|
240
|
+
req.url,
|
|
241
|
+
req.body || null,
|
|
242
|
+
{}
|
|
243
|
+
),
|
|
244
|
+
}));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const batchPayload = {
|
|
248
|
+
requests: requests.map((req) => ({
|
|
249
|
+
id: req.id,
|
|
250
|
+
method: req.method,
|
|
251
|
+
url: req.url.startsWith('/') ? req.url : `/${req.url}`,
|
|
252
|
+
...(req.body && { body: req.body }),
|
|
253
|
+
...(req.headers && { headers: req.headers }),
|
|
254
|
+
})),
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const response = await callGraphAPI(
|
|
258
|
+
accessToken,
|
|
259
|
+
'POST',
|
|
260
|
+
'$batch',
|
|
261
|
+
batchPayload
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
return (response.responses || []).sort(
|
|
265
|
+
(a, b) => parseInt(a.id) - parseInt(b.id)
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
205
269
|
/**
|
|
206
270
|
* Calls Graph API to get raw MIME content (for email export)
|
|
207
271
|
* @param {string} accessToken - The access token for authentication
|
|
@@ -264,5 +328,6 @@ async function callGraphAPIRaw(accessToken, emailId) {
|
|
|
264
328
|
module.exports = {
|
|
265
329
|
callGraphAPI,
|
|
266
330
|
callGraphAPIPaginated,
|
|
331
|
+
callGraphAPIBatch,
|
|
267
332
|
callGraphAPIRaw,
|
|
268
333
|
};
|