@littlebearapps/outlook-assistant 3.12.1 → 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 (69) hide show
  1. package/.env.example +27 -3
  2. package/README.md +108 -33
  3. package/advanced/index.js +44 -174
  4. package/auth/auth-errors.js +23 -1
  5. package/auth/client-config.js +142 -0
  6. package/auth/index.js +4 -2
  7. package/auth/oauth-server.js +12 -2
  8. package/auth/token-manager.js +7 -3
  9. package/auth/token-storage.js +46 -33
  10. package/auth/tools.js +223 -93
  11. package/calendar/attendees.js +36 -0
  12. package/calendar/cancel.js +9 -25
  13. package/calendar/create.js +42 -48
  14. package/calendar/decline.js +10 -25
  15. package/calendar/delete.js +10 -25
  16. package/calendar/index.js +20 -37
  17. package/calendar/list.js +4 -16
  18. package/calendar/preview.js +335 -0
  19. package/calendar/update.js +42 -86
  20. package/categories/index.js +59 -264
  21. package/config.js +36 -2
  22. package/contacts/index.js +72 -128
  23. package/email/attachments.js +42 -124
  24. package/email/conversations.js +44 -78
  25. package/email/delta.js +10 -34
  26. package/email/draft.js +140 -96
  27. package/email/export.js +141 -110
  28. package/email/folder-utils.js +3 -2
  29. package/email/headers.js +11 -49
  30. package/email/index.js +85 -109
  31. package/email/list.js +4 -17
  32. package/email/mail-tips.js +86 -57
  33. package/email/mark-as-read.js +13 -49
  34. package/email/mime.js +14 -49
  35. package/email/read.js +16 -50
  36. package/email/search.js +46 -86
  37. package/email/send.js +82 -48
  38. package/folder/create.js +6 -25
  39. package/folder/delete.js +117 -38
  40. package/folder/index.js +17 -16
  41. package/folder/list.js +5 -17
  42. package/folder/move.js +13 -42
  43. package/folder/resolve.js +11 -6
  44. package/folder/stats.js +6 -20
  45. package/index.js +23 -45
  46. package/llms-install.md +31 -7
  47. package/llms.txt +19 -10
  48. package/outlook-auth-server.js +10 -3
  49. package/package.json +6 -2
  50. package/request-handler.js +217 -116
  51. package/rules/create.js +27 -70
  52. package/rules/index.js +30 -92
  53. package/rules/list.js +5 -17
  54. package/rules/rule-builder.js +57 -20
  55. package/rules/update.js +26 -60
  56. package/server.js +37 -0
  57. package/settings/index.js +142 -143
  58. package/tools.js +30 -0
  59. package/utils/field-presets.js +4 -2
  60. package/utils/graph-api.js +65 -22
  61. package/utils/logger.js +251 -0
  62. package/utils/mock-data.js +91 -2
  63. package/utils/read-only.js +59 -0
  64. package/utils/response-formatter.js +54 -15
  65. package/utils/risk-classes.js +324 -0
  66. package/utils/safe-write.js +372 -6
  67. package/utils/safety.js +109 -25
  68. package/utils/server-instructions.js +62 -0
  69. package/utils/tool-error.js +33 -0
@@ -6,6 +6,8 @@
6
6
  const { callGraphAPI } = require('../utils/graph-api');
7
7
  const { ensureAuthenticated } = require('../auth');
8
8
  const { buildMailboxPrefix } = require('../utils/mailbox');
9
+ const { toolMetadata } = require('../utils/risk-classes');
10
+ const { toolError, authRequiredError } = require('../utils/tool-error');
9
11
 
10
12
  // Category color presets (Outlook uses these names)
11
13
  const CATEGORY_COLORS = [
@@ -137,23 +139,9 @@ async function handleListCategories(args) {
137
139
  };
138
140
  } catch (error) {
139
141
  if (error.message === 'Authentication required') {
140
- return {
141
- content: [
142
- {
143
- type: 'text',
144
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
145
- },
146
- ],
147
- };
142
+ return authRequiredError();
148
143
  }
149
- return {
150
- content: [
151
- {
152
- type: 'text',
153
- text: `Error listing categories: ${error.message}`,
154
- },
155
- ],
156
- };
144
+ return toolError(`Error listing categories: ${error.message}`);
157
145
  }
158
146
  }
159
147
 
@@ -164,30 +152,18 @@ async function handleCreateCategory(args) {
164
152
  const { displayName, color } = args;
165
153
 
166
154
  if (!displayName) {
167
- return {
168
- content: [
169
- {
170
- type: 'text',
171
- text: 'Category name (displayName) is required.',
172
- },
173
- ],
174
- };
155
+ return toolError('Category name (displayName) is required.');
175
156
  }
176
157
 
177
158
  // Validate color if provided
178
159
  if (color && !CATEGORY_COLORS.includes(color)) {
179
- return {
180
- content: [
181
- {
182
- type: 'text',
183
- text: `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}\n\nColor names: ${Object.entries(
184
- COLOR_NAMES
185
- )
186
- .map(([k, v]) => `${k}=${v}`)
187
- .join(', ')}`,
188
- },
189
- ],
190
- };
160
+ return toolError(
161
+ `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}\n\nColor names: ${Object.entries(
162
+ COLOR_NAMES
163
+ )
164
+ .map(([k, v]) => `${k}=${v}`)
165
+ .join(', ')}`
166
+ );
191
167
  }
192
168
 
193
169
  try {
@@ -220,35 +196,14 @@ async function handleCreateCategory(args) {
220
196
  };
221
197
  } catch (error) {
222
198
  if (error.message === 'Authentication required') {
223
- return {
224
- content: [
225
- {
226
- type: 'text',
227
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
228
- },
229
- ],
230
- };
199
+ return authRequiredError();
231
200
  }
232
201
 
233
202
  if (error.message.includes('already exists')) {
234
- return {
235
- content: [
236
- {
237
- type: 'text',
238
- text: `A category named "${displayName}" already exists.`,
239
- },
240
- ],
241
- };
203
+ return toolError(`A category named "${displayName}" already exists.`);
242
204
  }
243
205
 
244
- return {
245
- content: [
246
- {
247
- type: 'text',
248
- text: `Error creating category: ${error.message}`,
249
- },
250
- ],
251
- };
206
+ return toolError(`Error creating category: ${error.message}`);
252
207
  }
253
208
  }
254
209
 
@@ -261,37 +216,20 @@ async function handleUpdateCategory(args) {
261
216
  const { displayName, color } = args;
262
217
 
263
218
  if (!id) {
264
- return {
265
- content: [
266
- {
267
- type: 'text',
268
- text: 'Category ID is required. Use manage-category with action=list to find category IDs.',
269
- },
270
- ],
271
- };
219
+ return toolError(
220
+ 'Category ID is required. Use manage-category with action=list to find category IDs.'
221
+ );
272
222
  }
273
223
 
274
224
  if (!displayName && !color) {
275
- return {
276
- content: [
277
- {
278
- type: 'text',
279
- text: 'At least one of displayName or color must be provided.',
280
- },
281
- ],
282
- };
225
+ return toolError('At least one of displayName or color must be provided.');
283
226
  }
284
227
 
285
228
  // Validate color if provided
286
229
  if (color && !CATEGORY_COLORS.includes(color)) {
287
- return {
288
- content: [
289
- {
290
- type: 'text',
291
- text: `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}`,
292
- },
293
- ],
294
- };
230
+ return toolError(
231
+ `Invalid color. Valid options: ${CATEGORY_COLORS.join(', ')}`
232
+ );
295
233
  }
296
234
 
297
235
  try {
@@ -343,23 +281,9 @@ async function handleUpdateCategory(args) {
343
281
  };
344
282
  } catch (error) {
345
283
  if (error.message === 'Authentication required') {
346
- return {
347
- content: [
348
- {
349
- type: 'text',
350
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
351
- },
352
- ],
353
- };
284
+ return authRequiredError();
354
285
  }
355
- return {
356
- content: [
357
- {
358
- type: 'text',
359
- text: `Error updating category: ${error.message}`,
360
- },
361
- ],
362
- };
286
+ return toolError(`Error updating category: ${error.message}`);
363
287
  }
364
288
  }
365
289
 
@@ -371,14 +295,9 @@ async function handleDeleteCategory(args) {
371
295
  const id = args.id || args.categoryId;
372
296
 
373
297
  if (!id) {
374
- return {
375
- content: [
376
- {
377
- type: 'text',
378
- text: 'Category ID is required. Use manage-category with action=list to find category IDs.',
379
- },
380
- ],
381
- };
298
+ return toolError(
299
+ 'Category ID is required. Use manage-category with action=list to find category IDs.'
300
+ );
382
301
  }
383
302
 
384
303
  try {
@@ -400,35 +319,16 @@ async function handleDeleteCategory(args) {
400
319
  };
401
320
  } catch (error) {
402
321
  if (error.message === 'Authentication required') {
403
- return {
404
- content: [
405
- {
406
- type: 'text',
407
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
408
- },
409
- ],
410
- };
322
+ return authRequiredError();
411
323
  }
412
324
 
413
325
  if (error.message.includes('not found') || error.message.includes('404')) {
414
- return {
415
- content: [
416
- {
417
- type: 'text',
418
- text: `Category not found. Use manage-category with action=list to see available categories.`,
419
- },
420
- ],
421
- };
326
+ return toolError(
327
+ `Category not found. Use manage-category with action=list to see available categories.`
328
+ );
422
329
  }
423
330
 
424
- return {
425
- content: [
426
- {
427
- type: 'text',
428
- text: `Error deleting category: ${error.message}`,
429
- },
430
- ],
431
- };
331
+ return toolError(`Error deleting category: ${error.message}`);
432
332
  }
433
333
  }
434
334
 
@@ -443,39 +343,22 @@ async function handleApplyCategory(args) {
443
343
  const ids = messageIds || (messageId ? [messageId] : []);
444
344
 
445
345
  if (ids.length === 0) {
446
- return {
447
- content: [
448
- {
449
- type: 'text',
450
- text: 'Message ID (messageId) or IDs (messageIds) required.',
451
- },
452
- ],
453
- };
346
+ return toolError('Message ID (messageId) or IDs (messageIds) required.');
454
347
  }
455
348
 
456
349
  const applyAction = action || 'set'; // 'set', 'add', 'remove'
457
350
 
458
351
  if (!categories || !Array.isArray(categories)) {
459
- return {
460
- content: [
461
- {
462
- type: 'text',
463
- text: 'Categories array is required. Provide category display names.',
464
- },
465
- ],
466
- };
352
+ return toolError(
353
+ 'Categories array is required. Provide category display names.'
354
+ );
467
355
  }
468
356
 
469
357
  // Empty array is only valid for action=set (clears all categories)
470
358
  if (categories.length === 0 && applyAction !== 'set') {
471
- return {
472
- content: [
473
- {
474
- type: 'text',
475
- text: 'Categories array cannot be empty for add/remove. Use action=set with an empty array to clear all categories.',
476
- },
477
- ],
478
- };
359
+ return toolError(
360
+ 'Categories array cannot be empty for add/remove. Use action=set with an empty array to clear all categories.'
361
+ );
479
362
  }
480
363
 
481
364
  try {
@@ -562,23 +445,9 @@ async function handleApplyCategory(args) {
562
445
  };
563
446
  } catch (error) {
564
447
  if (error.message === 'Authentication required') {
565
- return {
566
- content: [
567
- {
568
- type: 'text',
569
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
570
- },
571
- ],
572
- };
448
+ return authRequiredError();
573
449
  }
574
- return {
575
- content: [
576
- {
577
- type: 'text',
578
- text: `Error applying categories: ${error.message}`,
579
- },
580
- ],
581
- };
450
+ return toolError(`Error applying categories: ${error.message}`);
582
451
  }
583
452
  }
584
453
 
@@ -664,23 +533,9 @@ async function handleGetFocusedInboxOverrides(args) {
664
533
  };
665
534
  } catch (error) {
666
535
  if (error.message === 'Authentication required') {
667
- return {
668
- content: [
669
- {
670
- type: 'text',
671
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
672
- },
673
- ],
674
- };
536
+ return authRequiredError();
675
537
  }
676
- return {
677
- content: [
678
- {
679
- type: 'text',
680
- text: `Error getting Focused Inbox overrides: ${error.message}`,
681
- },
682
- ],
683
- };
538
+ return toolError(`Error getting Focused Inbox overrides: ${error.message}`);
684
539
  }
685
540
  }
686
541
 
@@ -693,26 +548,12 @@ async function handleSetFocusedInboxOverride(args) {
693
548
  const overrideAction = args.action;
694
549
 
695
550
  if (!emailAddress) {
696
- return {
697
- content: [
698
- {
699
- type: 'text',
700
- text: 'Email address is required.',
701
- },
702
- ],
703
- };
551
+ return toolError('Email address is required.');
704
552
  }
705
553
 
706
554
  const classification = classifyAs || 'focused';
707
555
  if (!['focused', 'other'].includes(classification)) {
708
- return {
709
- content: [
710
- {
711
- type: 'text',
712
- text: "classifyAs must be 'focused' or 'other'.",
713
- },
714
- ],
715
- };
556
+ return toolError("classifyAs must be 'focused' or 'other'.");
716
557
  }
717
558
 
718
559
  try {
@@ -734,14 +575,7 @@ async function handleSetFocusedInboxOverride(args) {
734
575
  // Handle delete action
735
576
  if (overrideAction === 'delete') {
736
577
  if (!existing) {
737
- return {
738
- content: [
739
- {
740
- type: 'text',
741
- text: `No override found for ${emailAddress}.`,
742
- },
743
- ],
744
- };
578
+ return toolError(`No override found for ${emailAddress}.`);
745
579
  }
746
580
 
747
581
  await callGraphAPI(
@@ -806,23 +640,9 @@ async function handleSetFocusedInboxOverride(args) {
806
640
  };
807
641
  } catch (error) {
808
642
  if (error.message === 'Authentication required') {
809
- return {
810
- content: [
811
- {
812
- type: 'text',
813
- text: "Authentication required. Please use the 'auth' tool with action=authenticate first.",
814
- },
815
- ],
816
- };
643
+ return authRequiredError();
817
644
  }
818
- return {
819
- content: [
820
- {
821
- type: 'text',
822
- text: `Error setting Focused Inbox override: ${error.message}`,
823
- },
824
- ],
825
- };
645
+ return toolError(`Error setting Focused Inbox override: ${error.message}`);
826
646
  }
827
647
  }
828
648
 
@@ -832,12 +652,7 @@ const categoriesTools = [
832
652
  name: 'manage-category',
833
653
  description:
834
654
  "Manage the user's master category list (the colour-coded labels available across mail/calendar/contacts). action=`list` (default) returns categories with id/displayName/color. action=`create` adds a new category — `displayName` required, `color` optional (preset0-preset24, e.g. preset0=Red, preset7=Blue). action=`update` (alias `set` — deprecated) changes name/colour by `id`. action=`delete` removes a category — this does NOT untag messages already labelled with it; existing messages retain the orphaned label until manually cleaned. Use `apply-category` to tag/untag specific messages.",
835
- annotations: {
836
- title: 'Master Categories',
837
- readOnlyHint: false,
838
- destructiveHint: false,
839
- openWorldHint: false,
840
- },
655
+ ...toolMetadata('manage-category', 'Master Categories'),
841
656
  inputSchema: {
842
657
  type: 'object',
843
658
  properties: {
@@ -892,27 +707,17 @@ const categoriesTools = [
892
707
  case 'list':
893
708
  return handleListCategories(args);
894
709
  default:
895
- return {
896
- content: [
897
- {
898
- type: 'text',
899
- text: `Unknown action '${action}'. Valid actions: list, create, update, delete.`,
900
- },
901
- ],
902
- };
710
+ return toolError(
711
+ `Unknown action '${action}'. Valid actions: list, create, update, delete.`
712
+ );
903
713
  }
904
714
  },
905
715
  },
906
716
  {
907
717
  name: 'apply-category',
908
718
  description:
909
- "Tag or untag email messages with master categories (those created via `manage-category`). action=`set` (default) replaces the message's category set with the supplied `categories` array. action=`add` appends categories to whatever's already on the message. action=`remove` removes only the named categories, leaving the rest. Accepts either `messageId` (single) or `messageIds` (batch via Graph `$batch`). `categories` are matched by display name — names must already exist in the target mailbox's master list. For your own mailbox, create them via `manage-category` first; for a shared mailbox, the names must already exist there (`manage-category` only manages the signed-in account's master list). Pass `sharedMailbox` (or alias `email`) to categorise messages in a shared/delegated mailbox instead of the signed-in account (requires Mail.ReadWrite.Shared + delegate access). Returns per-message confirmation.",
910
- annotations: {
911
- title: 'Apply Categories',
912
- readOnlyHint: false,
913
- destructiveHint: false,
914
- openWorldHint: false,
915
- },
719
+ "Tag or untag email messages with master categories (those created via `manage-category`). action=`set` (default) replaces the message's category set with the supplied `categories` array. action=`add` appends categories to whatever's already on the message. action=`remove` removes only the named categories, leaving the rest. Accepts either `messageId` (single) or `messageIds` (batch via Graph `$batch`). `categories` are matched by display name — names must already exist in the target mailbox's master list. For your own mailbox, create them via `manage-category` first; for a shared mailbox, the names must already exist there (`manage-category` only manages the signed-in account's master list). Pass `sharedMailbox` (or alias `email`) to categorise messages in a shared/delegated mailbox (default: the signed-in account; requires Mail.ReadWrite.Shared + delegate access). Returns per-message confirmation.",
720
+ ...toolMetadata('apply-category', 'Apply Categories'),
916
721
  inputSchema: {
917
722
  type: 'object',
918
723
  properties: {
@@ -939,7 +744,7 @@ const categoriesTools = [
939
744
  sharedMailbox: {
940
745
  type: 'string',
941
746
  description:
942
- 'Email address of a shared/delegated mailbox whose messages to categorise instead of the signed-in account. Requires delegate access + Mail.ReadWrite.Shared. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
747
+ 'Email address of the shared/delegated mailbox whose messages to categorise (default: the signed-in account). Requires delegate access + Mail.ReadWrite.Shared. Work/school only; needs the server opt-in setting OUTLOOK_SHARED_MAILBOX (otherwise the call is refused with setup guidance).',
943
748
  },
944
749
  email: {
945
750
  type: 'string',
@@ -955,12 +760,7 @@ const categoriesTools = [
955
760
  name: 'manage-focused-inbox',
956
761
  description:
957
762
  'Manage Focused Inbox sender overrides — explicit rules that force messages from a given sender into Focused or Other regardless of the ML classifier. action=`list` (default) returns existing overrides with id/sender/classifyAs. action=`set` creates or updates an override for `emailAddress` (optional `name`), routing future mail to `focused` (default) or `other`. action=`delete` removes the override for `emailAddress`. Note: this only works on accounts that have Focused Inbox enabled — personal Outlook.com accounts without it return an empty list.',
958
- annotations: {
959
- title: 'Focused Inbox',
960
- readOnlyHint: false,
961
- destructiveHint: false,
962
- openWorldHint: false,
963
- },
763
+ ...toolMetadata('manage-focused-inbox', 'Focused Inbox'),
964
764
  inputSchema: {
965
765
  type: 'object',
966
766
  properties: {
@@ -1004,14 +804,9 @@ const categoriesTools = [
1004
804
  case 'list':
1005
805
  return handleGetFocusedInboxOverrides(args);
1006
806
  default:
1007
- return {
1008
- content: [
1009
- {
1010
- type: 'text',
1011
- text: `Unknown action '${action}'. Valid actions: list, set, delete.`,
1012
- },
1013
- ],
1014
- };
807
+ return toolError(
808
+ `Unknown action '${action}'. Valid actions: list, set, delete.`
809
+ );
1015
810
  }
1016
811
  },
1017
812
  },
package/config.js CHANGED
@@ -14,6 +14,8 @@ const {
14
14
  getFolderFields,
15
15
  } = require('./utils/field-presets');
16
16
  const { VERBOSITY, DEFAULT_LIMITS } = require('./utils/response-formatter');
17
+ const { resolveClientId } = require('./auth/client-config');
18
+ const { redact } = require('./utils/logger');
17
19
 
18
20
  // Ensure we have a home directory path — never fall back to /tmp (world-readable)
19
21
  const homeDir = process.env.HOME || process.env.USERPROFILE || os.homedir();
@@ -89,7 +91,8 @@ function parseSharedMailboxMode(raw) {
89
91
  if (['true', 'readwrite', '1'].includes(value)) return 'readwrite';
90
92
  if (value && !['false', '0', 'off', 'no'].includes(value)) {
91
93
  console.warn(
92
- `[outlook-assistant] OUTLOOK_SHARED_MAILBOX="${raw}" is not a recognised value. ` +
94
+ // redact(): people sometimes put the shared mailbox's address here.
95
+ `[outlook-assistant] OUTLOOK_SHARED_MAILBOX="${redact(raw)}" is not a recognised value. ` +
93
96
  'Expected read, true/readwrite/1, or unset. Shared-mailbox support stays off.'
94
97
  );
95
98
  }
@@ -123,6 +126,27 @@ const BASE_SCOPES = [
123
126
  // 'Place.Read.All', // find-meeting-rooms tool
124
127
  ];
125
128
 
129
+ /**
130
+ * Parse OUTLOOK_READ_ONLY. On: true/1/yes/on; off: unset, empty,
131
+ * false/0/no/off (any case). Anything else fails closed (read-only on, with
132
+ * a warning), because whoever set the variable meant to restrict the server.
133
+ * @param {string|undefined} raw
134
+ * @returns {boolean}
135
+ */
136
+ function parseReadOnly(raw) {
137
+ const value = String(raw ?? '')
138
+ .trim()
139
+ .toLowerCase();
140
+ if (['true', '1', 'yes', 'on'].includes(value)) return true;
141
+ if (['', 'false', '0', 'no', 'off'].includes(value)) return false;
142
+ // Redacted and capped: the value is echoed only to spot a typo.
143
+ console.warn(
144
+ `[outlook-assistant] OUTLOOK_READ_ONLY="${redact(String(raw).slice(0, 40))}" is not a recognised value. ` +
145
+ 'Expected true, 1, yes or on to switch it on, or false, 0, no or off (or unset) to switch it off. Treating it as on: read-only mode is on.'
146
+ );
147
+ return true;
148
+ }
149
+
126
150
  const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
127
151
 
128
152
  /**
@@ -154,6 +178,11 @@ module.exports = {
154
178
  // Test mode setting
155
179
  USE_TEST_MODE: process.env.USE_TEST_MODE === 'true',
156
180
 
181
+ // Read-only mode (OUTLOOK_READ_ONLY, #271): every tool call that isn't
182
+ // classified `read` in utils/risk-classes.js is refused before it runs.
183
+ READ_ONLY: parseReadOnly(process.env.OUTLOOK_READ_ONLY),
184
+ parseReadOnly,
185
+
157
186
  // OAuth scope sets (exported so tests + the fallback logic can reference them)
158
187
  BASE_SCOPES,
159
188
  // `.Shared` scopes requested at sign-in for the configured mode ([] = off)
@@ -165,7 +194,12 @@ module.exports = {
165
194
 
166
195
  // Authentication configuration
167
196
  AUTH_CONFIG: {
168
- clientId: process.env.OUTLOOK_CLIENT_ID || '',
197
+ // Getter, resolved on every read: OUTLOOK_CLIENT_ID → MS_CLIENT_ID →
198
+ // ~/.outlook-assistant-config.json (saved via `auth action=authenticate
199
+ // clientId=…`), so a saved ID takes effect without a restart.
200
+ get clientId() {
201
+ return resolveClientId();
202
+ },
169
203
  clientSecret: process.env.OUTLOOK_CLIENT_SECRET || '',
170
204
  redirectUri: 'http://localhost:3333/auth/callback',
171
205
  // Base scopes, plus the `.Shared` scopes only when OUTLOOK_SHARED_MAILBOX