@littlebearapps/outlook-assistant 3.5.2 → 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 +8 -5
- package/email/draft.js +484 -0
- package/email/index.js +80 -0
- 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
|
|
|
@@ -374,7 +377,7 @@ This starts a local server on port 3333 to handle the OAuth callback.
|
|
|
374
377
|
|
|
375
378
|
```
|
|
376
379
|
outlook-assistant/
|
|
377
|
-
├── index.js # Main entry point (
|
|
380
|
+
├── index.js # Main entry point (22 tools)
|
|
378
381
|
├── config.js # Configuration settings
|
|
379
382
|
├── outlook-auth-server.js # OAuth server (port 3333)
|
|
380
383
|
├── auth/ # Authentication module (1 tool)
|
|
@@ -466,7 +469,7 @@ USE_TEST_MODE=true npm start
|
|
|
466
469
|
| [Azure Setup Guide](docs/guides/azure-setup.md) | Azure account creation, app registration, permissions, and secrets |
|
|
467
470
|
| [How-To Guides](docs/how-to/index.md) | 28 practical guides for email, calendar, contacts, and settings |
|
|
468
471
|
| [Troubleshooting & FAQ](docs/how-to/getting-started/verify-your-connection.md#common-connection-problems) | Common problems, re-authentication, and frequently asked questions |
|
|
469
|
-
| [Tools Reference](docs/quickrefs/tools-reference.md) | All
|
|
472
|
+
| [Tools Reference](docs/quickrefs/tools-reference.md) | All 22 tools with parameters |
|
|
470
473
|
| [AI Agent Guide](docs/how-to/ai-agents/using-outlook-assistant-in-agents.md) | Tool selection and workflow patterns for AI agents |
|
|
471
474
|
|
|
472
475
|
Full documentation: [docs/](docs/README.md)
|
package/email/draft.js
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Draft email functionality
|
|
3
|
+
*
|
|
4
|
+
* Supports creating, updating, sending, and deleting drafts,
|
|
5
|
+
* plus creating reply/reply-all/forward drafts from existing messages.
|
|
6
|
+
*/
|
|
7
|
+
const { callGraphAPI } = require('../utils/graph-api');
|
|
8
|
+
const { ensureAuthenticated } = require('../auth');
|
|
9
|
+
const {
|
|
10
|
+
checkRateLimit,
|
|
11
|
+
checkRecipientAllowlist,
|
|
12
|
+
formatDryRunPreview,
|
|
13
|
+
} = require('../utils/safety');
|
|
14
|
+
const { handleGetMailTips } = require('./mail-tips');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Format comma-separated email string into Graph API recipient objects
|
|
18
|
+
* @param {string} recipientString - Comma-separated email addresses
|
|
19
|
+
* @returns {Array<{emailAddress: {address: string}}>}
|
|
20
|
+
*/
|
|
21
|
+
function formatRecipients(recipientString) {
|
|
22
|
+
if (!recipientString) return [];
|
|
23
|
+
return recipientString.split(',').map((email) => ({
|
|
24
|
+
emailAddress: { address: email.trim() },
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Auto-detect HTML vs plain text body
|
|
30
|
+
* @param {string} body - Email body content
|
|
31
|
+
* @returns {'html'|'text'}
|
|
32
|
+
*/
|
|
33
|
+
function detectContentType(body) {
|
|
34
|
+
if (!body) return 'text';
|
|
35
|
+
return /<(html|div|p|h[1-6]|br|table|ul|ol|li|span|a\s|img|strong|em|b|i)\b/i.test(
|
|
36
|
+
body
|
|
37
|
+
)
|
|
38
|
+
? 'html'
|
|
39
|
+
: 'text';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build a message object from draft parameters
|
|
44
|
+
* @param {object} args - Draft parameters
|
|
45
|
+
* @returns {object} - Graph API message object
|
|
46
|
+
*/
|
|
47
|
+
function buildMessageObject(args) {
|
|
48
|
+
const { to, cc, bcc, subject, body, importance } = args;
|
|
49
|
+
const message = {};
|
|
50
|
+
|
|
51
|
+
if (subject !== undefined) message.subject = subject;
|
|
52
|
+
if (body !== undefined) {
|
|
53
|
+
message.body = {
|
|
54
|
+
contentType: detectContentType(body),
|
|
55
|
+
content: body,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (importance) message.importance = importance;
|
|
59
|
+
|
|
60
|
+
const toRecipients = formatRecipients(to);
|
|
61
|
+
const ccRecipients = formatRecipients(cc);
|
|
62
|
+
const bccRecipients = formatRecipients(bcc);
|
|
63
|
+
|
|
64
|
+
if (toRecipients.length > 0) message.toRecipients = toRecipients;
|
|
65
|
+
if (ccRecipients.length > 0) message.ccRecipients = ccRecipients;
|
|
66
|
+
if (bccRecipients.length > 0) message.bccRecipients = bccRecipients;
|
|
67
|
+
|
|
68
|
+
return message;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format a draft response with key details
|
|
73
|
+
* @param {object} draft - Graph API message response
|
|
74
|
+
* @param {string} actionLabel - Human-readable action (e.g. "created", "updated")
|
|
75
|
+
* @returns {object} - MCP response
|
|
76
|
+
*/
|
|
77
|
+
function formatDraftResponse(draft, actionLabel) {
|
|
78
|
+
const to = (draft.toRecipients || [])
|
|
79
|
+
.map((r) => r.emailAddress?.address)
|
|
80
|
+
.join(', ');
|
|
81
|
+
|
|
82
|
+
let text = `Draft ${actionLabel}.\n\n`;
|
|
83
|
+
text += `**ID**: \`${draft.id}\`\n`;
|
|
84
|
+
if (draft.subject) text += `**Subject**: ${draft.subject}\n`;
|
|
85
|
+
if (to) text += `**To**: ${to}\n`;
|
|
86
|
+
if (draft.lastModifiedDateTime)
|
|
87
|
+
text += `**Modified**: ${draft.lastModifiedDateTime}\n`;
|
|
88
|
+
if (draft.hasAttachments) text += `**Attachments**: yes\n`;
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: 'text', text }],
|
|
92
|
+
_meta: { draftId: draft.id },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Draft handler — routes to action-specific logic
|
|
98
|
+
* @param {object} args - Tool arguments
|
|
99
|
+
* @returns {object} - MCP response
|
|
100
|
+
*/
|
|
101
|
+
async function handleDraft(args) {
|
|
102
|
+
const { action } = args;
|
|
103
|
+
|
|
104
|
+
if (!action) {
|
|
105
|
+
return {
|
|
106
|
+
content: [
|
|
107
|
+
{
|
|
108
|
+
type: 'text',
|
|
109
|
+
text: "Action is required. Use 'create', 'update', 'send', 'delete', 'reply', 'reply-all', or 'forward'.",
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
switch (action) {
|
|
116
|
+
case 'create':
|
|
117
|
+
return handleCreateDraft(args);
|
|
118
|
+
case 'update':
|
|
119
|
+
return handleUpdateDraft(args);
|
|
120
|
+
case 'send':
|
|
121
|
+
return handleSendDraft(args);
|
|
122
|
+
case 'delete':
|
|
123
|
+
return handleDeleteDraft(args);
|
|
124
|
+
case 'reply':
|
|
125
|
+
return handleReplyDraft(args, 'createReply');
|
|
126
|
+
case 'reply-all':
|
|
127
|
+
return handleReplyDraft(args, 'createReplyAll');
|
|
128
|
+
case 'forward':
|
|
129
|
+
return handleForwardDraft(args);
|
|
130
|
+
default:
|
|
131
|
+
return {
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'text',
|
|
135
|
+
text: `Invalid action '${action}'. Use 'create', 'update', 'send', 'delete', 'reply', 'reply-all', or 'forward'.`,
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Create a new draft
|
|
144
|
+
*/
|
|
145
|
+
async function handleCreateDraft(args) {
|
|
146
|
+
const { dryRun = false, checkRecipients: doCheckRecipients = false } = args;
|
|
147
|
+
const message = buildMessageObject(args);
|
|
148
|
+
|
|
149
|
+
// Check recipient allowlist if recipients specified
|
|
150
|
+
const allRecipients = [
|
|
151
|
+
...(message.toRecipients || []),
|
|
152
|
+
...(message.ccRecipients || []),
|
|
153
|
+
...(message.bccRecipients || []),
|
|
154
|
+
];
|
|
155
|
+
if (allRecipients.length > 0) {
|
|
156
|
+
const allowlistError = checkRecipientAllowlist(allRecipients);
|
|
157
|
+
if (allowlistError) return allowlistError;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Pre-save recipient validation via mail-tips
|
|
161
|
+
if (doCheckRecipients && allRecipients.length > 0) {
|
|
162
|
+
const allAddresses = allRecipients.map((r) => r.emailAddress.address);
|
|
163
|
+
const tipsResult = await handleGetMailTips({ recipients: allAddresses });
|
|
164
|
+
const tipsText = tipsResult.content[0]?.text || '';
|
|
165
|
+
|
|
166
|
+
if (dryRun) {
|
|
167
|
+
const preview = formatDryRunPreview({ message, saveToSentItems: true });
|
|
168
|
+
return {
|
|
169
|
+
content: [
|
|
170
|
+
{
|
|
171
|
+
type: 'text',
|
|
172
|
+
text:
|
|
173
|
+
tipsText +
|
|
174
|
+
'\n\n---\n\n' +
|
|
175
|
+
preview.content[0].text.replace(
|
|
176
|
+
'Email NOT sent',
|
|
177
|
+
'Draft NOT saved'
|
|
178
|
+
),
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
_meta: { mailTips: tipsResult._meta },
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Dry-run mode: preview without saving
|
|
187
|
+
if (dryRun) {
|
|
188
|
+
const preview = formatDryRunPreview({ message, saveToSentItems: true });
|
|
189
|
+
return {
|
|
190
|
+
content: [
|
|
191
|
+
{
|
|
192
|
+
type: 'text',
|
|
193
|
+
text: preview.content[0].text.replace(
|
|
194
|
+
'Email NOT sent',
|
|
195
|
+
'Draft NOT saved'
|
|
196
|
+
),
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Rate limit check
|
|
203
|
+
const rateLimitError = checkRateLimit('draft');
|
|
204
|
+
if (rateLimitError) return rateLimitError;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const accessToken = await ensureAuthenticated();
|
|
208
|
+
const draft = await callGraphAPI(
|
|
209
|
+
accessToken,
|
|
210
|
+
'POST',
|
|
211
|
+
'me/messages',
|
|
212
|
+
message
|
|
213
|
+
);
|
|
214
|
+
return formatDraftResponse(draft, 'created');
|
|
215
|
+
} catch (error) {
|
|
216
|
+
return handleError('creating draft', error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Update an existing draft
|
|
222
|
+
*/
|
|
223
|
+
async function handleUpdateDraft(args) {
|
|
224
|
+
const { id } = args;
|
|
225
|
+
|
|
226
|
+
if (!id) {
|
|
227
|
+
return {
|
|
228
|
+
content: [
|
|
229
|
+
{ type: 'text', text: 'Draft ID (id) is required for update.' },
|
|
230
|
+
],
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const message = buildMessageObject(args);
|
|
235
|
+
|
|
236
|
+
// Check recipient allowlist if recipients changed
|
|
237
|
+
const allRecipients = [
|
|
238
|
+
...(message.toRecipients || []),
|
|
239
|
+
...(message.ccRecipients || []),
|
|
240
|
+
...(message.bccRecipients || []),
|
|
241
|
+
];
|
|
242
|
+
if (allRecipients.length > 0) {
|
|
243
|
+
const allowlistError = checkRecipientAllowlist(allRecipients);
|
|
244
|
+
if (allowlistError) return allowlistError;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Rate limit check
|
|
248
|
+
const rateLimitError = checkRateLimit('draft');
|
|
249
|
+
if (rateLimitError) return rateLimitError;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const accessToken = await ensureAuthenticated();
|
|
253
|
+
const draft = await callGraphAPI(
|
|
254
|
+
accessToken,
|
|
255
|
+
'PATCH',
|
|
256
|
+
`me/messages/${id}`,
|
|
257
|
+
message
|
|
258
|
+
);
|
|
259
|
+
return formatDraftResponse(draft, 'updated');
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return handleError('updating draft', error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Send an existing draft
|
|
267
|
+
*/
|
|
268
|
+
async function handleSendDraft(args) {
|
|
269
|
+
const { id } = args;
|
|
270
|
+
|
|
271
|
+
if (!id) {
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: 'text', text: 'Draft ID (id) is required for send.' }],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Rate limit via send-email counter (shares limit with direct sends)
|
|
278
|
+
const rateLimitError = checkRateLimit('send-email');
|
|
279
|
+
if (rateLimitError) return rateLimitError;
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const accessToken = await ensureAuthenticated();
|
|
283
|
+
await callGraphAPI(accessToken, 'POST', `me/messages/${id}/send`);
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: 'text',
|
|
288
|
+
text: `Draft sent successfully.\n\n**Note**: The draft ID \`${id}\` is no longer valid — the message has been moved to Sent Items with a new ID.`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
};
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return handleError('sending draft', error);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Delete a draft
|
|
299
|
+
*/
|
|
300
|
+
async function handleDeleteDraft(args) {
|
|
301
|
+
const { id } = args;
|
|
302
|
+
|
|
303
|
+
if (!id) {
|
|
304
|
+
return {
|
|
305
|
+
content: [
|
|
306
|
+
{ type: 'text', text: 'Draft ID (id) is required for delete.' },
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const accessToken = await ensureAuthenticated();
|
|
313
|
+
await callGraphAPI(accessToken, 'DELETE', `me/messages/${id}`);
|
|
314
|
+
return {
|
|
315
|
+
content: [
|
|
316
|
+
{
|
|
317
|
+
type: 'text',
|
|
318
|
+
text: `Draft \`${id}\` deleted.`,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
};
|
|
322
|
+
} catch (error) {
|
|
323
|
+
return handleError('deleting draft', error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Create a reply or reply-all draft from an existing message
|
|
329
|
+
*/
|
|
330
|
+
async function handleReplyDraft(args, endpoint) {
|
|
331
|
+
const { id, body, comment } = args;
|
|
332
|
+
|
|
333
|
+
if (!id) {
|
|
334
|
+
return {
|
|
335
|
+
content: [
|
|
336
|
+
{
|
|
337
|
+
type: 'text',
|
|
338
|
+
text: `Message ID (id) is required for ${endpoint === 'createReplyAll' ? 'reply-all' : 'reply'}.`,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (comment && body) {
|
|
345
|
+
return {
|
|
346
|
+
content: [
|
|
347
|
+
{
|
|
348
|
+
type: 'text',
|
|
349
|
+
text: 'Cannot use both comment and body. Use comment for a short prepended note, or body for full HTML/text content.',
|
|
350
|
+
},
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const requestBody = {};
|
|
356
|
+
if (comment) {
|
|
357
|
+
requestBody.comment = comment;
|
|
358
|
+
} else if (body) {
|
|
359
|
+
requestBody.message = {
|
|
360
|
+
body: {
|
|
361
|
+
contentType: detectContentType(body),
|
|
362
|
+
content: body,
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const accessToken = await ensureAuthenticated();
|
|
369
|
+
const draft = await callGraphAPI(
|
|
370
|
+
accessToken,
|
|
371
|
+
'POST',
|
|
372
|
+
`me/messages/${id}/${endpoint}`,
|
|
373
|
+
Object.keys(requestBody).length > 0 ? requestBody : null
|
|
374
|
+
);
|
|
375
|
+
const label =
|
|
376
|
+
endpoint === 'createReplyAll'
|
|
377
|
+
? 'reply-all draft created'
|
|
378
|
+
: 'reply draft created';
|
|
379
|
+
return formatDraftResponse(draft, label);
|
|
380
|
+
} catch (error) {
|
|
381
|
+
return handleError(
|
|
382
|
+
`creating ${endpoint === 'createReplyAll' ? 'reply-all' : 'reply'} draft`,
|
|
383
|
+
error
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Create a forward draft from an existing message
|
|
390
|
+
*/
|
|
391
|
+
async function handleForwardDraft(args) {
|
|
392
|
+
const { id, to, body, comment } = args;
|
|
393
|
+
|
|
394
|
+
if (!id) {
|
|
395
|
+
return {
|
|
396
|
+
content: [
|
|
397
|
+
{ type: 'text', text: 'Message ID (id) is required for forward.' },
|
|
398
|
+
],
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!to) {
|
|
403
|
+
return {
|
|
404
|
+
content: [
|
|
405
|
+
{
|
|
406
|
+
type: 'text',
|
|
407
|
+
text: 'Forward recipient (to) is required for forward.',
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (comment && body) {
|
|
414
|
+
return {
|
|
415
|
+
content: [
|
|
416
|
+
{
|
|
417
|
+
type: 'text',
|
|
418
|
+
text: 'Cannot use both comment and body. Use comment for a short prepended note, or body for full HTML/text content.',
|
|
419
|
+
},
|
|
420
|
+
],
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const toRecipients = formatRecipients(to);
|
|
425
|
+
|
|
426
|
+
// Check recipient allowlist
|
|
427
|
+
const allowlistError = checkRecipientAllowlist(toRecipients);
|
|
428
|
+
if (allowlistError) return allowlistError;
|
|
429
|
+
|
|
430
|
+
const requestBody = {
|
|
431
|
+
toRecipients,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (comment) {
|
|
435
|
+
requestBody.comment = comment;
|
|
436
|
+
} else if (body) {
|
|
437
|
+
requestBody.message = {
|
|
438
|
+
body: {
|
|
439
|
+
contentType: detectContentType(body),
|
|
440
|
+
content: body,
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
const accessToken = await ensureAuthenticated();
|
|
447
|
+
const draft = await callGraphAPI(
|
|
448
|
+
accessToken,
|
|
449
|
+
'POST',
|
|
450
|
+
`me/messages/${id}/createForward`,
|
|
451
|
+
requestBody
|
|
452
|
+
);
|
|
453
|
+
return formatDraftResponse(draft, 'forward draft created');
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return handleError('creating forward draft', error);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Standard error handler
|
|
461
|
+
*/
|
|
462
|
+
function handleError(actionLabel, error) {
|
|
463
|
+
if (error.message === 'Authentication required') {
|
|
464
|
+
return {
|
|
465
|
+
content: [
|
|
466
|
+
{
|
|
467
|
+
type: 'text',
|
|
468
|
+
text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return {
|
|
475
|
+
content: [
|
|
476
|
+
{
|
|
477
|
+
type: 'text',
|
|
478
|
+
text: `Error ${actionLabel}: ${error.message}`,
|
|
479
|
+
},
|
|
480
|
+
],
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
module.exports = handleDraft;
|
package/email/index.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
handleExportConversation,
|
|
24
24
|
} = require('./conversations');
|
|
25
25
|
const { handleGetMailTips } = require('./mail-tips');
|
|
26
|
+
const handleDraft = require('./draft');
|
|
26
27
|
|
|
27
28
|
// Import flag handlers from advanced module
|
|
28
29
|
const { handleSetMessageFlag, handleClearMessageFlag } = require('../advanced');
|
|
@@ -289,6 +290,84 @@ const emailTools = [
|
|
|
289
290
|
},
|
|
290
291
|
handler: handleSendEmail,
|
|
291
292
|
},
|
|
293
|
+
{
|
|
294
|
+
name: 'draft',
|
|
295
|
+
description:
|
|
296
|
+
'Manage email drafts. action=create saves a new draft. action=update edits an existing draft. action=send sends a draft. action=delete removes a draft. action=reply/reply-all/forward creates a reply or forward draft from an existing message.',
|
|
297
|
+
annotations: {
|
|
298
|
+
title: 'Draft Operations',
|
|
299
|
+
readOnlyHint: false,
|
|
300
|
+
destructiveHint: true,
|
|
301
|
+
idempotentHint: false,
|
|
302
|
+
openWorldHint: true,
|
|
303
|
+
},
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
action: {
|
|
308
|
+
type: 'string',
|
|
309
|
+
enum: [
|
|
310
|
+
'create',
|
|
311
|
+
'update',
|
|
312
|
+
'send',
|
|
313
|
+
'delete',
|
|
314
|
+
'reply',
|
|
315
|
+
'reply-all',
|
|
316
|
+
'forward',
|
|
317
|
+
],
|
|
318
|
+
description: 'Action to perform (required)',
|
|
319
|
+
},
|
|
320
|
+
id: {
|
|
321
|
+
type: 'string',
|
|
322
|
+
description:
|
|
323
|
+
'Draft or message ID. Required for update/send/delete/reply/reply-all/forward.',
|
|
324
|
+
},
|
|
325
|
+
to: {
|
|
326
|
+
type: 'string',
|
|
327
|
+
description:
|
|
328
|
+
'Comma-separated recipient email addresses (optional for create/update, required for forward)',
|
|
329
|
+
},
|
|
330
|
+
cc: {
|
|
331
|
+
type: 'string',
|
|
332
|
+
description: 'Comma-separated CC email addresses',
|
|
333
|
+
},
|
|
334
|
+
bcc: {
|
|
335
|
+
type: 'string',
|
|
336
|
+
description: 'Comma-separated BCC email addresses',
|
|
337
|
+
},
|
|
338
|
+
subject: {
|
|
339
|
+
type: 'string',
|
|
340
|
+
description: 'Email subject',
|
|
341
|
+
},
|
|
342
|
+
body: {
|
|
343
|
+
type: 'string',
|
|
344
|
+
description: 'Email body (plain text or HTML)',
|
|
345
|
+
},
|
|
346
|
+
importance: {
|
|
347
|
+
type: 'string',
|
|
348
|
+
enum: ['normal', 'high', 'low'],
|
|
349
|
+
description: 'Email importance (default: normal)',
|
|
350
|
+
},
|
|
351
|
+
comment: {
|
|
352
|
+
type: 'string',
|
|
353
|
+
description:
|
|
354
|
+
'Comment text for reply/forward (prepended to original message). Cannot combine with body.',
|
|
355
|
+
},
|
|
356
|
+
dryRun: {
|
|
357
|
+
type: 'boolean',
|
|
358
|
+
description:
|
|
359
|
+
'Preview draft without saving (action=create only, default: false)',
|
|
360
|
+
},
|
|
361
|
+
checkRecipients: {
|
|
362
|
+
type: 'boolean',
|
|
363
|
+
description:
|
|
364
|
+
'Check recipients for out-of-office, delivery restrictions before saving (action=create, default: false)',
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
required: ['action'],
|
|
368
|
+
},
|
|
369
|
+
handler: handleDraft,
|
|
370
|
+
},
|
|
292
371
|
{
|
|
293
372
|
name: 'update-email',
|
|
294
373
|
description:
|
|
@@ -559,6 +638,7 @@ const emailTools = [
|
|
|
559
638
|
|
|
560
639
|
module.exports = {
|
|
561
640
|
emailTools,
|
|
641
|
+
handleDraft,
|
|
562
642
|
handleListEmails,
|
|
563
643
|
handleSearchEmails,
|
|
564
644
|
handleSearchByMessageId,
|
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**: 22 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
|
+
- 8 email tools covering search, conversations, attachments, bulk export, pre-send mail tips, and draft management
|
|
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
|
|
|
@@ -31,9 +31,9 @@ Built by [Little Bear Apps](https://littlebearapps.com).
|
|
|
31
31
|
|
|
32
32
|
## Safety & Token Efficiency
|
|
33
33
|
|
|
34
|
-
- **MCP safety annotations** on all
|
|
34
|
+
- **MCP safety annotations** on all 22 tools — AI clients auto-approve reads and prompt for destructive operations
|
|
35
35
|
- **Send-email protections**: pre-send mail tips, dry-run preview, session rate limiting, recipient allowlist
|
|
36
|
-
- **Token-optimised**:
|
|
36
|
+
- **Token-optimised**: 22 tools instead of 55 saves ~11,000 tokens per turn (~64% reduction), improving AI accuracy and context efficiency
|
|
37
37
|
- These safeguards reduce risk but are not foolproof — always review actions before approving
|
|
38
38
|
|
|
39
39
|
## Quick Start
|
|
@@ -58,7 +58,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
58
58
|
## Tool Categories
|
|
59
59
|
|
|
60
60
|
- **Authentication (1 tool)**: `auth` — OAuth flow, status, about
|
|
61
|
-
- **Email (
|
|
61
|
+
- **Email (8 tools)**: `search-emails`, `read-email`, `send-email`, `draft`, `update-email`, `attachments`, `export`, `get-mail-tips`
|
|
62
62
|
- **Calendar (3 tools)**: `list-events`, `create-event`, `manage-event`
|
|
63
63
|
- **Contacts (2 tools)**: `manage-contact`, `search-people`
|
|
64
64
|
- **Folders (1 tool)**: `folders` — list, create, move, stats
|
|
@@ -70,7 +70,7 @@ Requires an Azure app registration with Microsoft Graph delegated permissions. S
|
|
|
70
70
|
## Documentation
|
|
71
71
|
|
|
72
72
|
- [README](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/README.md): Full documentation including setup, Azure configuration, and usage
|
|
73
|
-
- [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 22 tools with parameters and safety annotations
|
|
74
74
|
- [CLAUDE.md](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CLAUDE.md): Quick reference for development
|
|
75
75
|
- [CONTRIBUTING](https://raw.githubusercontent.com/littlebearapps/outlook-assistant/main/CONTRIBUTING.md): Contribution guidelines
|
|
76
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.6.0",
|
|
4
4
|
"mcpName": "io.github.littlebearapps/outlook-assistant",
|
|
5
|
-
"description": "Outlook Assistant — MCP server with
|
|
5
|
+
"description": "Outlook Assistant — MCP server with 22 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"
|
package/utils/field-presets.js
CHANGED
|
@@ -107,6 +107,26 @@ const FIELD_PRESETS = {
|
|
|
107
107
|
'changeKey',
|
|
108
108
|
],
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Draft fields for viewing/editing drafts
|
|
112
|
+
* Use case: Draft creation, update, listing
|
|
113
|
+
*/
|
|
114
|
+
draft: [
|
|
115
|
+
'id',
|
|
116
|
+
'subject',
|
|
117
|
+
'from',
|
|
118
|
+
'toRecipients',
|
|
119
|
+
'ccRecipients',
|
|
120
|
+
'bccRecipients',
|
|
121
|
+
'body',
|
|
122
|
+
'bodyPreview',
|
|
123
|
+
'lastModifiedDateTime',
|
|
124
|
+
'isDraft',
|
|
125
|
+
'importance',
|
|
126
|
+
'hasAttachments',
|
|
127
|
+
'conversationId',
|
|
128
|
+
],
|
|
129
|
+
|
|
110
130
|
/**
|
|
111
131
|
* Search result fields (optimized for relevance display)
|
|
112
132
|
* Use case: Search results with context
|
package/utils/graph-api.js
CHANGED
|
@@ -7,6 +7,7 @@ const mockData = require('./mock-data');
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Makes a request to the Microsoft Graph API
|
|
10
|
+
* In test mode (USE_TEST_MODE=true), routes to mock data instead of the real API.
|
|
10
11
|
* @param {string} accessToken - The access token for authentication
|
|
11
12
|
* @param {string} method - HTTP method (GET, POST, etc.)
|
|
12
13
|
* @param {string} path - API endpoint path
|
|
@@ -14,6 +15,8 @@ const mockData = require('./mock-data');
|
|
|
14
15
|
* @param {object} queryParams - Query parameters
|
|
15
16
|
* @param {object} extraHeaders - Additional headers (e.g. Prefer for immutable IDs)
|
|
16
17
|
* @returns {Promise<object>} - The API response
|
|
18
|
+
* @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
|
|
19
|
+
* @throws {Error} If the HTTP status is outside 2xx, or if JSON parsing or network fails
|
|
17
20
|
*/
|
|
18
21
|
async function callGraphAPI(
|
|
19
22
|
accessToken,
|
|
@@ -153,6 +156,8 @@ async function callGraphAPI(
|
|
|
153
156
|
* @param {object} queryParams - Initial query parameters
|
|
154
157
|
* @param {number} maxCount - Maximum number of items to retrieve (0 = all)
|
|
155
158
|
* @returns {Promise<object>} - Combined API response with all items
|
|
159
|
+
* @throws {Error} If method is not 'GET'
|
|
160
|
+
* @throws {Error} If any page request fails for any other reason
|
|
156
161
|
*/
|
|
157
162
|
async function callGraphAPIPaginated(
|
|
158
163
|
accessToken,
|
|
@@ -268,9 +273,12 @@ async function callGraphAPIBatch(accessToken, requests) {
|
|
|
268
273
|
|
|
269
274
|
/**
|
|
270
275
|
* Calls Graph API to get raw MIME content (for email export)
|
|
276
|
+
* In test mode (USE_TEST_MODE=true), returns mock MIME content instead of calling the real API.
|
|
271
277
|
* @param {string} accessToken - The access token for authentication
|
|
272
278
|
* @param {string} emailId - The email ID to export
|
|
273
279
|
* @returns {Promise<string>} - Raw MIME content as string
|
|
280
|
+
* @throws {Error} 'UNAUTHORIZED' if the server returns HTTP 401 (token expired or invalid)
|
|
281
|
+
* @throws {Error} If the HTTP status is outside 2xx or a network error occurs
|
|
274
282
|
*/
|
|
275
283
|
async function callGraphAPIRaw(accessToken, emailId) {
|
|
276
284
|
// Test mode: return mock MIME content
|