m365-agent-cli 1.2.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 (92) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +916 -0
  3. package/package.json +50 -0
  4. package/src/cli.ts +100 -0
  5. package/src/commands/auto-reply.ts +182 -0
  6. package/src/commands/calendar.ts +576 -0
  7. package/src/commands/counter.ts +87 -0
  8. package/src/commands/create-event.ts +544 -0
  9. package/src/commands/delegates.ts +286 -0
  10. package/src/commands/delete-event.ts +321 -0
  11. package/src/commands/drafts.ts +502 -0
  12. package/src/commands/files.ts +532 -0
  13. package/src/commands/find.ts +195 -0
  14. package/src/commands/findtime.ts +270 -0
  15. package/src/commands/folders.ts +177 -0
  16. package/src/commands/forward-event.ts +49 -0
  17. package/src/commands/graph-calendar.ts +217 -0
  18. package/src/commands/login.ts +195 -0
  19. package/src/commands/mail.ts +950 -0
  20. package/src/commands/oof.ts +263 -0
  21. package/src/commands/outlook-categories.ts +173 -0
  22. package/src/commands/outlook-graph.ts +880 -0
  23. package/src/commands/planner.ts +1678 -0
  24. package/src/commands/respond.ts +291 -0
  25. package/src/commands/rooms.ts +210 -0
  26. package/src/commands/rules.ts +511 -0
  27. package/src/commands/schedule.ts +109 -0
  28. package/src/commands/send.ts +204 -0
  29. package/src/commands/serve.ts +14 -0
  30. package/src/commands/sharepoint.ts +179 -0
  31. package/src/commands/site-pages.ts +163 -0
  32. package/src/commands/subscribe.ts +103 -0
  33. package/src/commands/subscriptions.ts +29 -0
  34. package/src/commands/suggest.ts +155 -0
  35. package/src/commands/todo.ts +2092 -0
  36. package/src/commands/update-event.ts +608 -0
  37. package/src/commands/update.ts +88 -0
  38. package/src/commands/verify-token.ts +62 -0
  39. package/src/commands/whoami.ts +74 -0
  40. package/src/index.ts +190 -0
  41. package/src/lib/atomic-write.ts +20 -0
  42. package/src/lib/attach-link-spec.test.ts +24 -0
  43. package/src/lib/attach-link-spec.ts +70 -0
  44. package/src/lib/attachments.ts +79 -0
  45. package/src/lib/auth.ts +192 -0
  46. package/src/lib/calendar-range.test.ts +41 -0
  47. package/src/lib/calendar-range.ts +103 -0
  48. package/src/lib/dates.test.ts +74 -0
  49. package/src/lib/dates.ts +137 -0
  50. package/src/lib/delegate-client.test.ts +74 -0
  51. package/src/lib/delegate-client.ts +322 -0
  52. package/src/lib/ews-client.ts +3418 -0
  53. package/src/lib/git-commit.ts +4 -0
  54. package/src/lib/glitchtip-eligibility.ts +220 -0
  55. package/src/lib/glitchtip.ts +253 -0
  56. package/src/lib/global-env.ts +3 -0
  57. package/src/lib/graph-auth.ts +223 -0
  58. package/src/lib/graph-calendar-client.test.ts +118 -0
  59. package/src/lib/graph-calendar-client.ts +112 -0
  60. package/src/lib/graph-client.test.ts +107 -0
  61. package/src/lib/graph-client.ts +1058 -0
  62. package/src/lib/graph-constants.ts +12 -0
  63. package/src/lib/graph-directory.ts +116 -0
  64. package/src/lib/graph-event.ts +134 -0
  65. package/src/lib/graph-schedule.ts +173 -0
  66. package/src/lib/graph-subscriptions.ts +94 -0
  67. package/src/lib/graph-user-path.ts +13 -0
  68. package/src/lib/jwt-utils.ts +34 -0
  69. package/src/lib/markdown.test.ts +21 -0
  70. package/src/lib/markdown.ts +174 -0
  71. package/src/lib/mime-type.ts +106 -0
  72. package/src/lib/oof-client.test.ts +59 -0
  73. package/src/lib/oof-client.ts +122 -0
  74. package/src/lib/outlook-graph-client.test.ts +146 -0
  75. package/src/lib/outlook-graph-client.ts +649 -0
  76. package/src/lib/outlook-master-categories.ts +145 -0
  77. package/src/lib/package-info.ts +59 -0
  78. package/src/lib/places-client.ts +144 -0
  79. package/src/lib/planner-client.ts +1226 -0
  80. package/src/lib/rules-client.ts +178 -0
  81. package/src/lib/sharepoint-client.ts +101 -0
  82. package/src/lib/site-pages-client.ts +73 -0
  83. package/src/lib/todo-client.test.ts +298 -0
  84. package/src/lib/todo-client.ts +1309 -0
  85. package/src/lib/url-validation.ts +40 -0
  86. package/src/lib/utils.ts +45 -0
  87. package/src/lib/webhook-server.ts +51 -0
  88. package/src/test/auth.test.ts +104 -0
  89. package/src/test/cli.integration.test.ts +1083 -0
  90. package/src/test/ews-client.test.ts +268 -0
  91. package/src/test/mocks/index.ts +375 -0
  92. package/src/test/mocks/responses.ts +861 -0
@@ -0,0 +1,511 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import {
4
+ type CreateMessageRulePayload,
5
+ createMessageRule,
6
+ deleteMessageRule,
7
+ getMessageRule,
8
+ listMessageRules,
9
+ type MessageRule,
10
+ type MessageRuleAction,
11
+ type MessageRuleCondition,
12
+ type UpdateMessageRulePayload,
13
+ updateMessageRule
14
+ } from '../lib/rules-client.js';
15
+ import { checkReadOnly } from '../lib/utils.js';
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Helpers
19
+ // ---------------------------------------------------------------------------
20
+
21
+ function parseEmailAddresses(raw: string): { emailAddress: { name?: string; address: string } }[] {
22
+ try {
23
+ const parsed = JSON.parse(raw);
24
+ if (Array.isArray(parsed)) {
25
+ return parsed.map((item) => (typeof item === 'string' ? { emailAddress: { address: item } } : item));
26
+ }
27
+ return parsed;
28
+ } catch {
29
+ return raw.split(',').map((s) => ({ emailAddress: { address: s.trim() } }));
30
+ }
31
+ }
32
+
33
+ function parseCondition(key: string, raw: string): unknown {
34
+ if (key === 'hasAttachments' || key === 'isAutomaticForward') {
35
+ return raw.toLowerCase() === 'true';
36
+ }
37
+ // Addresses fields expect JSON array; plain strings are split by comma
38
+ if (key === 'fromAddresses' || key === 'sentToAddresses') {
39
+ return parseEmailAddresses(raw);
40
+ }
41
+ // Contains fields expect string arrays
42
+ if (key === 'bodyContains' || key === 'subjectContains' || key === 'senderContains' || key === 'recipientContains') {
43
+ return [raw];
44
+ }
45
+ return raw;
46
+ }
47
+
48
+ function parseAction(key: string, raw: any): MessageRuleAction[keyof MessageRuleAction] {
49
+ if (key === 'delete' || key === 'permanentDelete' || key === 'markAsRead' || key === 'stopProcessingRules') {
50
+ if (typeof raw === 'boolean') return raw;
51
+ return String(raw).toLowerCase() === 'true';
52
+ }
53
+ if (key === 'forwardToRecipients' || key === 'forwardAsAttachmentToRecipients') {
54
+ return parseEmailAddresses(String(raw));
55
+ }
56
+ if (key === 'assignCategories') {
57
+ return String(raw)
58
+ .split(',')
59
+ .map((s: string) => s.trim());
60
+ }
61
+ return raw;
62
+ }
63
+
64
+ function conditionsFromOpts(options: Record<string, unknown>): MessageRuleCondition | undefined {
65
+ const conditions: MessageRuleCondition = {};
66
+ let used = false;
67
+
68
+ const entries: [keyof MessageRuleCondition, unknown][] = [
69
+ ['bodyContains', options.bodyContains],
70
+ ['subjectContains', options.subjectContains],
71
+ ['senderContains', options.senderContains],
72
+ ['recipientContains', options.recipientContains],
73
+ ['fromAddresses', options.fromAddresses],
74
+ ['sentToAddresses', options.sentToAddresses],
75
+ ['hasAttachments', options.hasAttachments],
76
+ ['importance', options.importance],
77
+ ['isAutomaticForward', options.isAutomaticForward]
78
+ ];
79
+
80
+ for (const [key, val] of entries) {
81
+ if (val !== undefined) {
82
+ (conditions as Record<string, unknown>)[key] = parseCondition(key as string, String(val));
83
+ used = true;
84
+ }
85
+ }
86
+ return used ? conditions : undefined;
87
+ }
88
+
89
+ function actionsFromOpts(options: Record<string, unknown>): MessageRuleAction {
90
+ const actions: MessageRuleAction = {};
91
+
92
+ if (options.delete === true || options.delete === 'true') actions.delete = true;
93
+ if (options.permanentDelete === true || options.permanentDelete === 'true') actions.permanentDelete = true;
94
+ if (options.markAsRead === true || options.markAsRead === 'true') actions.markAsRead = true;
95
+ if (options.stopProcessingRules === true || options.stopProcessingRules === 'true')
96
+ actions.stopProcessingRules = true;
97
+
98
+ if (options.moveToFolder !== undefined) actions.moveToFolder = String(options.moveToFolder);
99
+ if (options.copyToFolder !== undefined) actions.copyToFolder = String(options.copyToFolder);
100
+ if (options.markImportance !== undefined) actions.markImportance = String(options.markImportance) as any;
101
+
102
+ if (options.forwardTo !== undefined)
103
+ actions.forwardToRecipients = parseAction('forwardToRecipients', options.forwardTo as string) as any;
104
+ if (options.forwardAsAttachmentTo !== undefined)
105
+ actions.forwardAsAttachmentToRecipients = parseAction(
106
+ 'forwardAsAttachmentToRecipients',
107
+ options.forwardAsAttachmentTo as string
108
+ ) as any;
109
+ if (options.assignCategories !== undefined)
110
+ actions.assignCategories = parseAction('assignCategories', options.assignCategories as string) as any;
111
+
112
+ return actions;
113
+ }
114
+
115
+ function printRule(rule: MessageRule, json: boolean): void {
116
+ if (json) {
117
+ console.log(JSON.stringify(rule, null, 2));
118
+ return;
119
+ }
120
+ console.log(`\nRule: ${rule.displayName}`);
121
+ console.log(` ID: ${rule.id}`);
122
+ console.log(` Priority: ${rule.priority}`);
123
+ console.log(` Enabled: ${rule.isEnabled}`);
124
+
125
+ if (rule.conditions) {
126
+ const c = rule.conditions;
127
+ const parts: string[] = [];
128
+ if (c.bodyContains?.length) parts.push(`body contains: ${c.bodyContains.join(', ')}`);
129
+ if (c.subjectContains?.length) parts.push(`subject contains: ${c.subjectContains.join(', ')}`);
130
+ if (c.senderContains?.length) parts.push(`sender contains: ${c.senderContains.join(', ')}`);
131
+ if (c.recipientContains?.length) parts.push(`recipient contains: ${c.recipientContains.join(', ')}`);
132
+ if (c.fromAddresses?.length) parts.push(`from: ${c.fromAddresses.map((a) => a.emailAddress.address).join(', ')}`);
133
+ if (c.sentToAddresses?.length)
134
+ parts.push(`sent to: ${c.sentToAddresses.map((a) => a.emailAddress.address).join(', ')}`);
135
+ if (c.hasAttachments !== undefined) parts.push(`has attachments: ${c.hasAttachments}`);
136
+ if (c.importance) parts.push(`importance: ${c.importance}`);
137
+ if (c.isAutomaticForward !== undefined) parts.push(`auto forward: ${c.isAutomaticForward}`);
138
+ if (parts.length) console.log(` Conditions: ${parts.join(' | ')}`);
139
+ }
140
+
141
+ if (rule.actions) {
142
+ const a = rule.actions;
143
+ const parts: string[] = [];
144
+ if (a.moveToFolder) parts.push(`move to: ${a.moveToFolder}`);
145
+ if (a.copyToFolder) parts.push(`copy to: ${a.copyToFolder}`);
146
+ if (a.delete) parts.push('delete');
147
+ if (a.permanentDelete) parts.push('permanent delete');
148
+ if (a.markAsRead) parts.push('mark as read');
149
+ if (a.markImportance) parts.push(`mark importance: ${a.markImportance}`);
150
+ if (a.forwardToRecipients?.length)
151
+ parts.push(`forward to: ${a.forwardToRecipients.map((r) => r.emailAddress.address).join(', ')}`);
152
+ if (a.forwardAsAttachmentToRecipients?.length) {
153
+ parts.push(
154
+ `forward as attachment to: ${a.forwardAsAttachmentToRecipients.map((r) => r.emailAddress.address).join(', ')}`
155
+ );
156
+ }
157
+ if (a.assignCategories?.length) parts.push(`categories: ${a.assignCategories.join(', ')}`);
158
+ if (a.stopProcessingRules) parts.push('stop processing rules');
159
+ if (parts.length) console.log(` Actions: ${parts.join(' | ')}`);
160
+ }
161
+
162
+ if (rule.exceptionConditions) {
163
+ console.log(` Exceptions: (see --json for details)`);
164
+ }
165
+ console.log('');
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // List subcommand
170
+ // ---------------------------------------------------------------------------
171
+ const listCmd = new Command('list')
172
+ .description('List all inbox message rules')
173
+ .option('--json', 'Output as JSON')
174
+ .option('--token <token>', 'Use a specific token')
175
+ .addHelpText(
176
+ 'after',
177
+ `
178
+ Conditions you can filter by:
179
+ --bodyContains <text> Match message body contains text
180
+ --subjectContains <text> Match subject contains text
181
+ --senderContains <text> Match sender display name/address contains text
182
+ --recipientContains <text> Match any recipient contains text
183
+ --fromAddresses <emails> Match from addresses (comma-separated or JSON)
184
+ --sentToAddresses <emails> Match sent to addresses (comma-separated or JSON)
185
+ --hasAttachments <true|false> Match attachment presence
186
+ --importance <Low|Normal|High> Match importance
187
+ --isAutomaticForward <true|false> Match auto-forward messages
188
+ `
189
+ )
190
+ .option('--bodyContains <text>', 'Match body contains text')
191
+ .option('--subjectContains <text>', 'Match subject contains text')
192
+ .option('--senderContains <text>', 'Match sender contains text')
193
+ .option('--recipientContains <text>', 'Match recipient contains text')
194
+ .option('--fromAddresses <emails>', 'Match from addresses (comma-separated or JSON)')
195
+ .option('--sentToAddresses <emails>', 'Match sent to addresses (comma-separated or JSON)')
196
+ .option('--hasAttachments <value>', 'Match has attachments (true|false)')
197
+ .option('--importance <level>', 'Match importance (Low|Normal|High)')
198
+ .option('--isAutomaticForward <value>', 'Match is auto-forward (true|false)')
199
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
200
+ .option('--user <email>', 'Target mailbox for inbox rules (Graph delegation)')
201
+ .action(async (opts) => {
202
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
203
+ if (!auth.success) {
204
+ console.error(`Error: ${auth.error}`);
205
+ process.exit(1);
206
+ }
207
+
208
+ const result = await listMessageRules(auth.token!, opts.user);
209
+ if (!result.ok) {
210
+ console.error(`Error: ${result.error?.message}`);
211
+ process.exit(1);
212
+ }
213
+
214
+ let rules = result.data || [];
215
+
216
+ // Apply client-side filtering based on condition options
217
+ const filterConditions = conditionsFromOpts(opts as Record<string, unknown>);
218
+ if (filterConditions) {
219
+ rules = rules.filter((rule) => {
220
+ if (!rule.conditions) return false;
221
+ const c = rule.conditions;
222
+
223
+ // Check each filter condition
224
+ if (
225
+ filterConditions.bodyContains &&
226
+ (!c.bodyContains || !filterConditions.bodyContains.some((term) => c.bodyContains?.includes(term)))
227
+ ) {
228
+ return false;
229
+ }
230
+ if (
231
+ filterConditions.subjectContains &&
232
+ (!c.subjectContains || !filterConditions.subjectContains.some((term) => c.subjectContains?.includes(term)))
233
+ ) {
234
+ return false;
235
+ }
236
+ if (
237
+ filterConditions.senderContains &&
238
+ (!c.senderContains || !filterConditions.senderContains.some((term) => c.senderContains?.includes(term)))
239
+ ) {
240
+ return false;
241
+ }
242
+ if (
243
+ filterConditions.recipientContains &&
244
+ (!c.recipientContains ||
245
+ !filterConditions.recipientContains.some((term) => c.recipientContains?.includes(term)))
246
+ ) {
247
+ return false;
248
+ }
249
+ if (
250
+ filterConditions.fromAddresses &&
251
+ (!c.fromAddresses ||
252
+ !filterConditions.fromAddresses.some((addr) =>
253
+ c.fromAddresses?.some((ruleAddr) => ruleAddr.emailAddress.address === addr.emailAddress.address)
254
+ ))
255
+ ) {
256
+ return false;
257
+ }
258
+ if (
259
+ filterConditions.sentToAddresses &&
260
+ (!c.sentToAddresses ||
261
+ !filterConditions.sentToAddresses.some((addr) =>
262
+ c.sentToAddresses?.some((ruleAddr) => ruleAddr.emailAddress.address === addr.emailAddress.address)
263
+ ))
264
+ ) {
265
+ return false;
266
+ }
267
+ if (filterConditions.hasAttachments !== undefined && c.hasAttachments !== filterConditions.hasAttachments) {
268
+ return false;
269
+ }
270
+ if (filterConditions.importance && c.importance !== filterConditions.importance) {
271
+ return false;
272
+ }
273
+ if (
274
+ filterConditions.isAutomaticForward !== undefined &&
275
+ c.isAutomaticForward !== filterConditions.isAutomaticForward
276
+ ) {
277
+ return false;
278
+ }
279
+
280
+ return true;
281
+ });
282
+ }
283
+
284
+ if (rules.length === 0) {
285
+ console.log(opts.json ? '[]' : 'No inbox rules found.');
286
+ return;
287
+ }
288
+
289
+ if (opts.json) {
290
+ console.log(JSON.stringify(rules, null, 2));
291
+ return;
292
+ }
293
+
294
+ for (const rule of rules) {
295
+ printRule(rule, false);
296
+ }
297
+ });
298
+
299
+ // ---------------------------------------------------------------------------
300
+ // Get subcommand
301
+ // ---------------------------------------------------------------------------
302
+ const getCmd = new Command('get')
303
+ .description('Get a single inbox rule by ID')
304
+ .argument('<ruleId>', 'The rule ID')
305
+ .option('--json', 'Output as JSON')
306
+ .option('--token <token>', 'Use a specific token')
307
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
308
+ .option('--user <email>', 'Target mailbox for inbox rules (Graph delegation)')
309
+ .action(async (ruleId, opts) => {
310
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
311
+ if (!auth.success) {
312
+ console.error(`Error: ${auth.error}`);
313
+ process.exit(1);
314
+ }
315
+
316
+ const result = await getMessageRule(auth.token!, ruleId, opts.user);
317
+ if (!result.ok) {
318
+ console.error(`Error: ${result.error?.message}`);
319
+ process.exit(1);
320
+ }
321
+
322
+ printRule(result.data!, !!opts.json);
323
+ });
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Create subcommand
327
+ // ---------------------------------------------------------------------------
328
+ const createCmd = new Command('create')
329
+ .description('Create a new inbox message rule')
330
+ .requiredOption('--name <name>', 'Rule display name')
331
+ .option('--priority <number>', 'Rule priority (lower = runs first)', parseInt)
332
+ .option('--disable', 'Create rule in disabled state')
333
+ .option('--json', 'Output as JSON')
334
+ .option('--token <token>', 'Use a specific token')
335
+ // Conditions
336
+ .option('--bodyContains <text>', 'Condition: body contains text')
337
+ .option('--subjectContains <text>', 'Condition: subject contains text')
338
+ .option('--senderContains <text>', 'Condition: sender contains text')
339
+ .option('--recipientContains <text>', 'Condition: recipient contains text')
340
+ .option('--fromAddresses <emails>', 'Condition: from addresses (comma-separated or JSON)')
341
+ .option('--sentToAddresses <emails>', 'Condition: sent to addresses (comma-separated or JSON)')
342
+ .option('--hasAttachments <value>', 'Condition: has attachments (true|false)')
343
+ .option('--importance <level>', 'Condition: importance (Low|Normal|High)')
344
+ .option('--isAutomaticForward <value>', 'Condition: is auto-forward (true|false)')
345
+ // Actions
346
+ .option('--moveToFolder <folder>', 'Action: move to folder')
347
+ .option('--copyToFolder <folder>', 'Action: copy to folder')
348
+ .option('--delete', 'Action: soft-delete')
349
+ .option('--permanentDelete', 'Action: permanent delete')
350
+ .option('--markAsRead', 'Action: mark as read')
351
+ .option('--markImportance <level>', 'Action: mark importance (Low|Normal|High)')
352
+ .option('--forwardTo <emails>', 'Action: forward to recipients (comma-separated or JSON)')
353
+ .option('--forwardAsAttachmentTo <emails>', 'Action: forward as attachment to recipients (comma-separated or JSON)')
354
+ .option('--assignCategories <cats>', 'Action: assign categories (comma-separated)')
355
+ .option('--stopProcessingRules', 'Action: stop processing more rules')
356
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
357
+ .option('--user <email>', 'Target mailbox for inbox rules (Graph delegation)')
358
+ .action(async (opts, cmd: any) => {
359
+ checkReadOnly(cmd);
360
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
361
+ if (!auth.success) {
362
+ console.error(`Error: ${auth.error}`);
363
+ process.exit(1);
364
+ }
365
+
366
+ const conditions = conditionsFromOpts(opts as Record<string, unknown>);
367
+ const actions = actionsFromOpts(opts as Record<string, unknown>);
368
+
369
+ if (!conditions) {
370
+ console.error('Error: at least one condition is required.');
371
+ process.exit(1);
372
+ }
373
+ if (Object.keys(actions).length === 0) {
374
+ console.error('Error: at least one action is required.');
375
+ process.exit(1);
376
+ }
377
+
378
+ const payload: CreateMessageRulePayload = {
379
+ displayName: opts.name,
380
+ isEnabled: !opts.disable,
381
+ priority: opts.priority,
382
+ conditions,
383
+ actions
384
+ };
385
+
386
+ console.log('Creating rule…');
387
+ if (!opts.json) {
388
+ console.log(` Name: ${payload.displayName}`);
389
+ console.log(` Priority: ${payload.priority ?? 'default'}`);
390
+ console.log(` Enabled: ${payload.isEnabled}`);
391
+ }
392
+
393
+ const result = await createMessageRule(auth.token!, payload, opts.user);
394
+ if (!result.ok) {
395
+ console.error(`Error: ${result.error?.message}`);
396
+ process.exit(1);
397
+ }
398
+
399
+ console.log(
400
+ opts.json
401
+ ? JSON.stringify(result.data, null, 2)
402
+ : `\u2713 Rule created: ${result.data!.displayName} (${result.data!.id})`
403
+ );
404
+ });
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // Update subcommand
408
+ // ---------------------------------------------------------------------------
409
+ const updateCmd = new Command('update')
410
+ .description('Update an existing inbox message rule')
411
+ .requiredOption('--id <ruleId>', 'The rule ID to update')
412
+ .option('--name <name>', 'New rule display name')
413
+ .option('--priority <number>', 'New rule priority', parseInt)
414
+ .option('--enable', 'Enable the rule')
415
+ .option('--disable', 'Disable the rule')
416
+ .option('--json', 'Output as JSON')
417
+ .option('--token <token>', 'Use a specific token')
418
+ // Conditions (replace all)
419
+ .option('--bodyContains <text>', 'Condition: body contains text')
420
+ .option('--subjectContains <text>', 'Condition: subject contains text')
421
+ .option('--senderContains <text>', 'Condition: sender contains text')
422
+ .option('--recipientContains <text>', 'Condition: recipient contains text')
423
+ .option('--fromAddresses <emails>', 'Condition: from addresses (comma-separated or JSON)')
424
+ .option('--sentToAddresses <emails>', 'Condition: sent to addresses (comma-separated or JSON)')
425
+ .option('--hasAttachments <value>', 'Condition: has attachments (true|false)')
426
+ .option('--importance <level>', 'Condition: importance (Low|Normal|High)')
427
+ .option('--isAutomaticForward <value>', 'Condition: is auto-forward (true|false)')
428
+ // Actions (replace all)
429
+ .option('--moveToFolder <folder>', 'Action: move to folder')
430
+ .option('--copyToFolder <folder>', 'Action: copy to folder')
431
+ .option('--delete', 'Action: soft-delete')
432
+ .option('--permanentDelete', 'Action: permanent delete')
433
+ .option('--markAsRead', 'Action: mark as read')
434
+ .option('--markImportance <level>', 'Action: mark importance (Low|Normal|High)')
435
+ .option('--forwardTo <emails>', 'Action: forward to recipients (comma-separated or JSON)')
436
+ .option('--forwardAsAttachmentTo <emails>', 'Action: forward as attachment to recipients (comma-separated or JSON)')
437
+ .option('--assignCategories <cats>', 'Action: assign categories (comma-separated)')
438
+ .option('--stopProcessingRules', 'Action: stop processing more rules')
439
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
440
+ .option('--user <email>', 'Target mailbox for inbox rules (Graph delegation)')
441
+ .action(async (opts, cmd: any) => {
442
+ checkReadOnly(cmd);
443
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
444
+ if (!auth.success) {
445
+ console.error(`Error: ${auth.error}`);
446
+ process.exit(1);
447
+ }
448
+
449
+ if (opts.enable && opts.disable) {
450
+ console.error('Error: --enable and --disable cannot be used together.');
451
+ process.exit(1);
452
+ }
453
+
454
+ const conditions = conditionsFromOpts(opts as Record<string, unknown>);
455
+ const actions = actionsFromOpts(opts as Record<string, unknown>);
456
+
457
+ const payload: UpdateMessageRulePayload = {};
458
+ if (opts.name) payload.displayName = opts.name;
459
+ if (opts.priority !== undefined) payload.priority = opts.priority;
460
+ if (opts.enable) payload.isEnabled = true;
461
+ if (opts.disable) payload.isEnabled = false;
462
+ if (conditions) payload.conditions = conditions;
463
+ if (Object.keys(actions).length > 0) payload.actions = actions;
464
+
465
+ console.log(`Updating rule ${opts.id}…`);
466
+ const result = await updateMessageRule(auth.token!, opts.id, payload, opts.user);
467
+ if (!result.ok) {
468
+ console.error(`Error: ${result.error?.message}`);
469
+ process.exit(1);
470
+ }
471
+
472
+ console.log(opts.json ? JSON.stringify(result.data, null, 2) : `\u2713 Rule updated: ${result.data!.displayName}`);
473
+ });
474
+
475
+ // ---------------------------------------------------------------------------
476
+ // Delete subcommand
477
+ // ---------------------------------------------------------------------------
478
+ const deleteCmd = new Command('delete')
479
+ .description('Delete an inbox message rule')
480
+ .argument('<ruleId>', 'The rule ID to delete')
481
+ .option('--token <token>', 'Use a specific token')
482
+ .option('--json', 'Output as JSON')
483
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
484
+ .option('--user <email>', 'Target mailbox for inbox rules (Graph delegation)')
485
+ .action(async (ruleId, opts, cmd: any) => {
486
+ checkReadOnly(cmd);
487
+ const auth = await resolveGraphAuth({ token: opts.token, identity: opts.identity });
488
+ if (!auth.success) {
489
+ console.error(`Error: ${auth.error}`);
490
+ process.exit(1);
491
+ }
492
+
493
+ const result = await deleteMessageRule(auth.token!, ruleId, opts.user);
494
+ if (!result.ok) {
495
+ console.error(`Error: ${result.error?.message}`);
496
+ process.exit(1);
497
+ }
498
+
499
+ console.log(opts.json ? JSON.stringify({ deleted: ruleId }) : `\u2713 Rule deleted: ${ruleId}`);
500
+ });
501
+
502
+ // ---------------------------------------------------------------------------
503
+ // Main command
504
+ // ---------------------------------------------------------------------------
505
+ export const rulesCommand = new Command('rules')
506
+ .description('Manage server-side inbox message rules (Graph messageRules API)')
507
+ .addCommand(listCmd)
508
+ .addCommand(getCmd)
509
+ .addCommand(createCmd)
510
+ .addCommand(updateCmd)
511
+ .addCommand(deleteCmd);
@@ -0,0 +1,109 @@
1
+ import { Command } from 'commander';
2
+ import { resolveGraphAuth } from '../lib/graph-auth.js';
3
+ import { getSchedule } from '../lib/graph-schedule.js';
4
+
5
+ export const scheduleCommand = new Command('schedule')
6
+ .description('Get merged free/busy schedule for multiple users')
7
+ .argument('<emails...>', 'One or more email addresses to check')
8
+ .requiredOption('--start <date>', 'Start date/time (e.g. 2026-04-01T00:00:00Z or 2026-04-01)')
9
+ .requiredOption('--end <date>', 'End date/time (e.g. 2026-04-07T00:00:00Z or 2026-04-07)')
10
+ .option('--json', 'Output as JSON')
11
+ .option('--token <token>', 'Graph access token (bypass interactive auth)')
12
+ .option('--identity <name>', 'Graph token cache identity (default: default)')
13
+ .option('--user <email>', 'Mailbox whose calendar getSchedule runs in (delegation); omit to use signed-in user')
14
+ .action(
15
+ async (
16
+ emails: string[],
17
+ options: { start: string; end: string; json?: boolean; token?: string; identity?: string; user?: string }
18
+ ) => {
19
+ const authResult = await resolveGraphAuth({ token: options.token, identity: options.identity });
20
+ if (!authResult.success || !authResult.token) {
21
+ if (options.json) {
22
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
23
+ } else {
24
+ console.error(`Error: ${authResult.error}`);
25
+ }
26
+ process.exit(1);
27
+ }
28
+
29
+ const startDate = new Date(options.start);
30
+ const endDate = new Date(options.end);
31
+
32
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
33
+ const errorMessage =
34
+ 'Invalid start or end date. Please provide ISO 8601 date/time values (e.g. 2026-04-01T00:00:00Z or 2026-04-01).';
35
+ if (options.json) {
36
+ console.log(JSON.stringify({ error: errorMessage }, null, 2));
37
+ } else {
38
+ console.error(`Error: ${errorMessage}`);
39
+ }
40
+ process.exit(1);
41
+ }
42
+
43
+ // dateTime should not include Z/offset - keep dateTime and timeZone separate
44
+ const startDateTime = startDate.toISOString().replace('Z', '');
45
+ const endDateTime = endDate.toISOString().replace('Z', '');
46
+
47
+ const result = await getSchedule(
48
+ authResult.token,
49
+ {
50
+ schedules: emails,
51
+ startTime: {
52
+ dateTime: startDateTime,
53
+ timeZone: 'UTC'
54
+ },
55
+ endTime: {
56
+ dateTime: endDateTime,
57
+ timeZone: 'UTC'
58
+ },
59
+ availabilityViewInterval: 60
60
+ },
61
+ options.user
62
+ );
63
+
64
+ if (!result.ok || !result.data) {
65
+ if (options.json) {
66
+ console.log(JSON.stringify({ error: result.error }, null, 2));
67
+ } else {
68
+ console.error('Error fetching schedule:', result.error?.message || 'Unknown error');
69
+ }
70
+ process.exit(1);
71
+ }
72
+
73
+ if (options.json) {
74
+ console.log(JSON.stringify(result.data, null, 2));
75
+ return;
76
+ }
77
+
78
+ console.log(`\nSchedule for ${emails.join(', ')}`);
79
+ console.log(`From: ${startDateTime}\nTo: ${endDateTime}\n`);
80
+
81
+ for (const schedule of result.data.value) {
82
+ console.log(`User: ${schedule.scheduleId}`);
83
+ if (schedule.error) {
84
+ console.log(` Error: ${schedule.error.message || schedule.error.responseCode}`);
85
+ continue;
86
+ }
87
+
88
+ if (schedule.workingHours) {
89
+ const wh = schedule.workingHours;
90
+ const days = wh.daysOfWeek?.join(', ') || 'N/A';
91
+ console.log(` Working Hours: ${wh.startTime} - ${wh.endTime} (${wh.timeZone?.name}) on ${days}`);
92
+ }
93
+
94
+ if (schedule.scheduleItems && schedule.scheduleItems.length > 0) {
95
+ console.log(' Busy times:');
96
+ for (const item of schedule.scheduleItems) {
97
+ const status = item.status || 'Busy';
98
+ const start = item.start?.dateTime ? new Date(`${item.start.dateTime}Z`).toLocaleString() : 'Unknown';
99
+ const end = item.end?.dateTime ? new Date(`${item.end.dateTime}Z`).toLocaleString() : 'Unknown';
100
+ const subject = item.subject ? ` - ${item.subject}` : '';
101
+ console.log(` [${status}] ${start} to ${end}${subject}`);
102
+ }
103
+ } else {
104
+ console.log(' No busy times scheduled.');
105
+ }
106
+ console.log();
107
+ }
108
+ }
109
+ );