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,502 @@
1
+ import { open, readFile } from 'node:fs/promises';
2
+ import { Command } from 'commander';
3
+ import { AttachmentLinkSpecError, parseAttachLinkSpec } from '../lib/attach-link-spec.js';
4
+ import { AttachmentPathError, validateAttachmentPath } from '../lib/attachments.js';
5
+ import { resolveAuth } from '../lib/auth.js';
6
+ import {
7
+ addAttachmentToDraft,
8
+ addReferenceAttachmentToDraft,
9
+ createDraft,
10
+ deleteDraftById,
11
+ getEmail,
12
+ getEmails,
13
+ sendDraftById,
14
+ updateDraft
15
+ } from '../lib/ews-client.js';
16
+ import { markdownToHtml } from '../lib/markdown.js';
17
+ import { lookupMimeType } from '../lib/mime-type.js';
18
+ import { checkReadOnly } from '../lib/utils.js';
19
+
20
+ function formatDate(dateStr: string): string {
21
+ const date = new Date(dateStr);
22
+ const now = new Date();
23
+ const isToday = date.toDateString() === now.toDateString();
24
+
25
+ if (isToday) {
26
+ return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
27
+ } else {
28
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
29
+ }
30
+ }
31
+
32
+ function truncate(str: string, maxLen: number): string {
33
+ if (!str) return '';
34
+ str = str.replace(/\s+/g, ' ').trim();
35
+ if (str.length <= maxLen) return str;
36
+ return `${str.substring(0, maxLen - 1)}\u2026`;
37
+ }
38
+
39
+ export const draftsCommand = new Command('drafts')
40
+ .description('Manage email drafts')
41
+ .option('-n, --limit <number>', 'Number of drafts to show', '10')
42
+ .option('-r, --read <id>', 'Read draft by ID')
43
+ .option('--create', 'Create a new draft')
44
+ .option('--edit <id>', 'Edit draft by ID')
45
+ .option('--send <id>', 'Send draft by ID')
46
+ .option('--delete <id>', 'Delete draft by ID')
47
+ .option('--to <emails>', 'Recipient(s) for create/edit, comma-separated')
48
+ .option('--cc <emails>', 'CC recipient(s), comma-separated')
49
+ .option('--subject <text>', 'Subject for create/edit')
50
+ .option('--body <text>', 'Body for create/edit')
51
+ .option('--attach <files>', 'Attach file(s), comma-separated paths')
52
+ .option(
53
+ '--attach-link <spec>',
54
+ 'Attach link: "Title|https://url" or bare https URL (repeatable)',
55
+ (v: string, prev: string[]) => [...prev, v],
56
+ [] as string[]
57
+ )
58
+ .option('--markdown', 'Parse body as markdown')
59
+ .option('--html', 'Treat body as HTML')
60
+ .option(
61
+ '--category <name>',
62
+ 'Outlook category (repeatable; colors follow mailbox master list)',
63
+ (v: string, prev: string[]) => [...prev, v],
64
+ [] as string[]
65
+ )
66
+ .option('--clear-categories', 'On --edit, remove all categories from the draft')
67
+ .option('--json', 'Output as JSON')
68
+ .option('--token <token>', 'Use a specific token')
69
+ .option('--identity <name>', 'Use a specific authentication identity (EWS; default: default)')
70
+ .option('--mailbox <email>', 'Delegated or shared mailbox drafts folder')
71
+ .action(
72
+ async (
73
+ options: {
74
+ limit: string;
75
+ read?: string;
76
+ create?: boolean;
77
+ edit?: string;
78
+ send?: string;
79
+ delete?: string;
80
+ to?: string;
81
+ cc?: string;
82
+ subject?: string;
83
+ body?: string;
84
+ attach?: string;
85
+ attachLink?: string[];
86
+ markdown?: boolean;
87
+ html?: boolean;
88
+ json?: boolean;
89
+ token?: string;
90
+ identity?: string;
91
+ mailbox?: string;
92
+ category?: string[];
93
+ clearCategories?: boolean;
94
+ },
95
+ cmd: any
96
+ ) => {
97
+ if (options.send || options.delete || options.create || options.edit) {
98
+ checkReadOnly(cmd);
99
+ }
100
+ const authResult = await resolveAuth({
101
+ token: options.token,
102
+ identity: options.identity
103
+ });
104
+
105
+ if (!authResult.success) {
106
+ if (options.json) {
107
+ console.log(JSON.stringify({ error: authResult.error }, null, 2));
108
+ } else {
109
+ console.error(`Error: ${authResult.error}`);
110
+ console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
111
+ }
112
+ process.exit(1);
113
+ }
114
+
115
+ const limit = parseInt(options.limit, 10) || 10;
116
+
117
+ // Get drafts for listing
118
+ const draftsResult = await getEmails({
119
+ token: authResult.token!,
120
+ folder: 'drafts',
121
+ mailbox: options.mailbox,
122
+ top: limit
123
+ });
124
+
125
+ if (!draftsResult.ok || !draftsResult.data) {
126
+ if (options.json) {
127
+ console.log(JSON.stringify({ error: draftsResult.error?.message || 'Failed to fetch drafts' }, null, 2));
128
+ } else {
129
+ console.error(`Error: ${draftsResult.error?.message || 'Failed to fetch drafts'}`);
130
+ }
131
+ process.exit(1);
132
+ }
133
+
134
+ const drafts = draftsResult.data.value;
135
+
136
+ // Handle create
137
+ if (options.create) {
138
+ const toList = options.to
139
+ ? options.to
140
+ .split(',')
141
+ .map((e) => e.trim())
142
+ .filter(Boolean)
143
+ : undefined;
144
+ const ccList = options.cc
145
+ ? options.cc
146
+ .split(',')
147
+ .map((e) => e.trim())
148
+ .filter(Boolean)
149
+ : undefined;
150
+
151
+ let body = options.body;
152
+ if (body) body = body.replace(/\\n/g, '\n');
153
+ let bodyType: 'Text' | 'HTML' = 'Text';
154
+ if (options.html && body) {
155
+ const escaped = body
156
+ .replace(/&/g, '&amp;')
157
+ .replace(/</g, '&lt;')
158
+ .replace(/>/g, '&gt;')
159
+ .replace(/\n/g, '<br>');
160
+ body = body.match(/<\w+[^>]*>/) ? body : escaped;
161
+ bodyType = 'HTML';
162
+ } else if (options.markdown && body) {
163
+ body = markdownToHtml(body);
164
+ bodyType = 'HTML';
165
+ }
166
+
167
+ const cats = (options.category ?? []).map((c) => c.trim()).filter(Boolean);
168
+ const result = await createDraft(authResult.token!, {
169
+ to: toList,
170
+ cc: ccList,
171
+ subject: options.subject,
172
+ body,
173
+ bodyType,
174
+ mailbox: options.mailbox,
175
+ categories: cats.length ? cats : undefined
176
+ });
177
+
178
+ if (!result.ok || !result.data) {
179
+ console.error(`Error: ${result.error?.message || 'Failed to create draft'}`);
180
+ process.exit(1);
181
+ }
182
+
183
+ // Add attachments if specified
184
+ const workingDirectory = process.cwd();
185
+ if (options.attach) {
186
+ const filePaths = options.attach
187
+ .split(',')
188
+ .map((f) => f.trim())
189
+ .filter(Boolean);
190
+ for (const filePath of filePaths) {
191
+ try {
192
+ const validated = await validateAttachmentPath(filePath, workingDirectory);
193
+ const fh = await open(validated.absolutePath, 'r');
194
+ let content: Buffer;
195
+ try {
196
+ const st = await fh.stat();
197
+ if (!st.isFile()) {
198
+ console.error(`Not a file: ${validated.absolutePath}`);
199
+ process.exit(1);
200
+ }
201
+ if (st.size > 25 * 1024 * 1024) {
202
+ console.error(`File too large (>25MB): ${validated.absolutePath}`);
203
+ process.exit(1);
204
+ }
205
+ content = await fh.readFile();
206
+ } finally {
207
+ await fh.close();
208
+ }
209
+ const contentType = lookupMimeType(validated.fileName) || 'application/octet-stream';
210
+
211
+ const attachResult = await addAttachmentToDraft(
212
+ authResult.token!,
213
+ result.data.Id,
214
+ {
215
+ name: validated.fileName,
216
+ contentType,
217
+ contentBytes: content.toString('base64')
218
+ },
219
+ options.mailbox
220
+ );
221
+
222
+ if (!attachResult.ok) {
223
+ console.error(`Failed to attach ${validated.fileName}: ${attachResult.error?.message}`);
224
+ } else if (!options.json) {
225
+ console.log(` Attached: ${validated.fileName}`);
226
+ }
227
+ } catch (err) {
228
+ if (err instanceof AttachmentPathError) {
229
+ console.error(`Invalid attachment path: ${filePath}: ${err.message}`);
230
+ } else {
231
+ console.error(`Failed to attach: ${filePath}`);
232
+ }
233
+ process.exit(1);
234
+ }
235
+ }
236
+ }
237
+
238
+ const linkSpecsCreate = options.attachLink ?? [];
239
+ for (const spec of linkSpecsCreate) {
240
+ try {
241
+ const { name, url } = parseAttachLinkSpec(spec);
242
+ const linkRes = await addReferenceAttachmentToDraft(
243
+ authResult.token!,
244
+ result.data.Id,
245
+ { name, url, contentType: 'text/html' },
246
+ options.mailbox
247
+ );
248
+ if (!linkRes.ok) {
249
+ console.error(`Failed to attach link ${name}: ${linkRes.error?.message}`);
250
+ process.exit(1);
251
+ }
252
+ if (!options.json) {
253
+ console.log(` Attached link: ${name}`);
254
+ }
255
+ } catch (err) {
256
+ const msg =
257
+ err instanceof AttachmentLinkSpecError ? err.message : err instanceof Error ? err.message : String(err);
258
+ console.error(`Invalid --attach-link: ${msg}`);
259
+ process.exit(1);
260
+ }
261
+ }
262
+
263
+ if (options.json) {
264
+ console.log(JSON.stringify({ success: true, draftId: result.data.Id }, null, 2));
265
+ } else {
266
+ console.log(`\n\u2713 Draft created`);
267
+ if (options.subject) console.log(` Subject: ${options.subject}`);
268
+ if (toList) console.log(` To: ${toList.join(', ')}`);
269
+ console.log();
270
+ }
271
+
272
+ return;
273
+ }
274
+
275
+ // Handle read
276
+ if (options.read) {
277
+ const id = options.read.trim();
278
+ const fullDraft = await getEmail(authResult.token!, id, options.mailbox);
279
+
280
+ if (!fullDraft.ok || !fullDraft.data) {
281
+ console.error(`Error: ${fullDraft.error?.message || 'Failed to fetch draft'}`);
282
+ process.exit(1);
283
+ }
284
+
285
+ const d = fullDraft.data;
286
+
287
+ if (options.json) {
288
+ console.log(JSON.stringify(d, null, 2));
289
+ return;
290
+ }
291
+
292
+ console.log(`\n${'\u2500'.repeat(60)}`);
293
+ console.log(`To: ${d.ToRecipients?.map((r) => r.EmailAddress?.Address).join(', ') || '(none)'}`);
294
+ console.log(`Subject: ${d.Subject || '(no subject)'}`);
295
+ if (d.Categories?.length) console.log(`Categories: ${d.Categories.join(', ')}`);
296
+ console.log(`${'\u2500'.repeat(60)}\n`);
297
+ console.log(d.Body?.Content || d.BodyPreview || '(no content)');
298
+ console.log(`\n${'\u2500'.repeat(60)}\n`);
299
+ return;
300
+ }
301
+
302
+ // Handle edit
303
+ if (options.edit) {
304
+ const id = options.edit.trim();
305
+ const toList = options.to
306
+ ? options.to
307
+ .split(',')
308
+ .map((e) => e.trim())
309
+ .filter(Boolean)
310
+ : undefined;
311
+ const ccList = options.cc
312
+ ? options.cc
313
+ .split(',')
314
+ .map((e) => e.trim())
315
+ .filter(Boolean)
316
+ : undefined;
317
+
318
+ let body = options.body;
319
+ if (body) body = body.replace(/\\n/g, '\n');
320
+ let bodyType: 'Text' | 'HTML' = 'Text';
321
+ if (options.html && body) {
322
+ const escaped = body
323
+ .replace(/&/g, '&amp;')
324
+ .replace(/</g, '&lt;')
325
+ .replace(/>/g, '&gt;')
326
+ .replace(/\n/g, '<br>');
327
+ body = body.match(/<\w+[^>]*>/) ? body : escaped;
328
+ bodyType = 'HTML';
329
+ } else if (options.markdown && body) {
330
+ body = markdownToHtml(body);
331
+ bodyType = 'HTML';
332
+ }
333
+
334
+ const cats = (options.category ?? []).map((c) => c.trim()).filter(Boolean);
335
+ const result = await updateDraft(authResult.token!, id, {
336
+ to: toList,
337
+ cc: ccList,
338
+ subject: options.subject,
339
+ body,
340
+ bodyType,
341
+ mailbox: options.mailbox,
342
+ categories: cats.length ? cats : undefined,
343
+ clearCategories: options.clearCategories
344
+ });
345
+
346
+ if (!result.ok) {
347
+ console.error(`Error: ${result.error?.message || 'Failed to update draft'}`);
348
+ process.exit(1);
349
+ }
350
+
351
+ // Add attachments if specified
352
+ const workingDirectory = process.cwd();
353
+ if (options.attach) {
354
+ const filePaths = options.attach
355
+ .split(',')
356
+ .map((f) => f.trim())
357
+ .filter(Boolean);
358
+ for (const filePath of filePaths) {
359
+ try {
360
+ const validated = await validateAttachmentPath(filePath, workingDirectory);
361
+ const content = await readFile(validated.absolutePath);
362
+ const contentType = lookupMimeType(validated.fileName) || 'application/octet-stream';
363
+
364
+ await addAttachmentToDraft(
365
+ authResult.token!,
366
+ id,
367
+ {
368
+ name: validated.fileName,
369
+ contentType,
370
+ contentBytes: content.toString('base64')
371
+ },
372
+ options.mailbox
373
+ );
374
+
375
+ if (!options.json) {
376
+ console.log(` Attached: ${validated.fileName}`);
377
+ }
378
+ } catch (err) {
379
+ if (err instanceof AttachmentPathError) {
380
+ console.error(`Invalid attachment path: ${filePath}: ${err.message}`);
381
+ } else {
382
+ console.error(`Failed to attach: ${filePath}`);
383
+ }
384
+ process.exit(1);
385
+ }
386
+ }
387
+ }
388
+
389
+ const linkSpecsEdit = options.attachLink ?? [];
390
+ for (const spec of linkSpecsEdit) {
391
+ try {
392
+ const { name, url } = parseAttachLinkSpec(spec);
393
+ const linkRes = await addReferenceAttachmentToDraft(
394
+ authResult.token!,
395
+ id,
396
+ { name, url, contentType: 'text/html' },
397
+ options.mailbox
398
+ );
399
+ if (!linkRes.ok) {
400
+ console.error(`Failed to attach link ${name}: ${linkRes.error?.message}`);
401
+ process.exit(1);
402
+ }
403
+ if (!options.json) {
404
+ console.log(` Attached link: ${name}`);
405
+ }
406
+ } catch (err) {
407
+ const msg =
408
+ err instanceof AttachmentLinkSpecError ? err.message : err instanceof Error ? err.message : String(err);
409
+ console.error(`Invalid --attach-link: ${msg}`);
410
+ process.exit(1);
411
+ }
412
+ }
413
+
414
+ console.log(`\u2713 Draft updated: ${id}`);
415
+ return;
416
+ }
417
+
418
+ // Handle send
419
+ if (options.send) {
420
+ const id = options.send.trim();
421
+ const result = await sendDraftById(authResult.token!, id, options.mailbox);
422
+
423
+ if (!result.ok) {
424
+ console.error(`Error: ${result.error?.message || 'Failed to send draft'}`);
425
+ process.exit(1);
426
+ }
427
+
428
+ console.log(`\u2713 Draft sent: ${id}`);
429
+ return;
430
+ }
431
+
432
+ // Handle delete
433
+ if (options.delete) {
434
+ const id = options.delete.trim();
435
+ if (!id) {
436
+ console.error('Error: --delete requires a draft ID');
437
+ process.exit(1);
438
+ }
439
+ const result = await deleteDraftById(authResult.token!, id, options.mailbox);
440
+
441
+ if (!result.ok) {
442
+ console.error(`Error: ${result.error?.message || 'Failed to delete draft'}`);
443
+ process.exit(1);
444
+ }
445
+
446
+ console.log(`\u2713 Draft deleted: ${id}`);
447
+ return;
448
+ }
449
+
450
+ // List drafts
451
+ if (options.json) {
452
+ console.log(
453
+ JSON.stringify(
454
+ {
455
+ drafts: drafts.map((d, i) => ({
456
+ index: i + 1,
457
+ id: d.Id,
458
+ to: d.ToRecipients?.map((r) => r.EmailAddress?.Address),
459
+ subject: d.Subject,
460
+ preview: d.BodyPreview,
461
+ lastModified: d.ReceivedDateTime,
462
+ categories: d.Categories
463
+ }))
464
+ },
465
+ null,
466
+ 2
467
+ )
468
+ );
469
+ return;
470
+ }
471
+
472
+ console.log(`\n\ud83d\udcdd Drafts${options.mailbox ? ` — ${options.mailbox}` : ''}:\n`);
473
+ console.log('\u2500'.repeat(70));
474
+
475
+ if (drafts.length === 0) {
476
+ console.log('\n No drafts found.\n');
477
+ return;
478
+ }
479
+
480
+ for (let i = 0; i < drafts.length; i++) {
481
+ const draft = drafts[i];
482
+ const to = draft.ToRecipients?.map((r) => r.EmailAddress?.Address).join(', ') || '(no recipient)';
483
+ const subject = draft.Subject || '(no subject)';
484
+ const date = draft.ReceivedDateTime ? formatDate(draft.ReceivedDateTime) : '';
485
+
486
+ console.log(
487
+ ` [${(i + 1).toString().padStart(2)}] ${truncate(to, 25).padEnd(25)} ${truncate(subject, 32).padEnd(32)} ${date}`
488
+ );
489
+ console.log(` ID: ${draft.Id}`);
490
+ if (draft.Categories?.length) console.log(` Categories: ${draft.Categories.join(', ')}`);
491
+ }
492
+
493
+ console.log(`\n${'\u2500'.repeat(70)}`);
494
+ console.log('\nCommands:');
495
+ console.log(' m365-agent-cli drafts -r <id> # Read draft');
496
+ console.log(' m365-agent-cli drafts --create --to "..." --subject "..." --body "..."');
497
+ console.log(' m365-agent-cli drafts --edit <id> --body "new text"');
498
+ console.log(' m365-agent-cli drafts --send <id> # Send draft');
499
+ console.log(' m365-agent-cli drafts --delete <id> # Delete draft');
500
+ console.log();
501
+ }
502
+ );