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.
- package/LICENSE +22 -0
- package/README.md +916 -0
- package/package.json +50 -0
- package/src/cli.ts +100 -0
- package/src/commands/auto-reply.ts +182 -0
- package/src/commands/calendar.ts +576 -0
- package/src/commands/counter.ts +87 -0
- package/src/commands/create-event.ts +544 -0
- package/src/commands/delegates.ts +286 -0
- package/src/commands/delete-event.ts +321 -0
- package/src/commands/drafts.ts +502 -0
- package/src/commands/files.ts +532 -0
- package/src/commands/find.ts +195 -0
- package/src/commands/findtime.ts +270 -0
- package/src/commands/folders.ts +177 -0
- package/src/commands/forward-event.ts +49 -0
- package/src/commands/graph-calendar.ts +217 -0
- package/src/commands/login.ts +195 -0
- package/src/commands/mail.ts +950 -0
- package/src/commands/oof.ts +263 -0
- package/src/commands/outlook-categories.ts +173 -0
- package/src/commands/outlook-graph.ts +880 -0
- package/src/commands/planner.ts +1678 -0
- package/src/commands/respond.ts +291 -0
- package/src/commands/rooms.ts +210 -0
- package/src/commands/rules.ts +511 -0
- package/src/commands/schedule.ts +109 -0
- package/src/commands/send.ts +204 -0
- package/src/commands/serve.ts +14 -0
- package/src/commands/sharepoint.ts +179 -0
- package/src/commands/site-pages.ts +163 -0
- package/src/commands/subscribe.ts +103 -0
- package/src/commands/subscriptions.ts +29 -0
- package/src/commands/suggest.ts +155 -0
- package/src/commands/todo.ts +2092 -0
- package/src/commands/update-event.ts +608 -0
- package/src/commands/update.ts +88 -0
- package/src/commands/verify-token.ts +62 -0
- package/src/commands/whoami.ts +74 -0
- package/src/index.ts +190 -0
- package/src/lib/atomic-write.ts +20 -0
- package/src/lib/attach-link-spec.test.ts +24 -0
- package/src/lib/attach-link-spec.ts +70 -0
- package/src/lib/attachments.ts +79 -0
- package/src/lib/auth.ts +192 -0
- package/src/lib/calendar-range.test.ts +41 -0
- package/src/lib/calendar-range.ts +103 -0
- package/src/lib/dates.test.ts +74 -0
- package/src/lib/dates.ts +137 -0
- package/src/lib/delegate-client.test.ts +74 -0
- package/src/lib/delegate-client.ts +322 -0
- package/src/lib/ews-client.ts +3418 -0
- package/src/lib/git-commit.ts +4 -0
- package/src/lib/glitchtip-eligibility.ts +220 -0
- package/src/lib/glitchtip.ts +253 -0
- package/src/lib/global-env.ts +3 -0
- package/src/lib/graph-auth.ts +223 -0
- package/src/lib/graph-calendar-client.test.ts +118 -0
- package/src/lib/graph-calendar-client.ts +112 -0
- package/src/lib/graph-client.test.ts +107 -0
- package/src/lib/graph-client.ts +1058 -0
- package/src/lib/graph-constants.ts +12 -0
- package/src/lib/graph-directory.ts +116 -0
- package/src/lib/graph-event.ts +134 -0
- package/src/lib/graph-schedule.ts +173 -0
- package/src/lib/graph-subscriptions.ts +94 -0
- package/src/lib/graph-user-path.ts +13 -0
- package/src/lib/jwt-utils.ts +34 -0
- package/src/lib/markdown.test.ts +21 -0
- package/src/lib/markdown.ts +174 -0
- package/src/lib/mime-type.ts +106 -0
- package/src/lib/oof-client.test.ts +59 -0
- package/src/lib/oof-client.ts +122 -0
- package/src/lib/outlook-graph-client.test.ts +146 -0
- package/src/lib/outlook-graph-client.ts +649 -0
- package/src/lib/outlook-master-categories.ts +145 -0
- package/src/lib/package-info.ts +59 -0
- package/src/lib/places-client.ts +144 -0
- package/src/lib/planner-client.ts +1226 -0
- package/src/lib/rules-client.ts +178 -0
- package/src/lib/sharepoint-client.ts +101 -0
- package/src/lib/site-pages-client.ts +73 -0
- package/src/lib/todo-client.test.ts +298 -0
- package/src/lib/todo-client.ts +1309 -0
- package/src/lib/url-validation.ts +40 -0
- package/src/lib/utils.ts +45 -0
- package/src/lib/webhook-server.ts +51 -0
- package/src/test/auth.test.ts +104 -0
- package/src/test/cli.integration.test.ts +1083 -0
- package/src/test/ews-client.test.ts +268 -0
- package/src/test/mocks/index.ts +375 -0
- package/src/test/mocks/responses.ts +861 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
function escapeHtml(value: string): string {
|
|
2
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
});
|