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,950 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { AttachmentLinkSpecError, parseAttachLinkSpec } from '../lib/attach-link-spec.js';
|
|
5
|
+
import { AttachmentPathError, validateAttachmentPath } from '../lib/attachments.js';
|
|
6
|
+
import { resolveAuth } from '../lib/auth.js';
|
|
7
|
+
import { parseDay, toLocalUnzonedISOString } from '../lib/dates.js';
|
|
8
|
+
import {
|
|
9
|
+
addAttachmentToDraft,
|
|
10
|
+
addReferenceAttachmentToDraft,
|
|
11
|
+
forwardEmail,
|
|
12
|
+
forwardEmailDraft,
|
|
13
|
+
getAttachment,
|
|
14
|
+
getAttachments,
|
|
15
|
+
getEmail,
|
|
16
|
+
getEmails,
|
|
17
|
+
getMailFolders,
|
|
18
|
+
moveEmail,
|
|
19
|
+
replyToEmail,
|
|
20
|
+
replyToEmailDraft,
|
|
21
|
+
SENSITIVITY_MAP,
|
|
22
|
+
sendDraftById,
|
|
23
|
+
updateDraft,
|
|
24
|
+
updateEmail
|
|
25
|
+
} from '../lib/ews-client.js';
|
|
26
|
+
import { markdownToHtml } from '../lib/markdown.js';
|
|
27
|
+
import { lookupMimeType } from '../lib/mime-type.js';
|
|
28
|
+
import { checkReadOnly } from '../lib/utils.js';
|
|
29
|
+
|
|
30
|
+
function formatDate(dateStr: string): string {
|
|
31
|
+
const date = new Date(dateStr);
|
|
32
|
+
const now = new Date();
|
|
33
|
+
const isToday = date.toDateString() === now.toDateString();
|
|
34
|
+
const yesterday = new Date(now);
|
|
35
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
36
|
+
const isYesterday = date.toDateString() === yesterday.toDateString();
|
|
37
|
+
|
|
38
|
+
if (isToday) {
|
|
39
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
40
|
+
} else if (isYesterday) {
|
|
41
|
+
return 'Yesterday';
|
|
42
|
+
} else {
|
|
43
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function truncate(str: string, maxLen: number): string {
|
|
48
|
+
if (!str) return '';
|
|
49
|
+
str = str.replace(/\s+/g, ' ').trim();
|
|
50
|
+
if (str.length <= maxLen) return str;
|
|
51
|
+
return `${str.substring(0, maxLen - 1)}\u2026`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function applyDraftCategoriesAttachments(
|
|
55
|
+
token: string,
|
|
56
|
+
draftId: string,
|
|
57
|
+
mailbox: string | undefined,
|
|
58
|
+
opts: {
|
|
59
|
+
withCategory?: string[];
|
|
60
|
+
attach?: string;
|
|
61
|
+
attachLink?: string[];
|
|
62
|
+
json?: boolean;
|
|
63
|
+
}
|
|
64
|
+
): Promise<void> {
|
|
65
|
+
const cats = (opts.withCategory ?? []).map((c) => c.trim()).filter(Boolean);
|
|
66
|
+
if (cats.length > 0) {
|
|
67
|
+
const r = await updateDraft(token, draftId, { categories: cats, mailbox });
|
|
68
|
+
if (!r.ok) {
|
|
69
|
+
console.error(`Error: ${r.error?.message || 'Failed to set categories on draft'}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const wd = process.cwd();
|
|
74
|
+
if (opts.attach) {
|
|
75
|
+
const filePaths = opts.attach
|
|
76
|
+
.split(',')
|
|
77
|
+
.map((f) => f.trim())
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
for (const filePath of filePaths) {
|
|
80
|
+
try {
|
|
81
|
+
const validated = await validateAttachmentPath(filePath, wd);
|
|
82
|
+
const content = await readFile(validated.absolutePath);
|
|
83
|
+
const contentType = lookupMimeType(validated.fileName) || 'application/octet-stream';
|
|
84
|
+
const ar = await addAttachmentToDraft(
|
|
85
|
+
token,
|
|
86
|
+
draftId,
|
|
87
|
+
{ name: validated.fileName, contentType, contentBytes: content.toString('base64') },
|
|
88
|
+
mailbox
|
|
89
|
+
);
|
|
90
|
+
if (!ar.ok) {
|
|
91
|
+
console.error(`Failed to attach ${validated.fileName}: ${ar.error?.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
if (!opts.json) console.log(` Attached: ${validated.fileName}`);
|
|
95
|
+
} catch (err) {
|
|
96
|
+
if (err instanceof AttachmentPathError) {
|
|
97
|
+
console.error(err.message);
|
|
98
|
+
} else {
|
|
99
|
+
console.error(`Failed to read attachment: ${filePath}`);
|
|
100
|
+
}
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
for (const spec of opts.attachLink ?? []) {
|
|
106
|
+
try {
|
|
107
|
+
const { name, url } = parseAttachLinkSpec(spec);
|
|
108
|
+
const linkRes = await addReferenceAttachmentToDraft(
|
|
109
|
+
token,
|
|
110
|
+
draftId,
|
|
111
|
+
{ name, url, contentType: 'text/html' },
|
|
112
|
+
mailbox
|
|
113
|
+
);
|
|
114
|
+
if (!linkRes.ok) {
|
|
115
|
+
console.error(`Failed to attach link ${name}: ${linkRes.error?.message}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (!opts.json) console.log(` Attached link: ${name}`);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const msg =
|
|
121
|
+
err instanceof AttachmentLinkSpecError ? err.message : err instanceof Error ? err.message : String(err);
|
|
122
|
+
console.error(`Invalid --attach-link: ${msg}`);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export const mailCommand = new Command('mail')
|
|
129
|
+
.description('List and read emails')
|
|
130
|
+
.argument('[folder]', 'Folder: inbox, sent, drafts, deleted, archive, junk', 'inbox')
|
|
131
|
+
.option('-n, --limit <number>', 'Number of emails to show', '10')
|
|
132
|
+
.option('-p, --page <number>', 'Page number (1-based)', '1')
|
|
133
|
+
.option('--unread', 'Show only unread emails')
|
|
134
|
+
.option('--flagged', 'Show only flagged emails')
|
|
135
|
+
.option('-s, --search <query>', 'Search emails (subject, body, sender)')
|
|
136
|
+
.option('-r, --read <id>', 'Read email by ID')
|
|
137
|
+
.option('-d, --download <id>', 'Download attachments from email by ID')
|
|
138
|
+
.option('-o, --output <dir>', 'Output directory for attachments', '.')
|
|
139
|
+
.option('--mark-read <id>', 'Mark email as read (by ID)')
|
|
140
|
+
.option('--mark-unread <id>', 'Mark email as unread (by ID)')
|
|
141
|
+
.option('--flag <id>', 'Flag email (by ID)')
|
|
142
|
+
.option('--start-date <date>', 'Start date for flag (YYYY-MM-DD)')
|
|
143
|
+
.option('--due <date>', 'Due date for flag (YYYY-MM-DD)')
|
|
144
|
+
.option('--unflag <id>', 'Remove flag (by ID)')
|
|
145
|
+
.option('--complete <id>', 'Mark flagged email as complete (by ID)')
|
|
146
|
+
.option('--sensitivity <id>', 'Set sensitivity on email by ID (use with --level)')
|
|
147
|
+
.option('--level <level>', 'Sensitivity level: normal, personal, private, confidential')
|
|
148
|
+
.option('--move <id>', 'Move email to folder (use with --to)')
|
|
149
|
+
.option('--to <folder>', 'Destination folder for move (inbox, archive, deleted, junk)')
|
|
150
|
+
.option('--reply <id>', 'Reply to email by ID')
|
|
151
|
+
.option('--reply-all <id>', 'Reply all to email by ID')
|
|
152
|
+
.option('--draft', 'Create a reply or forward draft (do not send); use with --reply, --reply-all, or --forward')
|
|
153
|
+
.option('--forward <id>', 'Forward email by ID (use with --to-addr)')
|
|
154
|
+
.option('--to-addr <emails>', 'Forward recipients (comma-separated)')
|
|
155
|
+
.option('--message <text>', 'Reply/forward message text')
|
|
156
|
+
.option('--attach <files>', 'On reply/forward: comma-separated file paths (uses draft + send)')
|
|
157
|
+
.option(
|
|
158
|
+
'--attach-link <spec>',
|
|
159
|
+
'On reply/forward: link attachment (repeatable)',
|
|
160
|
+
(v: string, prev: string[]) => [...prev, v],
|
|
161
|
+
[] as string[]
|
|
162
|
+
)
|
|
163
|
+
.option(
|
|
164
|
+
'--with-category <name>',
|
|
165
|
+
'On reply/forward: Outlook category on outgoing message (repeatable; not for --set-categories)',
|
|
166
|
+
(v: string, prev: string[]) => [...prev, v],
|
|
167
|
+
[] as string[]
|
|
168
|
+
)
|
|
169
|
+
.option('--markdown', 'Parse message as markdown (bold, links, lists)')
|
|
170
|
+
.option('--json', 'Output as JSON')
|
|
171
|
+
.option('--token <token>', 'Use a specific token')
|
|
172
|
+
.option(
|
|
173
|
+
'--mailbox <email>',
|
|
174
|
+
'Delegated or shared mailbox (list, read, move, flags, attachments, reply, forward; X-AnchorMailbox)'
|
|
175
|
+
)
|
|
176
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
177
|
+
.option('--set-categories <id>', 'Set categories on message (use with --category, repeatable)')
|
|
178
|
+
.option('--clear-categories <id>', 'Remove all categories from message')
|
|
179
|
+
.option(
|
|
180
|
+
'--category <name>',
|
|
181
|
+
'Category label (repeatable; use with --set-categories)',
|
|
182
|
+
(v: string, prev: string[]) => [...prev, v],
|
|
183
|
+
[] as string[]
|
|
184
|
+
)
|
|
185
|
+
.action(
|
|
186
|
+
async (
|
|
187
|
+
folder: string,
|
|
188
|
+
options: {
|
|
189
|
+
limit: string;
|
|
190
|
+
page: string;
|
|
191
|
+
unread?: boolean;
|
|
192
|
+
flagged?: boolean;
|
|
193
|
+
search?: string;
|
|
194
|
+
read?: string;
|
|
195
|
+
download?: string;
|
|
196
|
+
output: string;
|
|
197
|
+
force?: boolean;
|
|
198
|
+
markRead?: string;
|
|
199
|
+
markUnread?: string;
|
|
200
|
+
flag?: string;
|
|
201
|
+
startDate?: string;
|
|
202
|
+
due?: string;
|
|
203
|
+
unflag?: string;
|
|
204
|
+
complete?: string;
|
|
205
|
+
sensitivity?: string;
|
|
206
|
+
level?: string;
|
|
207
|
+
move?: string;
|
|
208
|
+
to?: string;
|
|
209
|
+
reply?: string;
|
|
210
|
+
replyAll?: string;
|
|
211
|
+
forward?: string;
|
|
212
|
+
toAddr?: string;
|
|
213
|
+
message?: string;
|
|
214
|
+
markdown?: boolean;
|
|
215
|
+
json?: boolean;
|
|
216
|
+
token?: string;
|
|
217
|
+
draft?: boolean;
|
|
218
|
+
mailbox?: string;
|
|
219
|
+
identity?: string;
|
|
220
|
+
setCategories?: string;
|
|
221
|
+
clearCategories?: string;
|
|
222
|
+
category?: string[];
|
|
223
|
+
attach?: string;
|
|
224
|
+
attachLink?: string[];
|
|
225
|
+
withCategory?: string[];
|
|
226
|
+
},
|
|
227
|
+
cmd: any
|
|
228
|
+
) => {
|
|
229
|
+
const isMutating =
|
|
230
|
+
options.flag ||
|
|
231
|
+
options.unflag ||
|
|
232
|
+
options.markRead ||
|
|
233
|
+
options.markUnread ||
|
|
234
|
+
options.complete ||
|
|
235
|
+
options.sensitivity ||
|
|
236
|
+
options.move ||
|
|
237
|
+
options.reply ||
|
|
238
|
+
options.replyAll ||
|
|
239
|
+
options.forward ||
|
|
240
|
+
options.setCategories ||
|
|
241
|
+
options.clearCategories;
|
|
242
|
+
|
|
243
|
+
if (isMutating) {
|
|
244
|
+
checkReadOnly(cmd);
|
|
245
|
+
}
|
|
246
|
+
const authResult = await resolveAuth({
|
|
247
|
+
token: options.token,
|
|
248
|
+
identity: options.identity
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (!authResult.success) {
|
|
252
|
+
if (options.json) {
|
|
253
|
+
console.log(JSON.stringify({ error: authResult.error }, null, 2));
|
|
254
|
+
} else {
|
|
255
|
+
console.error(`Error: ${authResult.error}`);
|
|
256
|
+
console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
|
|
257
|
+
}
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Map folder names to API folder IDs
|
|
262
|
+
const folderMap: Record<string, string> = {
|
|
263
|
+
inbox: 'inbox',
|
|
264
|
+
sent: 'sentitems',
|
|
265
|
+
sentitems: 'sentitems',
|
|
266
|
+
drafts: 'drafts',
|
|
267
|
+
deleted: 'deleteditems',
|
|
268
|
+
deleteditems: 'deleteditems',
|
|
269
|
+
trash: 'deleteditems',
|
|
270
|
+
archive: 'archive',
|
|
271
|
+
junk: 'junkemail',
|
|
272
|
+
junkemail: 'junkemail',
|
|
273
|
+
spam: 'junkemail'
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
let apiFolder = folderMap[folder.toLowerCase()];
|
|
277
|
+
|
|
278
|
+
// If not a well-known folder, look up by name
|
|
279
|
+
if (!apiFolder) {
|
|
280
|
+
const foldersResult = await getMailFolders(authResult.token!, undefined, options.mailbox);
|
|
281
|
+
if (foldersResult.ok && foldersResult.data) {
|
|
282
|
+
const found = foldersResult.data.value.find((f) => f.DisplayName.toLowerCase() === folder.toLowerCase());
|
|
283
|
+
if (found) {
|
|
284
|
+
apiFolder = found.Id;
|
|
285
|
+
} else {
|
|
286
|
+
console.error(`Folder "${folder}" not found.`);
|
|
287
|
+
console.error('Use "m365-agent-cli folders" to see available folders.');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
apiFolder = folder; // Fallback to using the name directly
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const limit = parseInt(options.limit, 10) || 10;
|
|
296
|
+
const page = parseInt(options.page, 10) || 1;
|
|
297
|
+
const skip = (page - 1) * limit;
|
|
298
|
+
|
|
299
|
+
const result = await getEmails({
|
|
300
|
+
token: authResult.token!,
|
|
301
|
+
folder: apiFolder,
|
|
302
|
+
mailbox: options.mailbox,
|
|
303
|
+
top: limit,
|
|
304
|
+
skip,
|
|
305
|
+
search: options.search,
|
|
306
|
+
isRead: options.unread ? false : undefined,
|
|
307
|
+
flagStatus: options.flagged ? 'Flagged' : undefined
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
if (!result.ok || !result.data) {
|
|
311
|
+
if (options.json) {
|
|
312
|
+
console.log(JSON.stringify({ error: result.error?.message || 'Failed to fetch emails' }, null, 2));
|
|
313
|
+
} else {
|
|
314
|
+
console.error(`Error: ${result.error?.message || 'Failed to fetch emails'}`);
|
|
315
|
+
}
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const emails = result.data.value;
|
|
320
|
+
|
|
321
|
+
// Handle reading a specific email
|
|
322
|
+
if (options.read) {
|
|
323
|
+
const id = options.read.trim();
|
|
324
|
+
const fullEmail = await getEmail(authResult.token!, id, options.mailbox);
|
|
325
|
+
|
|
326
|
+
if (!fullEmail.ok || !fullEmail.data) {
|
|
327
|
+
console.error(`Error: ${fullEmail.error?.message || 'Failed to fetch email'}`);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const email = fullEmail.data;
|
|
332
|
+
|
|
333
|
+
if (options.json) {
|
|
334
|
+
console.log(JSON.stringify(email, null, 2));
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
console.log(`\n${'\u2500'.repeat(60)}`);
|
|
339
|
+
console.log(`From: ${email.From?.EmailAddress?.Name || email.From?.EmailAddress?.Address || 'Unknown'}`);
|
|
340
|
+
if (email.From?.EmailAddress?.Address) {
|
|
341
|
+
console.log(` <${email.From.EmailAddress.Address}>`);
|
|
342
|
+
}
|
|
343
|
+
console.log(`Subject: ${email.Subject || '(no subject)'}`);
|
|
344
|
+
console.log(`Date: ${email.ReceivedDateTime ? new Date(email.ReceivedDateTime).toLocaleString() : 'Unknown'}`);
|
|
345
|
+
if (email.Categories?.length) {
|
|
346
|
+
console.log(`Categories: ${email.Categories.join(', ')}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (email.ToRecipients && email.ToRecipients.length > 0) {
|
|
350
|
+
const to = email.ToRecipients.map((r) => r.EmailAddress?.Address)
|
|
351
|
+
.filter(Boolean)
|
|
352
|
+
.join(', ');
|
|
353
|
+
console.log(`To: ${to}`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (email.CcRecipients && email.CcRecipients.length > 0) {
|
|
357
|
+
const cc = email.CcRecipients.map((r) => r.EmailAddress?.Address)
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.join(', ');
|
|
360
|
+
console.log(`Cc: ${cc}`);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (email.HasAttachments) {
|
|
364
|
+
const attachmentsResult = await getAttachments(authResult.token!, email.Id, options.mailbox);
|
|
365
|
+
if (attachmentsResult.ok && attachmentsResult.data) {
|
|
366
|
+
const atts = attachmentsResult.data.value.filter((a) => !a.IsInline);
|
|
367
|
+
if (atts.length > 0) {
|
|
368
|
+
console.log('Attachments:');
|
|
369
|
+
for (const att of atts) {
|
|
370
|
+
if (att.Kind === 'reference' && att.AttachLongPathName) {
|
|
371
|
+
console.log(` - ${att.Name} (link)`);
|
|
372
|
+
console.log(` ${att.AttachLongPathName}`);
|
|
373
|
+
} else {
|
|
374
|
+
const sizeKB = Math.round(att.Size / 1024);
|
|
375
|
+
console.log(` - ${att.Name} (${sizeKB} KB)`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(`${'\u2500'.repeat(60)}\n`);
|
|
383
|
+
console.log(email.Body?.Content || email.BodyPreview || '(no content)');
|
|
384
|
+
console.log(`\n${'\u2500'.repeat(60)}\n`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Handle downloading attachments
|
|
389
|
+
if (options.download) {
|
|
390
|
+
const id = options.download.trim();
|
|
391
|
+
const emailSummary = await getEmail(authResult.token!, id, options.mailbox);
|
|
392
|
+
if (!emailSummary.ok || !emailSummary.data) {
|
|
393
|
+
console.error(`Error: ${emailSummary.error?.message || 'Failed to fetch email'}`);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (!emailSummary.data.HasAttachments) {
|
|
398
|
+
console.log('This email has no attachments.');
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const attachmentsResult = await getAttachments(authResult.token!, emailSummary.data.Id, options.mailbox);
|
|
403
|
+
if (!attachmentsResult.ok || !attachmentsResult.data) {
|
|
404
|
+
console.error(`Error: ${attachmentsResult.error?.message || 'Failed to fetch attachments'}`);
|
|
405
|
+
process.exit(1);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const attachments = attachmentsResult.data.value.filter((a) => !a.IsInline);
|
|
409
|
+
|
|
410
|
+
if (attachments.length === 0) {
|
|
411
|
+
console.log('This email has no downloadable attachments.');
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Ensure output directory exists
|
|
416
|
+
await mkdir(options.output, { recursive: true });
|
|
417
|
+
|
|
418
|
+
console.log(`\nDownloading ${attachments.length} attachment(s) to ${options.output}/\n`);
|
|
419
|
+
|
|
420
|
+
const usedPaths = new Set<string>();
|
|
421
|
+
|
|
422
|
+
for (const att of attachments) {
|
|
423
|
+
if (att.Kind === 'reference' || att.AttachLongPathName) {
|
|
424
|
+
let url = att.AttachLongPathName;
|
|
425
|
+
if (!url) {
|
|
426
|
+
const fullRef = await getAttachment(authResult.token!, emailSummary.data.Id, att.Id, options.mailbox);
|
|
427
|
+
if (fullRef.ok && fullRef.data?.AttachLongPathName) {
|
|
428
|
+
url = fullRef.data.AttachLongPathName;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!url) {
|
|
432
|
+
console.error(` Failed to resolve link: ${att.Name}`);
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const safeBase = (att.Name || 'link').replace(/[/\\?%*:|"<>]/g, '_').trim() || 'link';
|
|
436
|
+
let filePath = join(options.output, `${safeBase}.url`);
|
|
437
|
+
let counter = 1;
|
|
438
|
+
while (usedPaths.has(filePath)) {
|
|
439
|
+
filePath = join(options.output, `${safeBase} (${counter}).url`);
|
|
440
|
+
counter++;
|
|
441
|
+
}
|
|
442
|
+
if (!options.force) {
|
|
443
|
+
while (true) {
|
|
444
|
+
try {
|
|
445
|
+
await access(filePath);
|
|
446
|
+
filePath = join(options.output, `${safeBase} (${counter}).url`);
|
|
447
|
+
counter++;
|
|
448
|
+
} catch {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
usedPaths.add(filePath);
|
|
454
|
+
const shortcut = `[InternetShortcut]\r\nURL=${url}\r\n`;
|
|
455
|
+
await writeFile(filePath, shortcut, 'utf8');
|
|
456
|
+
console.log(` \u2713 ${filePath.split(/[\\/]/).pop()} (link)`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const fullAtt = await getAttachment(authResult.token!, emailSummary.data.Id, att.Id, options.mailbox);
|
|
461
|
+
if (!fullAtt.ok || !fullAtt.data?.ContentBytes) {
|
|
462
|
+
console.error(` Failed to download: ${att.Name}`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const content = Buffer.from(fullAtt.data.ContentBytes, 'base64');
|
|
467
|
+
|
|
468
|
+
// Resolve the actual file path, avoiding collisions and existing files
|
|
469
|
+
let filePath = join(options.output, att.Name);
|
|
470
|
+
let counter = 1;
|
|
471
|
+
while (true) {
|
|
472
|
+
// Always check for intra-download collisions
|
|
473
|
+
if (usedPaths.has(filePath)) {
|
|
474
|
+
const ext = extname(att.Name);
|
|
475
|
+
const base = att.Name.slice(0, att.Name.length - ext.length);
|
|
476
|
+
filePath = join(options.output, `${base} (${counter})${ext}`);
|
|
477
|
+
counter++;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Check for pre-existing files only if --force is not set
|
|
482
|
+
if (!options.force) {
|
|
483
|
+
try {
|
|
484
|
+
await access(filePath);
|
|
485
|
+
// File exists — resolve collision with a numeric suffix
|
|
486
|
+
const ext = extname(att.Name);
|
|
487
|
+
const base = att.Name.slice(0, att.Name.length - ext.length);
|
|
488
|
+
filePath = join(options.output, `${base} (${counter})${ext}`);
|
|
489
|
+
counter++;
|
|
490
|
+
continue;
|
|
491
|
+
} catch {
|
|
492
|
+
// File doesn't exist — safe to use
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Path is safe to use
|
|
497
|
+
break;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
usedPaths.add(filePath);
|
|
501
|
+
await writeFile(filePath, content);
|
|
502
|
+
|
|
503
|
+
const sizeKB = Math.round(content.length / 1024);
|
|
504
|
+
const written = filePath === join(options.output, att.Name) ? att.Name : filePath.split(/[\\/]/).pop();
|
|
505
|
+
console.log(` \u2713 ${written} (${sizeKB} KB)`);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
console.log('\nDone.\n');
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Handle mark as read/unread
|
|
513
|
+
if (options.markRead || options.markUnread) {
|
|
514
|
+
const id = (options.markRead || options.markUnread)?.trim();
|
|
515
|
+
if (!id) {
|
|
516
|
+
console.error('Error: --mark-read/--mark-unread requires a message ID');
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
519
|
+
const isRead = !!options.markRead;
|
|
520
|
+
|
|
521
|
+
const result = await updateEmail(authResult.token!, id, { IsRead: isRead }, options.mailbox);
|
|
522
|
+
|
|
523
|
+
if (!result.ok) {
|
|
524
|
+
console.error(`Error: ${result.error?.message || 'Failed to update email'}`);
|
|
525
|
+
process.exit(1);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
console.log(`\u2713 Marked as ${isRead ? 'read' : 'unread'}: ${id}`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Handle Outlook categories (names; colors come from mailbox master list in Outlook)
|
|
533
|
+
if (options.setCategories || options.clearCategories) {
|
|
534
|
+
const id = (options.setCategories || options.clearCategories)?.trim();
|
|
535
|
+
if (!id) {
|
|
536
|
+
console.error('Error: --set-categories/--clear-categories requires a message ID');
|
|
537
|
+
process.exit(1);
|
|
538
|
+
}
|
|
539
|
+
if (options.setCategories && options.clearCategories) {
|
|
540
|
+
console.error('Error: use either --set-categories or --clear-categories, not both');
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
if (options.setCategories) {
|
|
544
|
+
const cats = (options.category ?? []).map((c) => c.trim()).filter(Boolean);
|
|
545
|
+
if (cats.length === 0) {
|
|
546
|
+
console.error('Error: --set-categories requires at least one --category <name>');
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
const result = await updateEmail(authResult.token!, id, { categories: cats }, options.mailbox);
|
|
550
|
+
if (!result.ok) {
|
|
551
|
+
console.error(`Error: ${result.error?.message || 'Failed to set categories'}`);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
console.log(`\u2713 Categories set (${cats.join(', ')}): ${id}`);
|
|
555
|
+
} else {
|
|
556
|
+
const result = await updateEmail(authResult.token!, id, { clearCategories: true }, options.mailbox);
|
|
557
|
+
if (!result.ok) {
|
|
558
|
+
console.error(`Error: ${result.error?.message || 'Failed to clear categories'}`);
|
|
559
|
+
process.exit(1);
|
|
560
|
+
}
|
|
561
|
+
console.log(`\u2713 Categories cleared: ${id}`);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Handle flag/unflag/complete
|
|
567
|
+
if (options.flag || options.unflag || options.complete) {
|
|
568
|
+
const id = (options.flag || options.unflag || options.complete)?.trim();
|
|
569
|
+
let flagStatus: 'NotFlagged' | 'Flagged' | 'Complete';
|
|
570
|
+
let actionLabel: string;
|
|
571
|
+
let startDate: { DateTime: string; TimeZone: string } | undefined;
|
|
572
|
+
let dueDate: { DateTime: string; TimeZone: string } | undefined;
|
|
573
|
+
|
|
574
|
+
if (options.flag) {
|
|
575
|
+
flagStatus = 'Flagged';
|
|
576
|
+
actionLabel = 'Flagged';
|
|
577
|
+
|
|
578
|
+
if (options.startDate) {
|
|
579
|
+
let parsedStartDate: Date;
|
|
580
|
+
try {
|
|
581
|
+
parsedStartDate = parseDay(options.startDate, { throwOnInvalid: true });
|
|
582
|
+
} catch (err) {
|
|
583
|
+
console.error(`Error: Invalid start date: ${err instanceof Error ? err.message : String(err)}`);
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
startDate = { DateTime: toLocalUnzonedISOString(parsedStartDate), TimeZone: 'UTC' };
|
|
587
|
+
}
|
|
588
|
+
if (options.due) {
|
|
589
|
+
let parsedDueDate: Date;
|
|
590
|
+
try {
|
|
591
|
+
parsedDueDate = parseDay(options.due, { throwOnInvalid: true });
|
|
592
|
+
} catch (err) {
|
|
593
|
+
console.error(`Error: Invalid due date: ${err instanceof Error ? err.message : String(err)}`);
|
|
594
|
+
process.exit(1);
|
|
595
|
+
}
|
|
596
|
+
dueDate = { DateTime: toLocalUnzonedISOString(parsedDueDate), TimeZone: 'UTC' };
|
|
597
|
+
}
|
|
598
|
+
} else if (options.complete) {
|
|
599
|
+
flagStatus = 'Complete';
|
|
600
|
+
actionLabel = 'Marked complete';
|
|
601
|
+
} else {
|
|
602
|
+
flagStatus = 'NotFlagged';
|
|
603
|
+
actionLabel = 'Unflagged';
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (!id) {
|
|
607
|
+
console.error('Error: --flag/--unflag/--complete requires a message ID');
|
|
608
|
+
process.exit(1);
|
|
609
|
+
}
|
|
610
|
+
const result = await updateEmail(
|
|
611
|
+
authResult.token!,
|
|
612
|
+
id,
|
|
613
|
+
{
|
|
614
|
+
Flag: { FlagStatus: flagStatus, StartDate: startDate, DueDate: dueDate }
|
|
615
|
+
},
|
|
616
|
+
options.mailbox
|
|
617
|
+
);
|
|
618
|
+
|
|
619
|
+
if (!result.ok) {
|
|
620
|
+
console.error(`Error: ${result.error?.message || 'Failed to update email'}`);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
console.log(`\u2713 ${actionLabel}: ${id}`);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Handle sensitivity
|
|
629
|
+
if (options.sensitivity) {
|
|
630
|
+
const id = options.sensitivity.trim();
|
|
631
|
+
|
|
632
|
+
if (!options.level) {
|
|
633
|
+
console.error('Error: --sensitivity requires --level to be specified');
|
|
634
|
+
console.error('Example: m365-agent-cli mail --sensitivity <id> --level personal');
|
|
635
|
+
console.error('Levels: normal, personal, private, confidential');
|
|
636
|
+
process.exit(1);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const sensitivity = SENSITIVITY_MAP[options.level.toLowerCase()];
|
|
640
|
+
|
|
641
|
+
if (!sensitivity) {
|
|
642
|
+
console.error(`Invalid sensitivity level: ${options.level}`);
|
|
643
|
+
console.error('Valid levels: normal, personal, private, confidential');
|
|
644
|
+
process.exit(1);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const result = await updateEmail(authResult.token!, id, { Sensitivity: sensitivity }, options.mailbox);
|
|
648
|
+
|
|
649
|
+
if (!result.ok) {
|
|
650
|
+
console.error(`Error: ${result.error?.message || 'Failed to update email sensitivity'}`);
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
console.log(`\u2713 Sensitivity set to ${sensitivity}: ${id}`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Handle move
|
|
659
|
+
if (options.move) {
|
|
660
|
+
if (!options.to) {
|
|
661
|
+
console.error('Please specify destination folder with --to');
|
|
662
|
+
console.error('Example: m365-agent-cli mail --move <id> --to archive');
|
|
663
|
+
console.error('Folders: inbox, archive, deleted, junk, drafts, sent');
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const id = options.move.trim();
|
|
668
|
+
|
|
669
|
+
// Map folder names to API folder IDs
|
|
670
|
+
const destFolderMap: Record<string, string> = {
|
|
671
|
+
inbox: 'inbox',
|
|
672
|
+
archive: 'archive',
|
|
673
|
+
deleted: 'deleteditems',
|
|
674
|
+
deleteditems: 'deleteditems',
|
|
675
|
+
trash: 'deleteditems',
|
|
676
|
+
junk: 'junkemail',
|
|
677
|
+
junkemail: 'junkemail',
|
|
678
|
+
spam: 'junkemail',
|
|
679
|
+
drafts: 'drafts',
|
|
680
|
+
sent: 'sentitems',
|
|
681
|
+
sentitems: 'sentitems'
|
|
682
|
+
};
|
|
683
|
+
|
|
684
|
+
let destFolder = destFolderMap[options.to.toLowerCase()];
|
|
685
|
+
|
|
686
|
+
// If not a well-known folder, look up by name
|
|
687
|
+
if (!destFolder) {
|
|
688
|
+
const foldersResult = await getMailFolders(authResult.token!, undefined, options.mailbox);
|
|
689
|
+
if (foldersResult.ok && foldersResult.data) {
|
|
690
|
+
const found = foldersResult.data.value.find(
|
|
691
|
+
(f) => f.DisplayName.toLowerCase() === options.to?.toLowerCase()
|
|
692
|
+
);
|
|
693
|
+
if (found) {
|
|
694
|
+
destFolder = found.Id;
|
|
695
|
+
} else {
|
|
696
|
+
console.error(`Folder "${options.to}" not found.`);
|
|
697
|
+
console.error('Use "m365-agent-cli folders" to see available folders.');
|
|
698
|
+
process.exit(1);
|
|
699
|
+
}
|
|
700
|
+
} else {
|
|
701
|
+
console.error('Failed to look up folder.');
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
const result = await moveEmail(authResult.token!, id, destFolder, options.mailbox);
|
|
707
|
+
|
|
708
|
+
if (!result.ok) {
|
|
709
|
+
console.error(`Error: ${result.error?.message || 'Failed to move email'}`);
|
|
710
|
+
process.exit(1);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const folderDisplay = options.to.charAt(0).toUpperCase() + options.to.slice(1);
|
|
714
|
+
console.log(`\u2713 Moved to ${folderDisplay}: ${id}`);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Handle reply
|
|
719
|
+
if (options.reply || options.replyAll) {
|
|
720
|
+
const id = (options.reply || options.replyAll)?.trim();
|
|
721
|
+
|
|
722
|
+
if (!id) {
|
|
723
|
+
console.error('Error: --reply/--reply-all requires a message ID');
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if (!options.message) {
|
|
728
|
+
console.error('Please provide reply text with --message');
|
|
729
|
+
console.error('Example: m365-agent-cli mail --reply <id> --message "Thanks for your email!"');
|
|
730
|
+
process.exit(1);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const isReplyAll = !!options.replyAll;
|
|
734
|
+
|
|
735
|
+
let message = options.message;
|
|
736
|
+
let isHtml = false;
|
|
737
|
+
|
|
738
|
+
if (options.markdown) {
|
|
739
|
+
message = markdownToHtml(options.message);
|
|
740
|
+
isHtml = true;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const withCat = options.withCategory ?? [];
|
|
744
|
+
const hasAttach = !!options.attach?.trim();
|
|
745
|
+
const hasLinks = (options.attachLink?.length ?? 0) > 0;
|
|
746
|
+
const hasOutgoingExtras = hasAttach || hasLinks || withCat.filter((c) => c.trim()).length > 0;
|
|
747
|
+
|
|
748
|
+
if (options.draft && !hasOutgoingExtras) {
|
|
749
|
+
const result = await replyToEmailDraft(authResult.token!, id, message, isReplyAll, isHtml, options.mailbox);
|
|
750
|
+
|
|
751
|
+
if (!result.ok || !result.data) {
|
|
752
|
+
console.error(`Error: ${result.error?.message || 'Failed to create reply draft'}`);
|
|
753
|
+
process.exit(1);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const replyType = isReplyAll ? 'Reply all' : 'Reply';
|
|
757
|
+
console.log(`\u2713 ${replyType} draft created: ${result.data.draftId}`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
if (hasOutgoingExtras) {
|
|
762
|
+
const draftR = await replyToEmailDraft(authResult.token!, id, message, isReplyAll, isHtml, options.mailbox);
|
|
763
|
+
if (!draftR.ok || !draftR.data) {
|
|
764
|
+
console.error(`Error: ${draftR.error?.message || 'Failed to create reply draft'}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
const draftId = draftR.data.draftId;
|
|
768
|
+
await applyDraftCategoriesAttachments(authResult.token!, draftId, options.mailbox, {
|
|
769
|
+
withCategory: withCat,
|
|
770
|
+
attach: options.attach,
|
|
771
|
+
attachLink: options.attachLink,
|
|
772
|
+
json: options.json
|
|
773
|
+
});
|
|
774
|
+
if (options.draft) {
|
|
775
|
+
const replyType = isReplyAll ? 'Reply all' : 'Reply';
|
|
776
|
+
console.log(`\u2713 ${replyType} draft created: ${draftId}`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const sendR = await sendDraftById(authResult.token!, draftId, options.mailbox);
|
|
780
|
+
if (!sendR.ok) {
|
|
781
|
+
console.error(`Error: ${sendR.error?.message || 'Failed to send reply'}`);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
}
|
|
784
|
+
const replyType = isReplyAll ? 'Reply all' : 'Reply';
|
|
785
|
+
console.log(`\u2713 ${replyType} sent (with attachments/categories): ${id}`);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const result = await replyToEmail(authResult.token!, id, message, isReplyAll, isHtml, options.mailbox);
|
|
790
|
+
|
|
791
|
+
if (!result.ok) {
|
|
792
|
+
console.error(`Error: ${result.error?.message || 'Failed to send reply'}`);
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const replyType = isReplyAll ? 'Reply all' : 'Reply';
|
|
797
|
+
console.log(`\u2713 ${replyType} sent to: ${id}`);
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Handle forward
|
|
802
|
+
if (options.forward) {
|
|
803
|
+
const id = options.forward.trim();
|
|
804
|
+
|
|
805
|
+
if (!options.toAddr) {
|
|
806
|
+
console.error('Please provide forward recipients with --to-addr');
|
|
807
|
+
console.error('Example: m365-agent-cli mail --forward <id> --to-addr "user@example.com"');
|
|
808
|
+
process.exit(1);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const recipients = options.toAddr
|
|
812
|
+
.split(',')
|
|
813
|
+
.map((e) => e.trim())
|
|
814
|
+
.filter(Boolean);
|
|
815
|
+
|
|
816
|
+
if (!id) {
|
|
817
|
+
console.error('Error: --forward requires a message ID');
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const withCat = options.withCategory ?? [];
|
|
822
|
+
const hasAttach = !!options.attach?.trim();
|
|
823
|
+
const hasLinks = (options.attachLink?.length ?? 0) > 0;
|
|
824
|
+
const hasOutgoingExtras = hasAttach || hasLinks || withCat.filter((c) => c.trim()).length > 0;
|
|
825
|
+
|
|
826
|
+
if (options.draft && !hasOutgoingExtras) {
|
|
827
|
+
const result = await forwardEmailDraft(authResult.token!, id, recipients, options.message, options.mailbox);
|
|
828
|
+
if (!result.ok || !result.data) {
|
|
829
|
+
console.error(`Error: ${result.error?.message || 'Failed to create forward draft'}`);
|
|
830
|
+
process.exit(1);
|
|
831
|
+
}
|
|
832
|
+
console.log(`\u2713 Forward draft created: ${result.data.draftId}`);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (hasOutgoingExtras) {
|
|
837
|
+
const draftR = await forwardEmailDraft(authResult.token!, id, recipients, options.message, options.mailbox);
|
|
838
|
+
if (!draftR.ok || !draftR.data) {
|
|
839
|
+
console.error(`Error: ${draftR.error?.message || 'Failed to create forward draft'}`);
|
|
840
|
+
process.exit(1);
|
|
841
|
+
}
|
|
842
|
+
const draftId = draftR.data.draftId;
|
|
843
|
+
await applyDraftCategoriesAttachments(authResult.token!, draftId, options.mailbox, {
|
|
844
|
+
withCategory: withCat,
|
|
845
|
+
attach: options.attach,
|
|
846
|
+
attachLink: options.attachLink,
|
|
847
|
+
json: options.json
|
|
848
|
+
});
|
|
849
|
+
if (options.draft) {
|
|
850
|
+
console.log(`\u2713 Forward draft created: ${draftId}`);
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const sendR = await sendDraftById(authResult.token!, draftId, options.mailbox);
|
|
854
|
+
if (!sendR.ok) {
|
|
855
|
+
console.error(`Error: ${sendR.error?.message || 'Failed to send forward'}`);
|
|
856
|
+
process.exit(1);
|
|
857
|
+
}
|
|
858
|
+
console.log(`\u2713 Forwarded to ${recipients.join(', ')} (with attachments/categories): ${id}`);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
const result = await forwardEmail(authResult.token!, id, recipients, options.message, options.mailbox);
|
|
863
|
+
|
|
864
|
+
if (!result.ok) {
|
|
865
|
+
console.error(`Error: ${result.error?.message || 'Failed to forward email'}`);
|
|
866
|
+
process.exit(1);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
console.log(`\u2713 Forwarded to ${recipients.join(', ')}: ${id}`);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// List emails
|
|
874
|
+
if (options.json) {
|
|
875
|
+
console.log(
|
|
876
|
+
JSON.stringify(
|
|
877
|
+
{
|
|
878
|
+
folder: apiFolder,
|
|
879
|
+
page,
|
|
880
|
+
limit,
|
|
881
|
+
emails: emails.map((e, i) => ({
|
|
882
|
+
index: skip + i + 1,
|
|
883
|
+
id: e.Id,
|
|
884
|
+
from: e.From?.EmailAddress?.Address,
|
|
885
|
+
fromName: e.From?.EmailAddress?.Name,
|
|
886
|
+
subject: e.Subject,
|
|
887
|
+
preview: e.BodyPreview,
|
|
888
|
+
receivedAt: e.ReceivedDateTime,
|
|
889
|
+
isRead: e.IsRead,
|
|
890
|
+
hasAttachments: e.HasAttachments,
|
|
891
|
+
importance: e.Importance,
|
|
892
|
+
flagged: e.Flag?.FlagStatus === 'Flagged',
|
|
893
|
+
categories: e.Categories
|
|
894
|
+
}))
|
|
895
|
+
},
|
|
896
|
+
null,
|
|
897
|
+
2
|
|
898
|
+
)
|
|
899
|
+
);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
const folderDisplay = folder.charAt(0).toUpperCase() + folder.slice(1);
|
|
904
|
+
const searchInfo = options.search ? ` - search: "${options.search}"` : '';
|
|
905
|
+
const pageInfo = page > 1 ? ` (page ${page})` : '';
|
|
906
|
+
const mbInfo = options.mailbox ? ` — ${options.mailbox}` : '';
|
|
907
|
+
console.log(`\n\ud83d\udcec ${folderDisplay}${mbInfo}${searchInfo}${pageInfo}:\n`);
|
|
908
|
+
console.log('\u2500'.repeat(70));
|
|
909
|
+
|
|
910
|
+
if (emails.length === 0) {
|
|
911
|
+
console.log('\n No emails found.\n');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
for (let i = 0; i < emails.length; i++) {
|
|
916
|
+
const email = emails[i];
|
|
917
|
+
const idx = skip + i + 1;
|
|
918
|
+
const unreadMark = email.IsRead ? ' ' : '\u2022';
|
|
919
|
+
const flagMark = email.Flag?.FlagStatus === 'Flagged' ? '\u2691' : ' ';
|
|
920
|
+
const attachMark = email.HasAttachments ? '\ud83d\udcce' : ' ';
|
|
921
|
+
const importanceMark = email.Importance === 'High' ? '!' : ' ';
|
|
922
|
+
|
|
923
|
+
const from = email.From?.EmailAddress?.Name || email.From?.EmailAddress?.Address || 'Unknown';
|
|
924
|
+
const subject = email.Subject || '(no subject)';
|
|
925
|
+
const date = email.ReceivedDateTime ? formatDate(email.ReceivedDateTime) : '';
|
|
926
|
+
|
|
927
|
+
// Format: [idx] marks | from | subject | date
|
|
928
|
+
const marks = `${unreadMark}${flagMark}${attachMark}${importanceMark}`;
|
|
929
|
+
const fromTrunc = truncate(from, 20);
|
|
930
|
+
const subjectTrunc = truncate(subject, 35);
|
|
931
|
+
|
|
932
|
+
console.log(
|
|
933
|
+
` [${idx.toString().padStart(2)}] ${marks} ${fromTrunc.padEnd(20)} ${subjectTrunc.padEnd(35)} ${date}`
|
|
934
|
+
);
|
|
935
|
+
console.log(` ID: ${email.Id}`);
|
|
936
|
+
if (email.Categories?.length) {
|
|
937
|
+
console.log(` Categories: ${email.Categories.join(', ')}`);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
console.log(`\n${'\u2500'.repeat(70)}`);
|
|
942
|
+
console.log('\nCommands:');
|
|
943
|
+
console.log(` m365-agent-cli mail -r <id> # Read email`);
|
|
944
|
+
console.log(` m365-agent-cli mail -p ${page + 1} # Next page`);
|
|
945
|
+
console.log(` m365-agent-cli mail --unread # Only unread`);
|
|
946
|
+
console.log(` m365-agent-cli mail -s "keyword" # Search emails`);
|
|
947
|
+
console.log(` m365-agent-cli mail sent # Sent folder`);
|
|
948
|
+
console.log('');
|
|
949
|
+
}
|
|
950
|
+
);
|