@littlebearapps/outlook-assistant 3.5.1 → 3.6.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/README.md +13 -9
- package/auth/index.js +2 -1
- package/auth/tools.js +8 -1
- package/categories/index.js +26 -5
- package/email/conversations.js +59 -1
- package/email/draft.js +484 -0
- package/email/index.js +80 -0
- package/email/mail-tips.js +36 -3
- package/email/search.js +132 -59
- package/email/send.js +32 -43
- package/index.js +4 -1
- package/llms.txt +6 -6
- package/package.json +2 -2
- package/utils/field-presets.js +20 -0
- package/utils/graph-api.js +8 -0
package/README.md
CHANGED
|
@@ -34,6 +34,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
34
34
|
|
|
35
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
|
|
36
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
|
|
37
|
+
- ✏️ **Draft emails for review** — create, update, and send drafts; reply and forward as drafts; preview before saving with dry-run mode
|
|
37
38
|
- 📅 **Manage your calendar** — view upcoming events, schedule meetings with attendees, decline or cancel invitations
|
|
38
39
|
- 📦 **Export emails** — save to Markdown, EML, MBOX, JSON, or HTML for archiving, analysis, or migration; export search results or entire threads in one call
|
|
39
40
|
- 🔍 **Investigate email headers** — check DKIM, SPF, and DMARC authentication; trace delivery chains; analyse spam scores — useful for phishing investigation and compliance
|
|
@@ -60,7 +61,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
60
61
|
|
|
61
62
|
| Module | Tools | What You Can Do |
|
|
62
63
|
|--------|------:|-----------------|
|
|
63
|
-
| **Email** |
|
|
64
|
+
| **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` |
|
|
64
65
|
| **Calendar** | 3 | `list-events`, `create-event`, `manage-event` (decline/cancel/delete) |
|
|
65
66
|
| **Contacts** | 2 | `manage-contact` (list/search/get/create/update/delete), `search-people` |
|
|
66
67
|
| **Categories** | 3 | `manage-category` (CRUD), `apply-category`, `manage-focused-inbox` |
|
|
@@ -70,7 +71,7 @@ Outlook Assistant connects AI assistants to your Microsoft Outlook account throu
|
|
|
70
71
|
| **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
|
|
71
72
|
| **Auth** | 1 | `auth` (status/authenticate/about) |
|
|
72
73
|
|
|
73
|
-
**
|
|
74
|
+
**22 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
|
|
74
75
|
|
|
75
76
|
### Export Formats
|
|
76
77
|
|
|
@@ -126,7 +127,9 @@ Outlook Assistant is designed with safety-first principles for AI-driven email a
|
|
|
126
127
|
- **Session rate limiting** — configurable via `OUTLOOK_MAX_EMAILS_PER_SESSION` (default: unlimited)
|
|
127
128
|
- **Recipient allowlist** — restrict sending to approved addresses/domains via `OUTLOOK_ALLOWED_RECIPIENTS`
|
|
128
129
|
|
|
129
|
-
**
|
|
130
|
+
**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.
|
|
131
|
+
|
|
132
|
+
**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.
|
|
130
133
|
|
|
131
134
|
> **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.
|
|
132
135
|
|
|
@@ -149,10 +152,11 @@ npx @littlebearapps/outlook-assistant
|
|
|
149
152
|
You need a Microsoft Azure app registration to authenticate. See the **[Azure Setup Guide](docs/guides/azure-setup.md)** for a detailed walkthrough (including first-time Azure account creation), or if you've done this before:
|
|
150
153
|
|
|
151
154
|
1. Create a new app registration at [portal.azure.com](https://portal.azure.com/)
|
|
152
|
-
2.
|
|
153
|
-
3.
|
|
154
|
-
4.
|
|
155
|
-
5. Enable **"Allow public client flows"** in Authentication > Advanced settings
|
|
155
|
+
2. Add Microsoft Graph delegated permissions (Mail, Calendar, Contacts)
|
|
156
|
+
3. Create a client secret and copy the **Value** (not the Secret ID)
|
|
157
|
+
4. Under Authentication > **Add a platform** > **Mobile and desktop applications** — check `nativeclient` URI
|
|
158
|
+
5. Enable **"Allow public client flows"** in Authentication > Advanced settings
|
|
159
|
+
6. _(Optional)_ Set redirect URI to `http://localhost:3333/auth/callback` — only needed for browser auth flow
|
|
156
160
|
|
|
157
161
|
### 3. Configure Your MCP Client
|
|
158
162
|
|
|
@@ -373,7 +377,7 @@ This starts a local server on port 3333 to handle the OAuth callback.
|
|
|
373
377
|
|
|
374
378
|
```
|
|
375
379
|
outlook-assistant/
|
|
376
|
-
├── index.js # Main entry point (
|
|
380
|
+
├── index.js # Main entry point (22 tools)
|
|
377
381
|
├── config.js # Configuration settings
|
|
378
382
|
├── outlook-auth-server.js # OAuth server (port 3333)
|
|
379
383
|
├── auth/ # Authentication module (1 tool)
|
|
@@ -465,7 +469,7 @@ USE_TEST_MODE=true npm start
|
|
|
465
469
|
| [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
|
|
466
470
|
| [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
|
|
467
471
|
| [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
|
|
468
|
-
| [Tools Reference](docs/quickrefs/tools-reference.md) | All
|
|
472
|
+
| [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
|
|
469
473
|
| [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
|
|
470
474
|
|
|
471
475
|
Full documentation: [docs/](docs/README.md)
|
package/auth/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
const tokenManager = require('./token-manager');
|
|
5
5
|
const TokenStorage = require('./token-storage');
|
|
6
6
|
const config = require('../config');
|
|
7
|
-
const { authTools } = require('./tools');
|
|
7
|
+
const { authTools, setToolCount } = require('./tools');
|
|
8
8
|
|
|
9
9
|
// Singleton TokenStorage instance with auto-refresh support
|
|
10
10
|
const tokenStorage = new TokenStorage({
|
|
@@ -39,5 +39,6 @@ module.exports = {
|
|
|
39
39
|
tokenManager, // deprecated: use tokenStorage
|
|
40
40
|
tokenStorage,
|
|
41
41
|
authTools,
|
|
42
|
+
setToolCount,
|
|
42
43
|
ensureAuthenticated,
|
|
43
44
|
};
|
package/auth/tools.js
CHANGED
|
@@ -5,6 +5,12 @@ const config = require('../config');
|
|
|
5
5
|
const tokenManager = require('./token-manager');
|
|
6
6
|
const { initiateDeviceCodeFlow, pollForToken } = require('./device-code');
|
|
7
7
|
|
|
8
|
+
// Dynamic tool count — set by index.js after TOOLS array is built
|
|
9
|
+
let _toolCount = 0;
|
|
10
|
+
function setToolCount(count) {
|
|
11
|
+
_toolCount = count;
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
/**
|
|
9
15
|
* About tool handler
|
|
10
16
|
* @returns {object} - MCP response
|
|
@@ -25,7 +31,7 @@ async function handleAbout() {
|
|
|
25
31
|
`## Diagnostics\n`,
|
|
26
32
|
`| Setting | Value |`,
|
|
27
33
|
`|---------|-------|`,
|
|
28
|
-
`| Tools |
|
|
34
|
+
`| Tools | ${_toolCount} across 9 modules |`,
|
|
29
35
|
`| Modules | auth, email, calendar, folder, rules, contacts, categories, settings, advanced |`,
|
|
30
36
|
`| Timezone | ${config.DEFAULT_TIMEZONE} |`,
|
|
31
37
|
`| Test Mode | ${testMode} |`,
|
|
@@ -320,6 +326,7 @@ const authTools = [
|
|
|
320
326
|
|
|
321
327
|
module.exports = {
|
|
322
328
|
authTools,
|
|
329
|
+
setToolCount,
|
|
323
330
|
handleAbout,
|
|
324
331
|
handleAuthenticate,
|
|
325
332
|
handleDeviceCodeAuth,
|
package/categories/index.js
CHANGED
|
@@ -305,17 +305,26 @@ async function handleUpdateCategory(args) {
|
|
|
305
305
|
updateData
|
|
306
306
|
);
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
// Prefer input values over response (PATCH may return partial data)
|
|
309
|
+
const updatedName = displayName || response.displayName;
|
|
310
|
+
const updatedColor = color || response.color;
|
|
311
|
+
const colorName = COLOR_NAMES[updatedColor] || updatedColor;
|
|
312
|
+
|
|
313
|
+
const updatedCategory = {
|
|
314
|
+
...response,
|
|
315
|
+
displayName: updatedName,
|
|
316
|
+
color: updatedColor,
|
|
317
|
+
};
|
|
309
318
|
|
|
310
319
|
return {
|
|
311
320
|
content: [
|
|
312
321
|
{
|
|
313
322
|
type: 'text',
|
|
314
|
-
text: `Category updated!\n\n**Name**: ${
|
|
323
|
+
text: `Category updated!\n\n**Name**: ${updatedName}\n**Color**: ${colorName} (${updatedColor})\n**ID**: ${response.id || id}`,
|
|
315
324
|
},
|
|
316
325
|
],
|
|
317
326
|
_meta: {
|
|
318
|
-
category: formatCategory(
|
|
327
|
+
category: formatCategory(updatedCategory),
|
|
319
328
|
},
|
|
320
329
|
};
|
|
321
330
|
} catch (error) {
|
|
@@ -428,7 +437,9 @@ async function handleApplyCategory(args) {
|
|
|
428
437
|
};
|
|
429
438
|
}
|
|
430
439
|
|
|
431
|
-
|
|
440
|
+
const applyAction = action || 'set'; // 'set', 'add', 'remove'
|
|
441
|
+
|
|
442
|
+
if (!categories || !Array.isArray(categories)) {
|
|
432
443
|
return {
|
|
433
444
|
content: [
|
|
434
445
|
{
|
|
@@ -439,7 +450,17 @@ async function handleApplyCategory(args) {
|
|
|
439
450
|
};
|
|
440
451
|
}
|
|
441
452
|
|
|
442
|
-
|
|
453
|
+
// Empty array is only valid for action=set (clears all categories)
|
|
454
|
+
if (categories.length === 0 && applyAction !== 'set') {
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{
|
|
458
|
+
type: 'text',
|
|
459
|
+
text: 'Categories array cannot be empty for add/remove. Use action=set with an empty array to clear all categories.',
|
|
460
|
+
},
|
|
461
|
+
],
|
|
462
|
+
};
|
|
463
|
+
}
|
|
443
464
|
|
|
444
465
|
try {
|
|
445
466
|
const accessToken = await ensureAuthenticated();
|
package/email/conversations.js
CHANGED
|
@@ -17,6 +17,8 @@ const {
|
|
|
17
17
|
formatEmailsAsCSV,
|
|
18
18
|
VERBOSITY,
|
|
19
19
|
} = require('../utils/response-formatter');
|
|
20
|
+
// Note: buildFromFilter/buildToFilter from search.js use OData $filter which causes
|
|
21
|
+
// InefficientFilter on personal accounts with $orderby. Client-side filtering used instead.
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Format a date for filenames
|
|
@@ -65,10 +67,12 @@ async function handleListConversations(args) {
|
|
|
65
67
|
'id',
|
|
66
68
|
'subject',
|
|
67
69
|
'from',
|
|
70
|
+
'toRecipients',
|
|
68
71
|
'receivedDateTime',
|
|
69
72
|
'conversationId',
|
|
70
73
|
'conversationIndex',
|
|
71
74
|
'isRead',
|
|
75
|
+
'hasAttachments',
|
|
72
76
|
'bodyPreview',
|
|
73
77
|
].join(',');
|
|
74
78
|
|
|
@@ -79,6 +83,34 @@ async function handleListConversations(args) {
|
|
|
79
83
|
$top: 200, // Get more to group
|
|
80
84
|
};
|
|
81
85
|
|
|
86
|
+
// Apply simple $filter conditions that Graph API supports on personal accounts
|
|
87
|
+
// Complex filters (contains on subject, endswith on email) cause InefficientFilter
|
|
88
|
+
// errors, so those are handled client-side after fetching.
|
|
89
|
+
const serverFilterConditions = [];
|
|
90
|
+
if (args.hasAttachments === true) {
|
|
91
|
+
serverFilterConditions.push('hasAttachments eq true');
|
|
92
|
+
}
|
|
93
|
+
if (args.receivedAfter) {
|
|
94
|
+
try {
|
|
95
|
+
const afterDate = new Date(args.receivedAfter).toISOString();
|
|
96
|
+
serverFilterConditions.push(`receivedDateTime ge ${afterDate}`);
|
|
97
|
+
} catch (_e) {
|
|
98
|
+
/* ignore invalid date */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (args.receivedBefore) {
|
|
102
|
+
try {
|
|
103
|
+
const beforeDate = new Date(args.receivedBefore).toISOString();
|
|
104
|
+
serverFilterConditions.push(`receivedDateTime le ${beforeDate}`);
|
|
105
|
+
} catch (_e) {
|
|
106
|
+
/* ignore invalid date */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (serverFilterConditions.length > 0) {
|
|
111
|
+
queryParams.$filter = serverFilterConditions.join(' and ');
|
|
112
|
+
}
|
|
113
|
+
|
|
82
114
|
const response = await callGraphAPI(
|
|
83
115
|
accessToken,
|
|
84
116
|
'GET',
|
|
@@ -86,7 +118,33 @@ async function handleListConversations(args) {
|
|
|
86
118
|
null,
|
|
87
119
|
queryParams
|
|
88
120
|
);
|
|
89
|
-
|
|
121
|
+
let messages = response.value || [];
|
|
122
|
+
|
|
123
|
+
// Client-side filtering for conditions that cause InefficientFilter on personal accounts
|
|
124
|
+
if (args.subject) {
|
|
125
|
+
const subjectLower = args.subject.toLowerCase();
|
|
126
|
+
messages = messages.filter((m) =>
|
|
127
|
+
(m.subject || '').toLowerCase().includes(subjectLower)
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (args.from) {
|
|
131
|
+
const fromLower = args.from.toLowerCase();
|
|
132
|
+
messages = messages.filter((m) => {
|
|
133
|
+
const addr = (m.from?.emailAddress?.address || '').toLowerCase();
|
|
134
|
+
const name = (m.from?.emailAddress?.name || '').toLowerCase();
|
|
135
|
+
return addr.includes(fromLower) || name.includes(fromLower);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (args.to) {
|
|
139
|
+
const toLower = args.to.toLowerCase();
|
|
140
|
+
messages = messages.filter((m) =>
|
|
141
|
+
(m.toRecipients || []).some((r) => {
|
|
142
|
+
const addr = (r.emailAddress?.address || '').toLowerCase();
|
|
143
|
+
const name = (r.emailAddress?.name || '').toLowerCase();
|
|
144
|
+
return addr.includes(toLower) || name.includes(toLower);
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
}
|
|
90
148
|
|
|
91
149
|
// Group by conversationId
|
|
92
150
|
const conversations = new Map();
|