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,174 @@
1
+ function escapeHtml(value: string): string {
2
+ return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
3
+ }
4
+
5
+ function sanitizeLinkUrl(url: string): string {
6
+ const trimmed = url.trim();
7
+ const withoutControlChars = Array.from(trimmed)
8
+ .filter((char) => {
9
+ const code = char.charCodeAt(0);
10
+ return !(
11
+ code <= 0x20 ||
12
+ (code >= 0x7f && code <= 0x9f) ||
13
+ code === 0x200b ||
14
+ code === 0x200c ||
15
+ code === 0x200d ||
16
+ code === 0xfeff
17
+ );
18
+ })
19
+ .join('');
20
+ const decoded = withoutControlChars.replace(/&(#x?[\da-f]+|[a-z]+);?/gi, (entity) => {
21
+ if (/^&#x/i.test(entity)) {
22
+ const value = Number.parseInt(entity.slice(3).replace(/;$/, ''), 16);
23
+ return Number.isFinite(value) ? String.fromCharCode(value) : entity;
24
+ }
25
+ if (/^&#/i.test(entity)) {
26
+ const value = Number.parseInt(entity.slice(2).replace(/;$/, ''), 10);
27
+ return Number.isFinite(value) ? String.fromCharCode(value) : entity;
28
+ }
29
+ const named: Record<string, string> = {
30
+ amp: '&',
31
+ lt: '<',
32
+ gt: '>',
33
+ quot: '"',
34
+ apos: "'"
35
+ };
36
+ const key = entity.slice(1).replace(/;$/, '').toLowerCase();
37
+ return named[key] ?? entity;
38
+ });
39
+ const lower = decoded.toLowerCase();
40
+
41
+ if (
42
+ lower.startsWith('javascript:') ||
43
+ lower.startsWith('data:') ||
44
+ lower.startsWith('vbscript:') ||
45
+ lower.startsWith('file:')
46
+ ) {
47
+ return '#';
48
+ }
49
+
50
+ return withoutControlChars;
51
+ }
52
+
53
+ /**
54
+ * Convert basic markdown to HTML for email.
55
+ * Supports: bold, italic, links, unordered lists, ordered lists, line breaks.
56
+ */
57
+ export function markdownToHtml(text: string): string {
58
+ // Extract and process links first to avoid double-encoding URLs
59
+ const links: Array<{ placeholder: string; html: string }> = [];
60
+ let linkIndex = 0;
61
+
62
+ let html = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, label, rawUrl) => {
63
+ const safeUrl = sanitizeLinkUrl(rawUrl);
64
+ const escapedLabel = escapeHtml(label);
65
+ const escapedUrl = escapeHtml(safeUrl);
66
+ const linkHtml = `<a href="${escapedUrl}">${escapedLabel}</a>`;
67
+ const placeholder = `__LINK_${linkIndex}__`;
68
+ links.push({ placeholder, html: linkHtml });
69
+ linkIndex++;
70
+ return placeholder;
71
+ });
72
+
73
+ // Escape HTML in the rest of the text
74
+ html = escapeHtml(html);
75
+
76
+ // Restore links
77
+ for (const { placeholder, html: linkHtml } of links) {
78
+ html = html.replaceAll(placeholder, linkHtml);
79
+ }
80
+
81
+ // Bold: **text** or __text__
82
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
83
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
84
+
85
+ // Italic: *text* or _text_ (but not inside words)
86
+ html = html.replace(/(?<!\w)\*([^*]+?)\*(?!\w)/g, '<em>$1</em>');
87
+ html = html.replace(/(?<!\w)_([^_]+?)_(?!\w)/g, '<em>$1</em>');
88
+
89
+ // Process lists - need to handle line by line
90
+ const lines = html.split('\n');
91
+ const result: string[] = [];
92
+ let inUnorderedList = false;
93
+ let inOrderedList = false;
94
+
95
+ for (let i = 0; i < lines.length; i++) {
96
+ const line = lines[i];
97
+ const unorderedMatch = line.match(/^[\s]*[-*]\s+(.+)$/);
98
+ const orderedMatch = line.match(/^[\s]*\d+\.\s+(.+)$/);
99
+
100
+ if (unorderedMatch) {
101
+ if (inOrderedList) {
102
+ result.push('</ol>');
103
+ inOrderedList = false;
104
+ }
105
+ if (!inUnorderedList) {
106
+ result.push('<ul>');
107
+ inUnorderedList = true;
108
+ }
109
+ result.push(`<li>${unorderedMatch[1]}</li>`);
110
+ } else if (orderedMatch) {
111
+ if (inUnorderedList) {
112
+ result.push('</ul>');
113
+ inUnorderedList = false;
114
+ }
115
+ if (!inOrderedList) {
116
+ result.push('<ol>');
117
+ inOrderedList = true;
118
+ }
119
+ result.push(`<li>${orderedMatch[1]}</li>`);
120
+ } else {
121
+ // Close any open lists
122
+ if (inUnorderedList) {
123
+ result.push('</ul>');
124
+ inUnorderedList = false;
125
+ }
126
+ if (inOrderedList) {
127
+ result.push('</ol>');
128
+ inOrderedList = false;
129
+ }
130
+ result.push(line);
131
+ }
132
+ }
133
+
134
+ // Close any remaining open lists
135
+ if (inUnorderedList) {
136
+ result.push('</ul>');
137
+ }
138
+ if (inOrderedList) {
139
+ result.push('</ol>');
140
+ }
141
+
142
+ html = result.join('\n');
143
+
144
+ // Convert line breaks to <br> (but not inside lists)
145
+ // Split by list tags, process non-list parts
146
+ html = html.replace(/\n(?!<\/?[uo]l>|<\/?li>)/g, '<br>\n');
147
+
148
+ // Wrap in basic HTML structure for email
149
+ return `<!DOCTYPE html>
150
+ <html>
151
+ <head>
152
+ <meta charset="utf-8">
153
+ <style>
154
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.5; }
155
+ a { color: #0066cc; }
156
+ ul, ol { margin: 10px 0; padding-left: 20px; }
157
+ li { margin: 5px 0; }
158
+ </style>
159
+ </head>
160
+ <body>
161
+ ${html}
162
+ </body>
163
+ </html>`;
164
+ }
165
+
166
+ /**
167
+ * Check if text contains markdown formatting.
168
+ */
169
+ export function hasMarkdown(text: string): boolean {
170
+ // Check for common markdown patterns
171
+ return /\*\*.+?\*\*|__.+?__|(?<!\w)\*[^*]+?\*(?!\w)|(?<!\w)_[^_]+?_(?!\w)|\[.+?\]\(.+?\)|^[\s]*[-*]\s+|^[\s]*\d+\.\s+/m.test(
172
+ text
173
+ );
174
+ }
@@ -0,0 +1,106 @@
1
+ // Lightweight MIME type lookup using a small inline extension map.
2
+ // This replaces the mime-types package (userland, unmaintained since 2019).
3
+ // Only covers common file extensions used in email attachments.
4
+
5
+ const MIME_TYPES: Record<string, string> = {
6
+ '.aac': 'audio/aac',
7
+ '.abw': 'application/x-abiword',
8
+ '.apng': 'image/apng',
9
+ '.arc': 'application/x-freearc',
10
+ '.avif': 'image/avif',
11
+ '.avi': 'video/x-msvideo',
12
+ '.azw': 'application/vnd.amazon.ebook',
13
+ '.bin': 'application/octet-stream',
14
+ '.bmp': 'image/bmp',
15
+ '.bz': 'application/x-bzip',
16
+ '.bz2': 'application/x-bzip2',
17
+ '.cda': 'application/x-cdf',
18
+ '.csh': 'application/x-csh',
19
+ '.class': 'application/java-vm',
20
+ '.css': 'text/css',
21
+ '.csv': 'text/csv',
22
+ '.doc': 'application/msword',
23
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
24
+ '.eot': 'application/vnd.ms-fontobject',
25
+ '.epub': 'application/epub+zip',
26
+ '.gz': 'application/gzip',
27
+ '.gif': 'image/gif',
28
+ '.gpx': 'application/gpx+xml',
29
+ '.htm': 'text/html',
30
+ '.html': 'text/html',
31
+ '.ico': 'image/x-icon',
32
+ '.ics': 'text/calendar',
33
+ '.jar': 'application/java-archive',
34
+ '.jpeg': 'image/jpeg',
35
+ '.jpg': 'image/jpeg',
36
+ '.js': 'text/javascript',
37
+ '.json': 'application/json',
38
+ '.jsonld': 'application/ld+json',
39
+ '.kml': 'application/vnd.google-earth.kml+xml',
40
+ '.kmz': 'application/vnd.google-earth.kmz',
41
+ '.log': 'text/plain',
42
+ '.m3u8': 'application/vnd.apple.mpegurl',
43
+ '.m4a': 'audio/mp4',
44
+ '.md': 'text/markdown',
45
+ '.mid': 'audio/midi',
46
+ '.midi': 'audio/midi',
47
+ '.mjs': 'text/javascript',
48
+ '.mov': 'video/quicktime',
49
+ '.mp3': 'audio/mpeg',
50
+ '.mp4': 'video/mp4',
51
+ '.mpeg': 'video/mpeg',
52
+ '.mpg': 'video/mpeg',
53
+ '.odp': 'application/vnd.oasis.opendocument.presentation',
54
+ '.ods': 'application/vnd.oasis.opendocument.spreadsheet',
55
+ '.odt': 'application/vnd.oasis.opendocument.text',
56
+ '.oga': 'audio/ogg',
57
+ '.ogv': 'video/ogg',
58
+ '.ogx': 'application/ogg',
59
+ '.opus': 'audio/opus',
60
+ '.otf': 'font/otf',
61
+ '.pdf': 'application/pdf',
62
+ '.php': 'application/x-httpd-php',
63
+ '.png': 'image/png',
64
+ '.ppt': 'application/vnd.ms-powerpoint',
65
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
66
+ '.psd': 'image/vnd.adobe.photoshop',
67
+ '.py': 'text/x-python',
68
+ '.rar': 'application/vnd.rar',
69
+ '.rtf': 'application/rtf',
70
+ '.sh': 'application/x-sh',
71
+ '.svg': 'image/svg+xml',
72
+ '.tar': 'application/x-tar',
73
+ '.tbz': 'application/x-bzip-compressed-tar',
74
+ '.tbz2': 'application/x-bzip-compressed-tar',
75
+ '.tgz': 'application/x-tar-gz',
76
+ '.tif': 'image/tiff',
77
+ '.tiff': 'image/tiff',
78
+ '.ts': 'video/mp2t',
79
+ '.ttf': 'font/ttf',
80
+ '.txt': 'text/plain',
81
+ '.vsd': 'application/vnd.visio',
82
+ '.wav': 'audio/wav',
83
+ '.weba': 'audio/webm',
84
+ '.webm': 'video/webm',
85
+ '.webp': 'image/webp',
86
+ '.woff': 'font/woff',
87
+ '.woff2': 'font/woff2',
88
+ '.xhtml': 'application/xhtml+xml',
89
+ '.xls': 'application/vnd.ms-excel',
90
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
91
+ '.xml': 'application/xml',
92
+ '.xul': 'application/vnd.mozilla.xul+xml',
93
+ '.zip': 'application/zip',
94
+ '.3gp': 'video/3gpp',
95
+ '.3g2': 'video/3gpp2',
96
+ '.7z': 'application/x-7z-compressed'
97
+ };
98
+
99
+ /**
100
+ * Look up MIME type by file extension.
101
+ * Returns 'application/octet-stream' for unknown extensions.
102
+ */
103
+ export function lookupMimeType(filename: string): string {
104
+ const ext = filename.toLowerCase().match(/\.[^.]+$/)?.[0];
105
+ return ext ? MIME_TYPES[ext] || 'application/octet-stream' : 'application/octet-stream';
106
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { getMailboxSettings, setMailboxSettings } from './oof-client.js';
3
+
4
+ describe('oof-client', () => {
5
+ const token = 'test-token';
6
+
7
+ it('getMailboxSettings handles GET', async () => {
8
+ const fetchCalls: any[] = [];
9
+ const originalFetch = globalThis.fetch;
10
+ try {
11
+ globalThis.fetch = (async (input, init) => {
12
+ fetchCalls.push({ input, init });
13
+ return new Response(JSON.stringify({ automaticRepliesSetting: { status: 'disabled' } }), {
14
+ status: 200,
15
+ headers: { 'content-type': 'application/json' }
16
+ });
17
+ }) as typeof fetch;
18
+
19
+ const res = await getMailboxSettings(token);
20
+ expect(res.ok).toBe(true);
21
+ expect(res.data?.automaticRepliesSetting?.status).toBe('disabled');
22
+ expect(fetchCalls).toHaveLength(1);
23
+ expect(fetchCalls[0].input.toString()).toContain('/me/mailboxSettings');
24
+ } finally {
25
+ globalThis.fetch = originalFetch;
26
+ }
27
+ });
28
+
29
+ it('setMailboxSettings handles PATCH with scheduled time', async () => {
30
+ const fetchCalls: any[] = [];
31
+ const originalFetch = globalThis.fetch;
32
+ try {
33
+ globalThis.fetch = (async (input, init) => {
34
+ fetchCalls.push({ input, init });
35
+ return new Response('', { status: 204 });
36
+ }) as typeof fetch;
37
+
38
+ const res = await setMailboxSettings(token, {
39
+ status: 'scheduled',
40
+ internalReplyMessage: 'Away',
41
+ scheduledStartDateTime: '2025-01-01T00:00:00.000Z'
42
+ });
43
+
44
+ expect(res.ok).toBe(true);
45
+ expect(fetchCalls).toHaveLength(1);
46
+ expect(fetchCalls[0].init.method).toBe('PATCH');
47
+
48
+ const body = JSON.parse(fetchCalls[0].init.body);
49
+ expect(body.automaticRepliesSetting.status).toBe('scheduled');
50
+ expect(body.automaticRepliesSetting.internalReplyMessage).toBe('Away');
51
+ expect(body.automaticRepliesSetting.scheduledStartDateTime).toEqual({
52
+ dateTime: '2025-01-01T00:00:00.000Z',
53
+ timeZone: 'UTC'
54
+ });
55
+ } finally {
56
+ globalThis.fetch = originalFetch;
57
+ }
58
+ });
59
+ });
@@ -0,0 +1,122 @@
1
+ import { callGraph, GraphApiError, graphError } from './graph-client.js';
2
+ import { graphUserPath } from './graph-user-path.js';
3
+
4
+ export type OofStatus = 'alwaysEnabled' | 'scheduled' | 'disabled';
5
+
6
+ export interface DateTimeTimeZone {
7
+ dateTime: string;
8
+ timeZone: string;
9
+ }
10
+
11
+ export interface AutomaticRepliesSetting {
12
+ status: OofStatus;
13
+ internalReplyMessage?: string;
14
+ externalReplyMessage?: string;
15
+ scheduledStartDateTime?: DateTimeTimeZone;
16
+ scheduledEndDateTime?: DateTimeTimeZone;
17
+ }
18
+
19
+ export interface MailboxSettings {
20
+ automaticRepliesSetting?: AutomaticRepliesSetting;
21
+ timeZone?: string;
22
+ }
23
+
24
+ export interface GetMailboxSettingsResponse {
25
+ automaticRepliesSetting?: AutomaticRepliesSetting;
26
+ timeZone?: string;
27
+ }
28
+
29
+ export async function getMailboxSettings(
30
+ token: string,
31
+ user?: string
32
+ ): Promise<{
33
+ ok: boolean;
34
+ data?: GetMailboxSettingsResponse;
35
+ error?: { message: string; code?: string; status?: number };
36
+ }> {
37
+ try {
38
+ return await callGraph<GetMailboxSettingsResponse>(token, graphUserPath(user, 'mailboxSettings'));
39
+ } catch (err) {
40
+ if (err instanceof GraphApiError) {
41
+ return graphError(err.message, err.code, err.status) as {
42
+ ok: boolean;
43
+ data?: GetMailboxSettingsResponse;
44
+ error?: { message: string; code?: string; status?: number };
45
+ };
46
+ }
47
+ return graphError(err instanceof Error ? err.message : 'Failed to get mailbox settings') as {
48
+ ok: boolean;
49
+ data?: GetMailboxSettingsResponse;
50
+ error?: { message: string; code?: string; status?: number };
51
+ };
52
+ }
53
+ }
54
+
55
+ export async function setMailboxSettings(
56
+ token: string,
57
+ settings: Omit<Partial<AutomaticRepliesSetting>, 'scheduledStartDateTime' | 'scheduledEndDateTime'> & {
58
+ scheduledStartDateTime?: string | DateTimeTimeZone;
59
+ scheduledEndDateTime?: string | DateTimeTimeZone;
60
+ },
61
+ user?: string
62
+ ): Promise<{
63
+ ok: boolean;
64
+ error?: { message: string; code?: string; status?: number };
65
+ }> {
66
+ const payload = {
67
+ automaticRepliesSetting: {
68
+ ...(settings.status !== undefined ? { status: settings.status } : {}),
69
+ ...(settings.internalReplyMessage !== undefined ? { internalReplyMessage: settings.internalReplyMessage } : {}),
70
+ ...(settings.externalReplyMessage !== undefined ? { externalReplyMessage: settings.externalReplyMessage } : {}),
71
+ ...(settings.scheduledStartDateTime !== undefined
72
+ ? {
73
+ scheduledStartDateTime:
74
+ typeof settings.scheduledStartDateTime === 'string'
75
+ ? { dateTime: settings.scheduledStartDateTime, timeZone: 'UTC' }
76
+ : settings.scheduledStartDateTime
77
+ }
78
+ : {}),
79
+ ...(settings.scheduledEndDateTime !== undefined
80
+ ? {
81
+ scheduledEndDateTime:
82
+ typeof settings.scheduledEndDateTime === 'string'
83
+ ? { dateTime: settings.scheduledEndDateTime, timeZone: 'UTC' }
84
+ : settings.scheduledEndDateTime
85
+ }
86
+ : {})
87
+ }
88
+ };
89
+
90
+ let result: any;
91
+ try {
92
+ result = await callGraph<Record<string, never>>(
93
+ token,
94
+ graphUserPath(user, 'mailboxSettings'),
95
+ {
96
+ method: 'PATCH',
97
+ body: JSON.stringify(payload)
98
+ },
99
+ false // don't expect JSON on 204
100
+ );
101
+ } catch (err) {
102
+ if (err instanceof GraphApiError) {
103
+ return {
104
+ ok: false,
105
+ error: { message: err.message, code: err.code, status: err.status }
106
+ };
107
+ }
108
+ return {
109
+ ok: false,
110
+ error: { message: err instanceof Error ? err.message : 'Failed to update mailbox settings' }
111
+ };
112
+ }
113
+
114
+ if (!result.ok) {
115
+ return {
116
+ ok: false,
117
+ error: result.error || { message: 'Failed to update mailbox settings' }
118
+ };
119
+ }
120
+
121
+ return { ok: true };
122
+ }
@@ -0,0 +1,146 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ const token = 'test-token';
4
+ const baseUrl = 'https://graph.microsoft.com/v1.0';
5
+
6
+ describe('listMailFolders', () => {
7
+ it('GETs /mailFolders collection', async () => {
8
+ process.env.GRAPH_BASE_URL = baseUrl;
9
+ const urls: string[] = [];
10
+ const originalFetch = globalThis.fetch;
11
+
12
+ try {
13
+ globalThis.fetch = (async (input: string | URL | Request) => {
14
+ urls.push(typeof input === 'string' ? input : input.toString());
15
+ return new Response(
16
+ JSON.stringify({
17
+ value: [{ id: 'inbox-id', displayName: 'Inbox' }]
18
+ }),
19
+ { status: 200, headers: { 'content-type': 'application/json' } }
20
+ );
21
+ }) as typeof fetch;
22
+
23
+ const { listMailFolders } = await import('./outlook-graph-client.js');
24
+ const r = await listMailFolders(token);
25
+
26
+ expect(r.ok).toBe(true);
27
+ expect(r.data?.[0]?.displayName).toBe('Inbox');
28
+ expect(urls[0]).toContain('/me/mailFolders');
29
+ } finally {
30
+ globalThis.fetch = originalFetch;
31
+ }
32
+ });
33
+ });
34
+
35
+ describe('getMessage', () => {
36
+ it('GETs /messages/{id}', async () => {
37
+ process.env.GRAPH_BASE_URL = baseUrl;
38
+ const urls: string[] = [];
39
+ const originalFetch = globalThis.fetch;
40
+
41
+ try {
42
+ globalThis.fetch = (async (input: string | URL | Request) => {
43
+ urls.push(typeof input === 'string' ? input : input.toString());
44
+ return new Response(JSON.stringify({ id: 'msg-1', subject: 'Hi', isRead: false }), {
45
+ status: 200,
46
+ headers: { 'content-type': 'application/json' }
47
+ });
48
+ }) as typeof fetch;
49
+
50
+ const { getMessage } = await import('./outlook-graph-client.js');
51
+ const r = await getMessage(token, 'msg-1', undefined, 'subject,isRead');
52
+
53
+ expect(r.ok).toBe(true);
54
+ expect(r.data?.subject).toBe('Hi');
55
+ expect(urls[0]).toContain('/me/messages/msg-1');
56
+ expect(urls[0]).toContain('$select=');
57
+ } finally {
58
+ globalThis.fetch = originalFetch;
59
+ }
60
+ });
61
+ });
62
+
63
+ describe('listMailboxMessages', () => {
64
+ it('GETs /me/messages', async () => {
65
+ process.env.GRAPH_BASE_URL = baseUrl;
66
+ const urls: string[] = [];
67
+ const originalFetch = globalThis.fetch;
68
+
69
+ try {
70
+ globalThis.fetch = (async (input: string | URL | Request) => {
71
+ urls.push(typeof input === 'string' ? input : input.toString());
72
+ return new Response(JSON.stringify({ value: [{ id: 'm1', subject: 'A' }] }), {
73
+ status: 200,
74
+ headers: { 'content-type': 'application/json' }
75
+ });
76
+ }) as typeof fetch;
77
+
78
+ const { listMailboxMessages } = await import('./outlook-graph-client.js');
79
+ const r = await listMailboxMessages(token, undefined, { top: 10 });
80
+
81
+ expect(r.ok).toBe(true);
82
+ expect(urls[0]).toContain('/me/messages');
83
+ expect(decodeURIComponent(urls[0])).toContain('$top=10');
84
+ } finally {
85
+ globalThis.fetch = originalFetch;
86
+ }
87
+ });
88
+
89
+ it('adds ConsistencyLevel when using search', async () => {
90
+ process.env.GRAPH_BASE_URL = baseUrl;
91
+ let consistency: string | undefined;
92
+ const originalFetch = globalThis.fetch;
93
+
94
+ try {
95
+ globalThis.fetch = (async (_input: string | URL | Request, init?: RequestInit) => {
96
+ const h = init?.headers;
97
+ if (h instanceof Headers) {
98
+ consistency = h.get('ConsistencyLevel') ?? undefined;
99
+ } else if (h && typeof h === 'object') {
100
+ consistency = (h as Record<string, string>).ConsistencyLevel;
101
+ }
102
+ return new Response(JSON.stringify({ value: [] }), {
103
+ status: 200,
104
+ headers: { 'content-type': 'application/json' }
105
+ });
106
+ }) as typeof fetch;
107
+
108
+ const { listMailboxMessages } = await import('./outlook-graph-client.js');
109
+ const r = await listMailboxMessages(token, undefined, { top: 5, search: 'budget' });
110
+
111
+ expect(r.ok).toBe(true);
112
+ expect(consistency).toBe('eventual');
113
+ } finally {
114
+ globalThis.fetch = originalFetch;
115
+ }
116
+ });
117
+ });
118
+
119
+ describe('sendMail', () => {
120
+ it('POSTs /sendMail', async () => {
121
+ process.env.GRAPH_BASE_URL = baseUrl;
122
+ const urls: string[] = [];
123
+ const bodies: string[] = [];
124
+ const originalFetch = globalThis.fetch;
125
+
126
+ try {
127
+ globalThis.fetch = (async (input: string | URL | Request, init?: RequestInit) => {
128
+ urls.push(typeof input === 'string' ? input : input.toString());
129
+ if (init?.body && typeof init.body === 'string') bodies.push(init.body);
130
+ return new Response(null, { status: 202 });
131
+ }) as typeof fetch;
132
+
133
+ const { sendMail } = await import('./outlook-graph-client.js');
134
+ const r = await sendMail(token, {
135
+ message: { subject: 'Hi', body: { contentType: 'Text', content: 'x' } },
136
+ saveToSentItems: true
137
+ });
138
+
139
+ expect(r.ok).toBe(true);
140
+ expect(urls[0]).toContain('/me/sendMail');
141
+ expect(bodies[0]).toContain('saveToSentItems');
142
+ } finally {
143
+ globalThis.fetch = originalFetch;
144
+ }
145
+ });
146
+ });