@littlebearapps/outlook-assistant 3.5.2 → 3.7.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 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,17 +61,17 @@ 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** | 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` |
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` |
67
68
  | **Settings** | 1 | `mailbox-settings` (get/set auto-replies/set working hours) |
68
69
  | **Folder** | 1 | `folders` (list/create/move/stats/delete) |
69
- | **Rules** | 1 | `manage-rules` (list/create/reorder/delete) |
70
+ | **Rules** | 1 | `manage-rules` (list/create/update/reorder/delete) |
70
71
  | **Advanced** | 2 | `access-shared-mailbox`, `find-meeting-rooms` |
71
72
  | **Auth** | 1 | `auth` (status/authenticate/about) |
72
73
 
73
- **21 tools total** — consolidated from 55 for optimal AI performance. See the [Tools Reference](docs/quickrefs/tools-reference.md) for complete parameter details.
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
- **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.
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 (21 tools)
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 21 tools with parameters |
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/advanced/index.js CHANGED
@@ -96,7 +96,7 @@ async function handleAccessSharedMailbox(args) {
96
96
  };
97
97
  }
98
98
 
99
- let output = [];
99
+ const output = [];
100
100
  output.push(`# Shared Mailbox: ${sharedMailbox}`);
101
101
  output.push(`**Folder**: ${mailFolder} | **Count**: ${messages.length}\n`);
102
102
 
@@ -241,7 +241,7 @@ async function handleSetMessageFlag(args) {
241
241
  timeZone: DEFAULT_TIMEZONE,
242
242
  };
243
243
  } else {
244
- const startOfDay = dueDt.split('T')[0] + 'T09:00:00';
244
+ const startOfDay = `${dueDt.split('T')[0]}T09:00:00`;
245
245
  flag.startDateTime = {
246
246
  dateTime: startOfDay,
247
247
  timeZone: DEFAULT_TIMEZONE,
@@ -269,7 +269,7 @@ async function handleSetMessageFlag(args) {
269
269
  }
270
270
  }
271
271
 
272
- let output = [];
272
+ const output = [];
273
273
 
274
274
  if (results.length > 0) {
275
275
  output.push(`Flagged ${results.length} message(s) for follow-up`);
@@ -377,7 +377,7 @@ async function handleClearMessageFlag(args) {
377
377
  }
378
378
 
379
379
  const action = markComplete ? 'marked complete' : 'cleared';
380
- let output = [];
380
+ const output = [];
381
381
 
382
382
  if (results.length > 0) {
383
383
  output.push(`${results.length} message(s) ${action}`);
@@ -504,7 +504,7 @@ async function handleFindMeetingRooms(args) {
504
504
  };
505
505
  }
506
506
 
507
- let output = [];
507
+ const output = [];
508
508
  output.push(`# Meeting Rooms (${rooms.length})\n`);
509
509
 
510
510
  if (verbosity === 'minimal') {
@@ -92,7 +92,9 @@ async function pollForToken(clientId, deviceCode, interval, expiresIn) {
92
92
  let pollInterval = interval;
93
93
 
94
94
  while (Date.now() < deadline) {
95
- await new Promise((resolve) => setTimeout(resolve, pollInterval * 1000));
95
+ await new Promise((resolve) => {
96
+ setTimeout(resolve, pollInterval * 1000);
97
+ });
96
98
 
97
99
  const postData = querystring.stringify({
98
100
  client_id: clientId,
@@ -112,16 +112,16 @@ function setupOAuthRoutes(
112
112
  // Since this is a module, actual session handling is outside its direct scope,
113
113
  // but it's crucial for the consuming application to handle state verification.
114
114
 
115
- const authorizationUrl =
116
- `${authConfig.authEndpoint}?` +
117
- querystring.stringify({
115
+ const authorizationUrl = `${authConfig.authEndpoint}?${querystring.stringify(
116
+ {
118
117
  client_id: authConfig.clientId,
119
118
  response_type: 'code',
120
119
  redirect_uri: authConfig.redirectUri,
121
120
  scope: authConfig.scopes.join(' '),
122
121
  response_mode: 'query',
123
122
  state: state,
124
- });
123
+ }
124
+ )}`;
125
125
  res.redirect(authorizationUrl);
126
126
  });
127
127
 
@@ -88,8 +88,8 @@ function getAccessToken() {
88
88
  */
89
89
  function createTestTokens() {
90
90
  const testTokens = {
91
- access_token: 'test_access_token_' + Date.now(),
92
- refresh_token: 'test_refresh_token_' + Date.now(),
91
+ access_token: `test_access_token_${Date.now()}`,
92
+ refresh_token: `test_refresh_token_${Date.now()}`,
93
93
  expires_at: Date.now() + 3600 * 1000, // 1 hour
94
94
  };
95
95
 
package/calendar/list.js CHANGED
@@ -18,7 +18,7 @@ async function handleListEvents(args) {
18
18
  const accessToken = await ensureAuthenticated();
19
19
 
20
20
  // Build API endpoint
21
- let endpoint = 'me/events';
21
+ const endpoint = 'me/events';
22
22
 
23
23
  // Add query parameters
24
24
  const queryParams = {
@@ -54,10 +54,10 @@ async function handleListEvents(args) {
54
54
  .map((event, index) => {
55
55
  const startDt = event.start.dateTime.endsWith('Z')
56
56
  ? event.start.dateTime
57
- : event.start.dateTime + 'Z';
57
+ : `${event.start.dateTime}Z`;
58
58
  const endDt = event.end.dateTime.endsWith('Z')
59
59
  ? event.end.dateTime
60
- : event.end.dateTime + 'Z';
60
+ : `${event.end.dateTime}Z`;
61
61
  const startDate = new Date(startDt).toLocaleString('en-AU', {
62
62
  timeZone: tz,
63
63
  dateStyle: 'medium',
@@ -106,7 +106,7 @@ async function handleListCategories(args) {
106
106
  }
107
107
 
108
108
  // Format output based on verbosity
109
- let output = [];
109
+ const output = [];
110
110
  output.push(`# Master Categories (${categories.length})\n`);
111
111
 
112
112
  if (outputVerbosity === 'minimal') {
@@ -117,7 +117,7 @@ async function handleListCategories(args) {
117
117
  categories.forEach((cat) => {
118
118
  const colorName = COLOR_NAMES[cat.color] || cat.color;
119
119
  const idDisplay =
120
- outputVerbosity === 'full' ? cat.id : cat.id.substring(0, 8) + '...';
120
+ outputVerbosity === 'full' ? cat.id : `${cat.id.substring(0, 8)}...`;
121
121
  output.push(`| ${cat.displayName} | ${colorName} | ${idDisplay} |`);
122
122
  });
123
123
  }
@@ -503,7 +503,7 @@ async function handleApplyCategory(args) {
503
503
  }
504
504
  }
505
505
 
506
- let output = [];
506
+ const output = [];
507
507
 
508
508
  if (results.length > 0) {
509
509
  output.push(
@@ -591,7 +591,7 @@ async function handleGetFocusedInboxOverrides(args) {
591
591
  };
592
592
  }
593
593
 
594
- let output = [];
594
+ const output = [];
595
595
  output.push(`# Focused Inbox Overrides (${overrides.length})\n`);
596
596
 
597
597
  // Group by classification
package/contacts/index.js CHANGED
@@ -66,10 +66,12 @@ function formatContact(contact, verbosity = 'standard') {
66
66
  // Phone numbers
67
67
  const phones = [];
68
68
  if (contact.mobilePhone) phones.push(`Mobile: ${contact.mobilePhone}`);
69
- if (contact.businessPhones?.length > 0)
69
+ if (contact.businessPhones?.length > 0) {
70
70
  phones.push(`Work: ${contact.businessPhones[0]}`);
71
- if (contact.homePhones?.length > 0)
71
+ }
72
+ if (contact.homePhones?.length > 0) {
72
73
  phones.push(`Home: ${contact.homePhones[0]}`);
74
+ }
73
75
  if (phones.length > 0) {
74
76
  lines.push(`**Phone**: ${phones.join(' | ')}`);
75
77
  }
@@ -86,8 +88,9 @@ function formatContact(contact, verbosity = 'standard') {
86
88
  // Full verbosity extras
87
89
  if (verbosity === 'full') {
88
90
  if (contact.department) lines.push(`**Department**: ${contact.department}`);
89
- if (contact.officeLocation)
91
+ if (contact.officeLocation) {
90
92
  lines.push(`**Office**: ${contact.officeLocation}`);
93
+ }
91
94
  if (contact.birthday) lines.push(`**Birthday**: ${contact.birthday}`);
92
95
 
93
96
  // Addresses
@@ -130,12 +133,7 @@ async function handleListContacts(args) {
130
133
  try {
131
134
  const accessToken = await ensureAuthenticated();
132
135
 
133
- const fields =
134
- verbosity === 'full'
135
- ? CONTACT_FIELDS.full
136
- : verbosity === 'minimal'
137
- ? CONTACT_FIELDS.minimal
138
- : CONTACT_FIELDS.list;
136
+ const fields = CONTACT_FIELDS[verbosity] || CONTACT_FIELDS.list;
139
137
  const endpoint = folder
140
138
  ? `me/contactFolders/${folder}/contacts`
141
139
  : 'me/contacts';
@@ -155,7 +153,7 @@ async function handleListContacts(args) {
155
153
  );
156
154
  const contacts = response.value || [];
157
155
 
158
- let output = [];
156
+ const output = [];
159
157
  output.push(`# Contacts\n`);
160
158
  output.push(`**Total**: ${contacts.length}`);
161
159
  output.push('');
@@ -202,12 +200,7 @@ async function handleSearchContacts(args) {
202
200
  try {
203
201
  const accessToken = await ensureAuthenticated();
204
202
 
205
- const fields =
206
- verbosity === 'full'
207
- ? CONTACT_FIELDS.full
208
- : verbosity === 'minimal'
209
- ? CONTACT_FIELDS.minimal
210
- : CONTACT_FIELDS.list;
203
+ const fields = CONTACT_FIELDS[verbosity] || CONTACT_FIELDS.list;
211
204
  const endpoint = 'me/contacts';
212
205
 
213
206
  // Build filter for name — emailAddresses/any() lambda is unreliable on personal accounts
@@ -259,7 +252,7 @@ async function handleSearchContacts(args) {
259
252
  }
260
253
  const contacts = response.value || [];
261
254
 
262
- let output = [];
255
+ const output = [];
263
256
  output.push(`# Contact Search Results\n`);
264
257
  output.push(`**Query**: "${query}"`);
265
258
  output.push(`**Found**: ${contacts.length}`);
@@ -559,7 +552,7 @@ async function handleSearchPeople(args) {
559
552
  );
560
553
  const people = response.value || [];
561
554
 
562
- let output = [];
555
+ const output = [];
563
556
  output.push(`# People Search Results\n`);
564
557
  output.push(`**Query**: "${query}"`);
565
558
  output.push(`**Found**: ${people.length} (sorted by relevance)`);
@@ -175,12 +175,12 @@ async function handleListConversations(args) {
175
175
  });
176
176
 
177
177
  // Convert to array and sort by most recent
178
- let conversationList = Array.from(conversations.values())
178
+ const conversationList = Array.from(conversations.values())
179
179
  .sort((a, b) => new Date(b.lastDate) - new Date(a.lastDate))
180
180
  .slice(0, count);
181
181
 
182
182
  // Format output
183
- let output = [];
183
+ const output = [];
184
184
  output.push(`# Email Conversations\n`);
185
185
  output.push(`**Folder**: ${folder}`);
186
186
  output.push(`**Conversations**: ${conversationList.length}`);
@@ -314,7 +314,7 @@ async function handleGetConversation(args) {
314
314
  }
315
315
 
316
316
  // Format output
317
- let output = [];
317
+ const output = [];
318
318
  output.push(`# Email Conversation\n`);
319
319
  output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
320
320
  output.push(`**Messages**: ${messages.length}`);
@@ -463,8 +463,8 @@ async function handleExportConversation(args) {
463
463
  const date = formatDateForFilename(messages[0].receivedDateTime);
464
464
  const filenameBase = `${date}_${subject}_conversation`;
465
465
 
466
- let exportedFiles = [];
467
- let exportStats = { messages: messages.length, attachments: 0, bytes: 0 };
466
+ const exportedFiles = [];
467
+ const exportStats = { messages: messages.length, attachments: 0, bytes: 0 };
468
468
 
469
469
  switch (format) {
470
470
  case 'eml': {
@@ -497,8 +497,8 @@ async function handleExportConversation(args) {
497
497
  for (const msg of messages) {
498
498
  const mimeContent = await callGraphAPIRaw(accessToken, msg.id);
499
499
  const from = msg.from?.emailAddress?.address || 'unknown@unknown.com';
500
- const date = new Date(msg.receivedDateTime);
501
- const mboxDate = date.toUTCString().replace('GMT', '+0000');
500
+ const msgDate = new Date(msg.receivedDateTime);
501
+ const mboxDate = msgDate.toUTCString().replace('GMT', '+0000');
502
502
 
503
503
  // MBOX format: From line + MIME content + blank line
504
504
  mboxContent += `From ${from} ${mboxDate}\n`;
@@ -515,7 +515,7 @@ async function handleExportConversation(args) {
515
515
  case 'markdown': {
516
516
  // Export as threaded Markdown document
517
517
  const mdPath = path.join(resolvedDir, `${filenameBase}.md`);
518
- let mdContent = [];
518
+ const mdContent = [];
519
519
 
520
520
  mdContent.push(
521
521
  `# Email Conversation: ${messages[0].subject || '(no subject)'}\n`
@@ -600,7 +600,7 @@ async function handleExportConversation(args) {
600
600
  case 'html': {
601
601
  // Export as HTML document
602
602
  const htmlPath = path.join(resolvedDir, `${filenameBase}.html`);
603
- let htmlContent = [];
603
+ const htmlContent = [];
604
604
 
605
605
  htmlContent.push('<!DOCTYPE html>');
606
606
  htmlContent.push('<html><head>');
@@ -674,14 +674,16 @@ async function handleExportConversation(args) {
674
674
  }
675
675
 
676
676
  // Format result
677
- const sizeFormatted =
678
- exportStats.bytes < 1024
679
- ? `${exportStats.bytes} B`
680
- : exportStats.bytes < 1024 * 1024
681
- ? `${(exportStats.bytes / 1024).toFixed(1)} KB`
682
- : `${(exportStats.bytes / (1024 * 1024)).toFixed(2)} MB`;
683
-
684
- let output = [];
677
+ let sizeFormatted;
678
+ if (exportStats.bytes < 1024) {
679
+ sizeFormatted = `${exportStats.bytes} B`;
680
+ } else if (exportStats.bytes < 1024 * 1024) {
681
+ sizeFormatted = `${(exportStats.bytes / 1024).toFixed(1)} KB`;
682
+ } else {
683
+ sizeFormatted = `${(exportStats.bytes / (1024 * 1024)).toFixed(2)} MB`;
684
+ }
685
+
686
+ const output = [];
685
687
  output.push(`# Conversation Exported\n`);
686
688
  output.push(`**Subject**: ${messages[0].subject || '(no subject)'}`);
687
689
  output.push(`**Format**: ${format.toUpperCase()}`);
package/email/delta.js CHANGED
@@ -88,7 +88,7 @@ async function handleListEmailsDelta(args) {
88
88
 
89
89
  // Build response
90
90
  const isInitialSync = !deltaToken;
91
- const hasMoreChanges = !!nextLink;
91
+ const hasMoreChanges = Boolean(nextLink);
92
92
  const newDeltaToken = deltaLink || nextLink;
93
93
 
94
94
  // Format output based on verbosity