@littlebearapps/outlook-assistant 3.13.0 → 3.14.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.
Files changed (67) hide show
  1. package/.env.example +27 -3
  2. package/README.md +66 -26
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/oauth-server.js +7 -1
  6. package/auth/token-manager.js +7 -3
  7. package/auth/token-storage.js +28 -30
  8. package/auth/tools.js +45 -76
  9. package/calendar/attendees.js +36 -0
  10. package/calendar/cancel.js +9 -25
  11. package/calendar/create.js +42 -48
  12. package/calendar/decline.js +10 -25
  13. package/calendar/delete.js +10 -25
  14. package/calendar/index.js +20 -37
  15. package/calendar/list.js +4 -16
  16. package/calendar/preview.js +335 -0
  17. package/calendar/update.js +42 -86
  18. package/categories/index.js +59 -264
  19. package/config.js +29 -1
  20. package/contacts/index.js +72 -128
  21. package/email/attachments.js +42 -124
  22. package/email/conversations.js +44 -78
  23. package/email/delta.js +10 -34
  24. package/email/draft.js +140 -96
  25. package/email/export.js +141 -110
  26. package/email/folder-utils.js +3 -2
  27. package/email/headers.js +11 -49
  28. package/email/index.js +85 -109
  29. package/email/list.js +4 -17
  30. package/email/mail-tips.js +86 -57
  31. package/email/mark-as-read.js +13 -49
  32. package/email/mime.js +14 -49
  33. package/email/read.js +16 -50
  34. package/email/search.js +46 -86
  35. package/email/send.js +82 -48
  36. package/folder/create.js +6 -25
  37. package/folder/delete.js +117 -38
  38. package/folder/index.js +17 -16
  39. package/folder/list.js +5 -17
  40. package/folder/move.js +13 -42
  41. package/folder/resolve.js +11 -6
  42. package/folder/stats.js +6 -20
  43. package/index.js +19 -43
  44. package/llms-install.md +22 -4
  45. package/llms.txt +17 -8
  46. package/outlook-auth-server.js +10 -3
  47. package/package.json +4 -1
  48. package/request-handler.js +217 -116
  49. package/rules/create.js +27 -70
  50. package/rules/index.js +30 -92
  51. package/rules/list.js +5 -17
  52. package/rules/rule-builder.js +57 -20
  53. package/rules/update.js +26 -60
  54. package/server.js +37 -0
  55. package/settings/index.js +142 -143
  56. package/tools.js +30 -0
  57. package/utils/field-presets.js +4 -2
  58. package/utils/graph-api.js +65 -22
  59. package/utils/logger.js +251 -0
  60. package/utils/mock-data.js +91 -2
  61. package/utils/read-only.js +59 -0
  62. package/utils/response-formatter.js +54 -15
  63. package/utils/risk-classes.js +324 -0
  64. package/utils/safe-write.js +372 -6
  65. package/utils/safety.js +109 -25
  66. package/utils/server-instructions.js +62 -0
  67. package/utils/tool-error.js +33 -0
@@ -6,6 +6,7 @@
6
6
  */
7
7
  const { callGraphAPI } = require('../utils/graph-api');
8
8
  const { ensureAuthenticated } = require('../auth');
9
+ const { toolError, authRequiredError } = require('../utils/tool-error');
9
10
 
10
11
  /**
11
12
  * All available mail tip types from Microsoft Graph API
@@ -25,17 +26,27 @@ const MAIL_TIP_TYPES = [
25
26
 
26
27
  /**
27
28
  * Format mail tips into readable markdown
29
+ *
30
+ * Graph's mailTips resource reports `mailboxFull`, `deliveryRestricted` and
31
+ * `isModerated` as booleans. The older `mailboxFullStatus`,
32
+ * `deliveryRestriction` and `moderationStatus` shapes are still recognised.
28
33
  * @param {Array} mailTips - Array of mail tip objects from Graph API
29
- * @returns {string} - Formatted markdown output
34
+ * @returns {{formatted: string, warningCount: number, issues: Array<{address: string, type: string}>}}
35
+ * `issues` lists each flagged condition per recipient, with `type` one of
36
+ * outOfOffice, mailboxFull, customTip, deliveryRestricted, moderated,
37
+ * external or externalMembers. Every issue is also a warning in the text,
38
+ * so the text never shows ✓ for a recipient send-email would refuse
30
39
  */
31
40
  function formatMailTips(mailTips) {
32
41
  const lines = [];
42
+ const issues = [];
33
43
  let warningCount = 0;
34
44
 
35
45
  for (const tip of mailTips) {
36
46
  const email = tip.emailAddress?.address || 'Unknown';
37
47
  const tipLines = [];
38
48
  const warnings = [];
49
+ const flag = (type) => issues.push({ address: email, type });
39
50
 
40
51
  // Out-of-office / automatic replies
41
52
  if (
@@ -43,6 +54,7 @@ function formatMailTips(mailTips) {
43
54
  tip.automaticReplies.message.trim() !== ''
44
55
  ) {
45
56
  warnings.push('Out of Office');
57
+ flag('outOfOffice');
46
58
  const reply = tip.automaticReplies;
47
59
  tipLines.push(
48
60
  ` **Out of Office**: ${reply.message.replace(/\n/g, ' ').substring(0, 200)}`
@@ -57,8 +69,9 @@ function formatMailTips(mailTips) {
57
69
  }
58
70
 
59
71
  // Mailbox full
60
- if (tip.mailboxFullStatus) {
72
+ if (tip.mailboxFull === true || tip.mailboxFullStatus === true) {
61
73
  warnings.push('Mailbox Full');
74
+ flag('mailboxFull');
62
75
  tipLines.push(
63
76
  ` **Mailbox Full**: Recipient's mailbox is full — delivery may fail`
64
77
  );
@@ -67,30 +80,38 @@ function formatMailTips(mailTips) {
67
80
  // Custom mail tip (admin-configured)
68
81
  if (tip.customMailTip) {
69
82
  warnings.push('Custom Tip');
83
+ flag('customTip');
70
84
  tipLines.push(` **Notice**: ${tip.customMailTip}`);
71
85
  }
72
86
 
73
87
  // 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
- }
88
+ if (
89
+ tip.deliveryRestricted === true ||
90
+ tip.deliveryRestriction?.isDeliveryRestricted
91
+ ) {
92
+ warnings.push('Delivery Restricted');
93
+ flag('deliveryRestricted');
94
+ tipLines.push(
95
+ ` **Delivery Restricted**: ${tip.deliveryRestriction?.message || 'Cannot deliver to this recipient'}`
96
+ );
82
97
  }
83
98
 
84
99
  // Moderation status
85
- if (tip.moderationStatus && tip.moderationStatus !== 'notModerated') {
100
+ if (
101
+ tip.isModerated === true ||
102
+ (tip.moderationStatus && tip.moderationStatus !== 'notModerated')
103
+ ) {
86
104
  warnings.push('Moderated');
105
+ flag('moderated');
87
106
  tipLines.push(
88
107
  ` **Moderated**: Messages to this recipient require approval`
89
108
  );
90
109
  }
91
110
 
92
- // Recipient scope (external)
93
- if (tip.recipientScope === 'external') {
111
+ // Recipient scope: external, externalPartner or externalNonPartner
112
+ if (/^external/i.test(tip.recipientScope || '')) {
113
+ warnings.push('External');
114
+ flag('external');
94
115
  tipLines.push(` **External**: Recipient is outside your organisation`);
95
116
  }
96
117
 
@@ -101,12 +122,27 @@ function formatMailTips(mailTips) {
101
122
  }
102
123
 
103
124
  // Group member counts
104
- if (tip.totalMemberCount > 0) {
125
+ if (tip.externalMemberCount > 0) {
126
+ warnings.push('External Members');
127
+ flag('externalMembers');
128
+ }
129
+ if (tip.totalMemberCount > 0 || tip.externalMemberCount > 0) {
105
130
  tipLines.push(
106
131
  ` *Group members*: ${tip.totalMemberCount} total (${tip.externalMemberCount || 0} external)`
107
132
  );
108
133
  }
109
134
 
135
+ // Graph couldn't check this recipient, so its other fields say nothing.
136
+ if (tip.error) {
137
+ warnings.push('Not Checked');
138
+ const code = /^[A-Za-z0-9]{1,64}$/.test(tip.error.code || '')
139
+ ? ` (${tip.error.code})`
140
+ : '';
141
+ tipLines.push(
142
+ ` **Not checked**: Graph returned an error for this recipient${code}`
143
+ );
144
+ }
145
+
110
146
  // Build section for this recipient
111
147
  const statusIcon = warnings.length > 0 ? '⚠' : '✓';
112
148
  lines.push(`### ${statusIcon} ${email}`);
@@ -124,7 +160,29 @@ function formatMailTips(mailTips) {
124
160
  lines.push('');
125
161
  }
126
162
 
127
- return { formatted: lines.join('\n'), warningCount };
163
+ return { formatted: lines.join('\n'), warningCount, issues };
164
+ }
165
+
166
+ /**
167
+ * Whether Graph returned anything actionable for a recipient: any field a
168
+ * personal Outlook.com account leaves out. A recipientScope of `none` (or
169
+ * none at all) says nothing.
170
+ * @param {object} tip - One mailTips object from Graph
171
+ * @returns {boolean}
172
+ */
173
+ function hasTipContent(tip) {
174
+ return Boolean(
175
+ tip.error ||
176
+ tip.mailboxFull ||
177
+ tip.deliveryRestricted ||
178
+ tip.isModerated ||
179
+ tip.automaticReplies?.message ||
180
+ tip.maxMessageSize ||
181
+ tip.totalMemberCount ||
182
+ tip.externalMemberCount ||
183
+ tip.customMailTip ||
184
+ (tip.recipientScope && tip.recipientScope !== 'none')
185
+ );
128
186
  }
129
187
 
130
188
  /**
@@ -136,14 +194,7 @@ async function handleGetMailTips(args) {
136
194
  const { recipients, tipTypes } = args;
137
195
 
138
196
  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
- };
197
+ return toolError('At least one recipient email address is required.');
147
198
  }
148
199
 
149
200
  // Normalise recipients to a clean array of email strings
@@ -171,14 +222,7 @@ async function handleGetMailTips(args) {
171
222
  recipientList = recipientList.filter((e) => e.length > 0);
172
223
 
173
224
  if (recipientList.length === 0) {
174
- return {
175
- content: [
176
- {
177
- type: 'text',
178
- text: 'At least one valid recipient email address is required.',
179
- },
180
- ],
181
- };
225
+ return toolError('At least one valid recipient email address is required.');
182
226
  }
183
227
 
184
228
  try {
@@ -206,28 +250,23 @@ async function handleGetMailTips(args) {
206
250
  text: 'No mail tips returned. Mail Tips is M365-only — personal Outlook.com accounts return empty responses, so recipient validation is unavailable on this account.',
207
251
  },
208
252
  ],
253
+ _meta: {
254
+ recipientCount: 0,
255
+ warningCount: 0,
256
+ allEmpty: true,
257
+ issues: [],
258
+ },
209
259
  };
210
260
  }
211
261
 
212
- const { formatted, warningCount } = formatMailTips(mailTips);
262
+ const { formatted, warningCount, issues } = formatMailTips(mailTips);
213
263
 
214
264
  // F-23: Detect a "fully empty" tips response — every recipient
215
265
  // returned with no actionable fields. Personal Outlook.com
216
266
  // accounts surface this as a successful empty response rather
217
267
  // than a feature-unsupported error, leading to false confidence
218
268
  // when callers see "No issues detected".
219
- const allEmpty = mailTips.every((tip) => {
220
- const hasContent =
221
- tip.recipientNotFound ||
222
- tip.mailboxFull ||
223
- tip.deliveryRestricted ||
224
- tip.isModerated ||
225
- tip.automaticReplies?.message ||
226
- tip.maxMessageSize ||
227
- tip.totalMemberCount ||
228
- tip.customMailTip;
229
- return !hasContent;
230
- });
269
+ const allEmpty = mailTips.every((tip) => !hasTipContent(tip));
231
270
 
232
271
  let header = `# Mail Tips\n\n`;
233
272
  header += `**Recipients checked**: ${mailTips.length}\n`;
@@ -244,24 +283,14 @@ async function handleGetMailTips(args) {
244
283
  recipientCount: mailTips.length,
245
284
  warningCount,
246
285
  allEmpty,
286
+ issues,
247
287
  },
248
288
  };
249
289
  } catch (error) {
250
290
  if (error.message === 'Authentication required') {
251
- return {
252
- content: [
253
- {
254
- type: 'text',
255
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
256
- },
257
- ],
258
- };
291
+ return authRequiredError();
259
292
  }
260
- return {
261
- content: [
262
- { type: 'text', text: `Error getting mail tips: ${error.message}` },
263
- ],
264
- };
293
+ return toolError(`Error getting mail tips: ${error.message}`);
265
294
  }
266
295
  }
267
296
 
@@ -5,6 +5,8 @@ const _config = require('../config'); // Reserved for future use
5
5
  const { callGraphAPI } = require('../utils/graph-api');
6
6
  const { ensureAuthenticated } = require('../auth');
7
7
  const { buildMailboxPrefix } = require('../utils/mailbox');
8
+ const { toolError, authRequiredError } = require('../utils/tool-error');
9
+ const { log } = require('../utils/logger');
8
10
 
9
11
  /**
10
12
  * Mark email as read handler
@@ -17,14 +19,7 @@ async function handleMarkAsRead(args) {
17
19
  const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
18
20
 
19
21
  if (!emailId) {
20
- return {
21
- content: [
22
- {
23
- type: 'text',
24
- text: 'Email ID is required.',
25
- },
26
- ],
27
- };
22
+ return toolError('Email ID is required.');
28
23
  }
29
24
 
30
25
  try {
@@ -56,60 +51,29 @@ async function handleMarkAsRead(args) {
56
51
  ],
57
52
  };
58
53
  } catch (error) {
59
- console.error(
54
+ log.debug(
60
55
  `Error marking email as ${isRead ? 'read' : 'unread'}: ${error.message}`
61
56
  );
62
57
 
63
58
  // Improved error handling with more specific messages
64
59
  if (error.message.includes("doesn't belong to the targeted mailbox")) {
65
- return {
66
- content: [
67
- {
68
- type: 'text',
69
- text: `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`,
70
- },
71
- ],
72
- };
60
+ return toolError(
61
+ `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`
62
+ );
73
63
  } else if (error.message.includes('UNAUTHORIZED')) {
74
- return {
75
- content: [
76
- {
77
- type: 'text',
78
- text: 'Authentication failed. Please re-authenticate and try again.',
79
- },
80
- ],
81
- };
64
+ return authRequiredError();
82
65
  } else {
83
- return {
84
- content: [
85
- {
86
- type: 'text',
87
- text: `Failed to mark email as ${isRead ? 'read' : 'unread'}: ${error.message}`,
88
- },
89
- ],
90
- };
66
+ return toolError(
67
+ `Failed to mark email as ${isRead ? 'read' : 'unread'}: ${error.message}`
68
+ );
91
69
  }
92
70
  }
93
71
  } catch (error) {
94
72
  if (error.message === 'Authentication required') {
95
- return {
96
- content: [
97
- {
98
- type: 'text',
99
- text: "Authentication required. Please use the 'authenticate' tool first.",
100
- },
101
- ],
102
- };
73
+ return authRequiredError();
103
74
  }
104
75
 
105
- return {
106
- content: [
107
- {
108
- type: 'text',
109
- text: `Error accessing email: ${error.message}`,
110
- },
111
- ],
112
- };
76
+ return toolError(`Error accessing email: ${error.message}`);
113
77
  }
114
78
  }
115
79
 
package/email/mime.js CHANGED
@@ -7,6 +7,8 @@
7
7
  const { callGraphAPIRaw } = require('../utils/graph-api');
8
8
  const { ensureAuthenticated } = require('../auth');
9
9
  const { buildMailboxPrefix } = require('../utils/mailbox');
10
+ const { toolError, authRequiredError } = require('../utils/tool-error');
11
+ const { log } = require('../utils/logger');
10
12
 
11
13
  /**
12
14
  * Parse MIME headers from raw content
@@ -98,14 +100,7 @@ async function handleGetMimeContent(args) {
98
100
  const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
99
101
 
100
102
  if (!emailId) {
101
- return {
102
- content: [
103
- {
104
- type: 'text',
105
- text: 'Email ID is required.',
106
- },
107
- ],
108
- };
103
+ return toolError('Email ID is required.');
109
104
  }
110
105
 
111
106
  try {
@@ -117,14 +112,9 @@ async function handleGetMimeContent(args) {
117
112
  const mimeContent = await callGraphAPIRaw(accessToken, emailId, prefix);
118
113
 
119
114
  if (!mimeContent) {
120
- return {
121
- content: [
122
- {
123
- type: 'text',
124
- text: `Failed to retrieve MIME content for email ${emailId}.`,
125
- },
126
- ],
127
- };
115
+ return toolError(
116
+ `Failed to retrieve MIME content for email ${emailId}.`
117
+ );
128
118
  }
129
119
 
130
120
  const stats = getMimeStats(mimeContent);
@@ -174,6 +164,7 @@ async function handleGetMimeContent(args) {
174
164
  truncated: true,
175
165
  maxSizeExceeded: true,
176
166
  },
167
+ isError: true,
177
168
  };
178
169
  }
179
170
 
@@ -238,48 +229,22 @@ async function handleGetMimeContent(args) {
238
229
  },
239
230
  };
240
231
  } catch (error) {
241
- console.error(`Error getting MIME content: ${error.message}`);
232
+ log.debug(`Error getting MIME content: ${error.message}`);
242
233
 
243
234
  if (error.message.includes("doesn't belong to the targeted mailbox")) {
244
- return {
245
- content: [
246
- {
247
- type: 'text',
248
- text: `The email ID seems invalid or doesn't belong to your mailbox.`,
249
- },
250
- ],
251
- };
235
+ return toolError(
236
+ `The email ID seems invalid or doesn't belong to your mailbox.`
237
+ );
252
238
  }
253
239
 
254
- return {
255
- content: [
256
- {
257
- type: 'text',
258
- text: `Failed to get MIME content: ${error.message}`,
259
- },
260
- ],
261
- };
240
+ return toolError(`Failed to get MIME content: ${error.message}`);
262
241
  }
263
242
  } catch (error) {
264
243
  if (error.message === 'Authentication required') {
265
- return {
266
- content: [
267
- {
268
- type: 'text',
269
- text: "Authentication required. Please use the 'authenticate' tool first.",
270
- },
271
- ],
272
- };
244
+ return authRequiredError();
273
245
  }
274
246
 
275
- return {
276
- content: [
277
- {
278
- type: 'text',
279
- text: `Error accessing email: ${error.message}`,
280
- },
281
- ],
282
- };
247
+ return toolError(`Error accessing email: ${error.message}`);
283
248
  }
284
249
  }
285
250
 
package/email/read.js CHANGED
@@ -9,9 +9,12 @@ const { ensureAuthenticated } = require('../auth');
9
9
  const {
10
10
  formatEmailContent,
11
11
  VERBOSITY,
12
+ DEFAULT_LIMITS,
12
13
  } = require('../utils/response-formatter');
13
14
  const { getEmailFields } = require('../utils/field-presets');
14
15
  const { buildMailboxPrefix } = require('../utils/mailbox');
16
+ const { toolError, authRequiredError } = require('../utils/tool-error');
17
+ const { log } = require('../utils/logger');
15
18
 
16
19
  /**
17
20
  * Get field preset based on verbosity and options
@@ -48,17 +51,11 @@ async function handleReadEmail(args) {
48
51
  const includeHeaders = args.includeHeaders || false;
49
52
  // Message IDs are mailbox-scoped: an ID issued by a shared/delegated mailbox
50
53
  // is not resolvable under /me. Route to /users/{mailbox} when supplied.
51
- const prefix = buildMailboxPrefix(args.sharedMailbox || args.email || null);
54
+ const sharedMailbox = args.sharedMailbox || args.email || null;
55
+ const prefix = buildMailboxPrefix(sharedMailbox);
52
56
 
53
57
  if (!emailId) {
54
- return {
55
- content: [
56
- {
57
- type: 'text',
58
- text: 'Email ID is required.',
59
- },
60
- ],
61
- };
58
+ return toolError('Email ID is required.');
62
59
  }
63
60
 
64
61
  try {
@@ -85,20 +82,15 @@ async function handleReadEmail(args) {
85
82
  );
86
83
 
87
84
  if (!email) {
88
- return {
89
- content: [
90
- {
91
- type: 'text',
92
- text: `Email with ID ${emailId} not found.`,
93
- },
94
- ],
95
- };
85
+ return toolError(`Email with ID ${emailId} not found.`);
96
86
  }
97
87
 
98
88
  // Format using shared formatter (returns Markdown)
99
89
  const formattedOutput = formatEmailContent(email, verbosity, {
100
90
  includeHeaders: includeHeaders,
101
91
  includeAllHeaders: false, // Only important headers by default
92
+ sharedMailbox,
93
+ maxFullBodyChars: DEFAULT_LIMITS.maxFullBodyChars,
102
94
  });
103
95
 
104
96
  return {
@@ -116,49 +108,23 @@ async function handleReadEmail(args) {
116
108
  },
117
109
  };
118
110
  } catch (error) {
119
- console.error(`Error reading email: ${error.message}`);
111
+ log.debug(`Error reading email: ${error.message}`);
120
112
 
121
113
  // Improved error handling with more specific messages
122
114
  if (error.message.includes("doesn't belong to the targeted mailbox")) {
123
- return {
124
- content: [
125
- {
126
- type: 'text',
127
- text: `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`,
128
- },
129
- ],
130
- };
115
+ return toolError(
116
+ `The email ID seems invalid or doesn't belong to your mailbox. Please try with a different email ID.`
117
+ );
131
118
  } else {
132
- return {
133
- content: [
134
- {
135
- type: 'text',
136
- text: `Failed to read email: ${error.message}`,
137
- },
138
- ],
139
- };
119
+ return toolError(`Failed to read email: ${error.message}`);
140
120
  }
141
121
  }
142
122
  } catch (error) {
143
123
  if (error.message === 'Authentication required') {
144
- return {
145
- content: [
146
- {
147
- type: 'text',
148
- text: "Authentication required. Please use the 'authenticate' tool first.",
149
- },
150
- ],
151
- };
124
+ return authRequiredError();
152
125
  }
153
126
 
154
- return {
155
- content: [
156
- {
157
- type: 'text',
158
- text: `Error accessing email: ${error.message}`,
159
- },
160
- ],
161
- };
127
+ return toolError(`Error accessing email: ${error.message}`);
162
128
  }
163
129
  }
164
130