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,286 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveAuth } from '../lib/auth.js';
|
|
3
|
+
import {
|
|
4
|
+
addDelegate,
|
|
5
|
+
type DelegateInfo,
|
|
6
|
+
type DelegatePermissions,
|
|
7
|
+
type DeliverMeetingRequests,
|
|
8
|
+
getDelegates,
|
|
9
|
+
removeDelegate,
|
|
10
|
+
updateDelegate
|
|
11
|
+
} from '../lib/delegate-client.js';
|
|
12
|
+
import { checkReadOnly } from '../lib/utils.js';
|
|
13
|
+
|
|
14
|
+
const VALID_PERMISSIONS = [
|
|
15
|
+
'None',
|
|
16
|
+
'Owner',
|
|
17
|
+
'PublishingEditor',
|
|
18
|
+
'Editor',
|
|
19
|
+
'PublishingAuthor',
|
|
20
|
+
'Author',
|
|
21
|
+
'Reviewer',
|
|
22
|
+
'NonEditingAuthor',
|
|
23
|
+
'FolderVisible'
|
|
24
|
+
] as const;
|
|
25
|
+
const VALID_FOLDERS = ['calendar', 'inbox', 'contacts', 'tasks', 'notes'] as const;
|
|
26
|
+
const VALID_DELIVER = ['DelegatesAndMe', 'DelegatesOnly', 'DelegatesAndSendInformationToMe', 'NoForward'] as const;
|
|
27
|
+
|
|
28
|
+
function formatPermissionLevel(level: string | undefined): string {
|
|
29
|
+
return level ?? 'None';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatDelegate(delegate: DelegateInfo): string {
|
|
33
|
+
const lines: string[] = [];
|
|
34
|
+
const name = delegate.displayName || delegate.primaryEmail || delegate.userId;
|
|
35
|
+
lines.push(` ${name} <${delegate.userId}>`);
|
|
36
|
+
lines.push(` View private items: ${delegate.viewPrivateItems}`);
|
|
37
|
+
lines.push(` Deliver meeting requests: ${delegate.deliverMeetingRequests}`);
|
|
38
|
+
|
|
39
|
+
const folderPerms = delegate.permissions;
|
|
40
|
+
if (folderPerms.calendar) lines.push(` Calendar: ${formatPermissionLevel(folderPerms.calendar)}`);
|
|
41
|
+
if (folderPerms.inbox) lines.push(` Inbox: ${formatPermissionLevel(folderPerms.inbox)}`);
|
|
42
|
+
if (folderPerms.contacts) lines.push(` Contacts: ${formatPermissionLevel(folderPerms.contacts)}`);
|
|
43
|
+
if (folderPerms.tasks) lines.push(` Tasks: ${formatPermissionLevel(folderPerms.tasks)}`);
|
|
44
|
+
if (folderPerms.notes) lines.push(` Notes: ${formatPermissionLevel(folderPerms.notes)}`);
|
|
45
|
+
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── list ───
|
|
50
|
+
|
|
51
|
+
const listCommand = new Command('list');
|
|
52
|
+
listCommand
|
|
53
|
+
.description('List all delegates on the mailbox')
|
|
54
|
+
.option('--mailbox <email>', 'mailbox (shared/alternative primary)')
|
|
55
|
+
.option('--token <token>', 'Use a specific token')
|
|
56
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
57
|
+
.action(async (opts: { mailbox?: string; token?: string; identity?: string }) => {
|
|
58
|
+
const auth = await resolveAuth({ token: opts.token, identity: opts.identity });
|
|
59
|
+
if (!auth.success || !auth.token) {
|
|
60
|
+
console.error('Auth failed:', auth.error);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await getDelegates(auth.token, opts.mailbox);
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
console.error('GetDelegates failed:', result.error?.message);
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const delegates = result.data ?? [];
|
|
71
|
+
if (delegates.length === 0) {
|
|
72
|
+
console.log('No delegates configured.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`Delegates (${delegates.length}):\n`);
|
|
77
|
+
for (const d of delegates) {
|
|
78
|
+
console.log(formatDelegate(d));
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// ─── add ───
|
|
84
|
+
|
|
85
|
+
const addCommand = new Command('add');
|
|
86
|
+
addCommand
|
|
87
|
+
.description('Add a delegate with per-folder permissions')
|
|
88
|
+
.requiredOption('--email <email>', 'delegate email address')
|
|
89
|
+
.option('--name <name>', 'display name for the delegate')
|
|
90
|
+
.option('--calendar <level>', `Calendar permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
91
|
+
.option('--inbox <level>', `Inbox permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
92
|
+
.option('--contacts <level>', `Contacts permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
93
|
+
.option('--tasks <level>', `Tasks permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
94
|
+
.option('--notes <level>', `Notes permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
95
|
+
.option('--view-private', 'allow delegate to view private items', false)
|
|
96
|
+
.option('--deliver <mode>', `deliver meeting requests (${VALID_DELIVER.join('|')})`, 'DelegatesAndMe')
|
|
97
|
+
.option('--mailbox <email>', 'mailbox to add delegate to (shared/alternative primary)')
|
|
98
|
+
.option('--token <token>', 'Use a specific token')
|
|
99
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
100
|
+
.action(
|
|
101
|
+
async (
|
|
102
|
+
opts: {
|
|
103
|
+
email: string;
|
|
104
|
+
name?: string;
|
|
105
|
+
calendar?: string;
|
|
106
|
+
inbox?: string;
|
|
107
|
+
contacts?: string;
|
|
108
|
+
tasks?: string;
|
|
109
|
+
notes?: string;
|
|
110
|
+
viewPrivate?: boolean;
|
|
111
|
+
deliver: string;
|
|
112
|
+
mailbox?: string;
|
|
113
|
+
token?: string;
|
|
114
|
+
identity?: string;
|
|
115
|
+
},
|
|
116
|
+
cmd: any
|
|
117
|
+
) => {
|
|
118
|
+
checkReadOnly(cmd);
|
|
119
|
+
// Validate permission levels
|
|
120
|
+
const perms: DelegatePermissions = {};
|
|
121
|
+
for (const folder of VALID_FOLDERS) {
|
|
122
|
+
const key = folder as (typeof VALID_FOLDERS)[number];
|
|
123
|
+
const level = opts[key] as string | undefined;
|
|
124
|
+
if (level) {
|
|
125
|
+
if (!VALID_PERMISSIONS.includes(level as (typeof VALID_PERMISSIONS)[number])) {
|
|
126
|
+
console.error(`Invalid permission level "${level}" for ${folder}. Valid: ${VALID_PERMISSIONS.join(', ')}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
(perms as Record<string, string>)[key] = level;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const deliver = opts.deliver as DeliverMeetingRequests;
|
|
134
|
+
if (!VALID_DELIVER.includes(deliver)) {
|
|
135
|
+
console.error(`Invalid deliver mode "${deliver}". Valid: ${VALID_DELIVER.join(', ')}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const auth = await resolveAuth({ token: opts.token, identity: opts.identity });
|
|
140
|
+
if (!auth.success || !auth.token) {
|
|
141
|
+
console.error('Auth failed:', auth.error);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await addDelegate({
|
|
146
|
+
token: auth.token,
|
|
147
|
+
delegateEmail: opts.email,
|
|
148
|
+
delegateName: opts.name,
|
|
149
|
+
permissions: perms,
|
|
150
|
+
viewPrivateItems: opts.viewPrivate,
|
|
151
|
+
deliverMeetingRequests: deliver,
|
|
152
|
+
mailbox: opts.mailbox
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!result.ok) {
|
|
156
|
+
console.error('AddDelegate failed:', result.error?.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log('Delegate added:');
|
|
161
|
+
console.log(formatDelegate(result.data!));
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// ─── update ───
|
|
166
|
+
|
|
167
|
+
const updateCommand = new Command('update');
|
|
168
|
+
updateCommand
|
|
169
|
+
.description("Update an existing delegate's permissions")
|
|
170
|
+
.requiredOption('--email <email>', 'delegate email address')
|
|
171
|
+
.option('--calendar <level>', `Calendar permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
172
|
+
.option('--inbox <level>', `Inbox permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
173
|
+
.option('--contacts <level>', `Contacts permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
174
|
+
.option('--tasks <level>', `Tasks permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
175
|
+
.option('--notes <level>', `Notes permission level (${VALID_PERMISSIONS.join('|')})`)
|
|
176
|
+
.option('--view-private <boolean>', 'allow delegate to view private items (true/false)')
|
|
177
|
+
.option('--deliver <mode>', `deliver meeting requests (${VALID_DELIVER.join('|')})`)
|
|
178
|
+
.option('--mailbox <email>', 'mailbox (shared/alternative primary)')
|
|
179
|
+
.option('--token <token>', 'Use a specific token')
|
|
180
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
181
|
+
.action(
|
|
182
|
+
async (
|
|
183
|
+
opts: {
|
|
184
|
+
email: string;
|
|
185
|
+
calendar?: string;
|
|
186
|
+
inbox?: string;
|
|
187
|
+
contacts?: string;
|
|
188
|
+
tasks?: string;
|
|
189
|
+
notes?: string;
|
|
190
|
+
viewPrivate?: string | boolean;
|
|
191
|
+
deliver?: string;
|
|
192
|
+
mailbox?: string;
|
|
193
|
+
token?: string;
|
|
194
|
+
identity?: string;
|
|
195
|
+
},
|
|
196
|
+
cmd: any
|
|
197
|
+
) => {
|
|
198
|
+
checkReadOnly(cmd);
|
|
199
|
+
const permsOut: DelegatePermissions = {};
|
|
200
|
+
let hasPerms = false;
|
|
201
|
+
|
|
202
|
+
for (const folder of VALID_FOLDERS) {
|
|
203
|
+
const key = folder as (typeof VALID_FOLDERS)[number];
|
|
204
|
+
const level = opts[key] as string | undefined;
|
|
205
|
+
if (level !== undefined) {
|
|
206
|
+
if (!VALID_PERMISSIONS.includes(level as (typeof VALID_PERMISSIONS)[number])) {
|
|
207
|
+
console.error(`Invalid permission level "${level}" for ${folder}. Valid: ${VALID_PERMISSIONS.join(', ')}`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
(permsOut as Record<string, string>)[key] = level;
|
|
211
|
+
hasPerms = true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const deliver = opts.deliver as DeliverMeetingRequests | undefined;
|
|
216
|
+
if (deliver && !VALID_DELIVER.includes(deliver)) {
|
|
217
|
+
console.error(`Invalid deliver mode "${deliver}". Valid: ${VALID_DELIVER.join(', ')}`);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const auth = await resolveAuth({ token: opts.token, identity: opts.identity });
|
|
222
|
+
if (!auth.success || !auth.token) {
|
|
223
|
+
console.error('Auth failed:', auth.error);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const result = await updateDelegate({
|
|
228
|
+
token: auth.token,
|
|
229
|
+
delegateEmail: opts.email,
|
|
230
|
+
permissions: hasPerms ? permsOut : undefined,
|
|
231
|
+
viewPrivateItems:
|
|
232
|
+
opts.viewPrivate === undefined ? undefined : opts.viewPrivate === 'true' || opts.viewPrivate === true,
|
|
233
|
+
deliverMeetingRequests: deliver,
|
|
234
|
+
mailbox: opts.mailbox
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
console.error('UpdateDelegate failed:', result.error?.message);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
console.log('Delegate updated:');
|
|
243
|
+
console.log(formatDelegate(result.data!));
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// ─── remove ───
|
|
248
|
+
|
|
249
|
+
const removeCommand = new Command('remove');
|
|
250
|
+
removeCommand
|
|
251
|
+
.description('Remove a delegate from the mailbox')
|
|
252
|
+
.requiredOption('--email <email>', 'delegate email address')
|
|
253
|
+
.option('--mailbox <email>', 'mailbox (shared/alternative primary)')
|
|
254
|
+
.option('--token <token>', 'Use a specific token')
|
|
255
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
256
|
+
.action(async (opts: { email: string; mailbox?: string; token?: string; identity?: string }, cmd: any) => {
|
|
257
|
+
checkReadOnly(cmd);
|
|
258
|
+
const auth = await resolveAuth({ token: opts.token, identity: opts.identity });
|
|
259
|
+
if (!auth.success || !auth.token) {
|
|
260
|
+
console.error('Auth failed:', auth.error);
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const result = await removeDelegate({
|
|
265
|
+
token: auth.token,
|
|
266
|
+
delegateEmail: opts.email,
|
|
267
|
+
mailbox: opts.mailbox
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (!result.ok) {
|
|
271
|
+
console.error('RemoveDelegate failed:', result.error?.message);
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
console.log(`Delegate ${opts.email} removed.`);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ─── Root ───
|
|
279
|
+
|
|
280
|
+
export const delegatesCommand = new Command('delegates');
|
|
281
|
+
delegatesCommand
|
|
282
|
+
.description('Manage delegates via EWS SOAP (list, add, update, remove)')
|
|
283
|
+
.addCommand(listCommand)
|
|
284
|
+
.addCommand(addCommand)
|
|
285
|
+
.addCommand(updateCommand)
|
|
286
|
+
.addCommand(removeCommand);
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { resolveAuth } from '../lib/auth.js';
|
|
3
|
+
import { parseDay } from '../lib/dates.js';
|
|
4
|
+
import { cancelEvent, deleteEvent, getCalendarEvents } from '../lib/ews-client.js';
|
|
5
|
+
import { checkReadOnly } from '../lib/utils.js';
|
|
6
|
+
|
|
7
|
+
function formatTime(dateStr: string): string {
|
|
8
|
+
const date = new Date(dateStr);
|
|
9
|
+
return date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function formatDate(dateStr: string): string {
|
|
13
|
+
const date = new Date(dateStr);
|
|
14
|
+
return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const deleteEventCommand = new Command('delete-event')
|
|
18
|
+
.description('Delete/cancel a calendar event (sends cancellation if there are attendees)')
|
|
19
|
+
.argument('[eventIndex]', 'Event index from the list (deprecated; use --id)')
|
|
20
|
+
.option('--id <eventId>', 'Delete event by stable ID')
|
|
21
|
+
.option(
|
|
22
|
+
'--day <day>',
|
|
23
|
+
'Day to show events from (today, tomorrow, YYYY-MM-DD) - note: may miss multi-day events crossing midnight',
|
|
24
|
+
'today'
|
|
25
|
+
)
|
|
26
|
+
.option('--search <text>', 'Search for events by title')
|
|
27
|
+
.option('--message <text>', 'Cancellation message to send to attendees')
|
|
28
|
+
.option('--force-delete', 'Delete without sending cancellation (even with attendees)')
|
|
29
|
+
.option('--occurrence <index>', 'Delete only the Nth occurrence of a recurring event')
|
|
30
|
+
.option('--instance <date>', 'Delete only the occurrence on a specific date (YYYY-MM-DD)')
|
|
31
|
+
.option('--scope <scope>', 'Scope: all (default), this (single occurrence), future (this and future)', 'all')
|
|
32
|
+
.option('--json', 'Output as JSON')
|
|
33
|
+
.option('--token <token>', 'Use a specific token')
|
|
34
|
+
.option('--identity <name>', 'Use a specific authentication identity (default: default)')
|
|
35
|
+
.option('--mailbox <email>', 'Delete event in shared mailbox calendar')
|
|
36
|
+
.action(
|
|
37
|
+
async (
|
|
38
|
+
_eventIndex: string | undefined,
|
|
39
|
+
options: {
|
|
40
|
+
id?: string;
|
|
41
|
+
day: string;
|
|
42
|
+
search?: string;
|
|
43
|
+
message?: string;
|
|
44
|
+
forceDelete?: boolean;
|
|
45
|
+
occurrence?: string;
|
|
46
|
+
instance?: string;
|
|
47
|
+
scope: string;
|
|
48
|
+
json?: boolean;
|
|
49
|
+
token?: string;
|
|
50
|
+
identity?: string;
|
|
51
|
+
mailbox?: string;
|
|
52
|
+
},
|
|
53
|
+
cmd: any
|
|
54
|
+
) => {
|
|
55
|
+
checkReadOnly(cmd);
|
|
56
|
+
const authResult = await resolveAuth({
|
|
57
|
+
token: options.token,
|
|
58
|
+
identity: options.identity
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (!authResult.success) {
|
|
62
|
+
if (options.json) {
|
|
63
|
+
console.log(JSON.stringify({ error: authResult.error }, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
console.error(`Error: ${authResult.error}`);
|
|
66
|
+
console.error('\nCheck your .env file for EWS_CLIENT_ID and EWS_REFRESH_TOKEN.');
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Get events for the day
|
|
72
|
+
const baseDate = parseDay(options.day);
|
|
73
|
+
const startOfDay = new Date(baseDate);
|
|
74
|
+
startOfDay.setHours(0, 0, 0, 0);
|
|
75
|
+
const endOfDay = new Date(baseDate);
|
|
76
|
+
endOfDay.setHours(23, 59, 59, 999);
|
|
77
|
+
|
|
78
|
+
const result = await getCalendarEvents(
|
|
79
|
+
authResult.token!,
|
|
80
|
+
startOfDay.toISOString(),
|
|
81
|
+
endOfDay.toISOString(),
|
|
82
|
+
options.mailbox
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
if (!result.ok || !result.data) {
|
|
86
|
+
if (options.json) {
|
|
87
|
+
console.log(JSON.stringify({ error: result.error?.message || 'Failed to fetch events' }, null, 2));
|
|
88
|
+
} else {
|
|
89
|
+
console.error(`Error: ${result.error?.message || 'Failed to fetch events'}`);
|
|
90
|
+
}
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Filter to events the user owns (IsOrganizer) and optionally by search
|
|
95
|
+
let events = result.data.filter((e) => e.IsOrganizer && !e.IsCancelled);
|
|
96
|
+
|
|
97
|
+
if (options.search) {
|
|
98
|
+
const searchLower = options.search.toLowerCase();
|
|
99
|
+
events = events.filter((e) => e.Subject?.toLowerCase().includes(searchLower));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// If no id provided, list events
|
|
103
|
+
if (!options.id) {
|
|
104
|
+
if (options.json) {
|
|
105
|
+
console.log(
|
|
106
|
+
JSON.stringify(
|
|
107
|
+
{
|
|
108
|
+
events: events.map((e, i) => ({
|
|
109
|
+
index: i + 1,
|
|
110
|
+
id: e.Id,
|
|
111
|
+
subject: e.Subject,
|
|
112
|
+
start: e.Start.DateTime,
|
|
113
|
+
end: e.End.DateTime
|
|
114
|
+
}))
|
|
115
|
+
},
|
|
116
|
+
null,
|
|
117
|
+
2
|
|
118
|
+
)
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log(`\nYour events for ${formatDate(baseDate.toISOString())}:\n`);
|
|
124
|
+
console.log('\u2500'.repeat(60));
|
|
125
|
+
|
|
126
|
+
if (events.length === 0) {
|
|
127
|
+
console.log('\n No events found that you can delete.');
|
|
128
|
+
console.log(' (You can only delete events you organized)\n');
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < events.length; i++) {
|
|
133
|
+
const event = events[i];
|
|
134
|
+
const startTime = formatTime(event.Start.DateTime);
|
|
135
|
+
const endTime = formatTime(event.End.DateTime);
|
|
136
|
+
const attendees = event.Attendees?.filter((a) => a.EmailAddress?.Address && a.Type !== 'Resource') || [];
|
|
137
|
+
|
|
138
|
+
console.log(`\n [${i + 1}] ${event.Subject}`);
|
|
139
|
+
console.log(` ${startTime} - ${endTime}`);
|
|
140
|
+
console.log(` ID: ${event.Id}`);
|
|
141
|
+
if (event.Location?.DisplayName) {
|
|
142
|
+
console.log(` Location: ${event.Location.DisplayName}`);
|
|
143
|
+
}
|
|
144
|
+
if (attendees.length > 0) {
|
|
145
|
+
console.log(` Attendees: ${attendees.length} (will be notified on cancel)`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
console.log(`\n${'\u2500'.repeat(60)}`);
|
|
150
|
+
console.log('\nTo delete/cancel an event:');
|
|
151
|
+
console.log(' m365-agent-cli delete-event <number> # Cancel & notify attendees');
|
|
152
|
+
console.log(' m365-agent-cli delete-event <number> --message "Sorry" # With cancellation message');
|
|
153
|
+
console.log(' m365-agent-cli delete-event <number> --force-delete # Delete without notifying');
|
|
154
|
+
console.log('');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Delete the specified event by ID
|
|
159
|
+
if (!options.id) {
|
|
160
|
+
console.error('Please specify the event id with --id.');
|
|
161
|
+
console.error('Run `m365-agent-cli delete-event` to list events and IDs.');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Determine scope and occurrence ID
|
|
166
|
+
let scope = options.scope as 'all' | 'this' | 'future';
|
|
167
|
+
let occurrenceItemId: string | undefined;
|
|
168
|
+
let targetEvent = events.find((e) => e.Id === options.id);
|
|
169
|
+
|
|
170
|
+
// Validate scope: 'future' is not currently supported by EWS
|
|
171
|
+
if (scope === 'future') {
|
|
172
|
+
console.error('Error: --scope future is not supported.');
|
|
173
|
+
console.error('EWS does not provide a native operation to delete "this and future" occurrences.');
|
|
174
|
+
console.error('Use --scope this to delete a single occurrence, or --scope all to delete the entire series.');
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// If occurrence/instance flags are provided without explicit scope, default to 'this'
|
|
179
|
+
if ((options.occurrence || options.instance) && options.scope === 'all') {
|
|
180
|
+
scope = 'this';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if ((options.occurrence || options.instance) && scope === 'this') {
|
|
184
|
+
// Find the occurrence by index or date, ensuring it matches the provided event ID
|
|
185
|
+
if (options.instance) {
|
|
186
|
+
// Find occurrence matching the specific date and event ID
|
|
187
|
+
let instanceDate: Date;
|
|
188
|
+
try {
|
|
189
|
+
instanceDate = parseDay(options.instance, { throwOnInvalid: true });
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const message = err instanceof Error ? err.message : 'Invalid instance date';
|
|
192
|
+
if (options.json) {
|
|
193
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
194
|
+
} else {
|
|
195
|
+
console.error(`Error: ${message}`);
|
|
196
|
+
}
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
instanceDate.setHours(0, 0, 0, 0);
|
|
200
|
+
const occEvent = events.find((e) => {
|
|
201
|
+
const eventDate = new Date(e.Start.DateTime);
|
|
202
|
+
eventDate.setHours(0, 0, 0, 0);
|
|
203
|
+
return eventDate.getTime() === instanceDate.getTime() && e.Id === options.id;
|
|
204
|
+
});
|
|
205
|
+
if (!occEvent) {
|
|
206
|
+
console.error(
|
|
207
|
+
`No occurrence found on ${options.instance} with ID ${options.id}. Try expanding the date range with --day.`
|
|
208
|
+
);
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
// For CalendarView items, the Id we get IS the occurrence ID
|
|
212
|
+
occurrenceItemId = occEvent.Id;
|
|
213
|
+
targetEvent = occEvent;
|
|
214
|
+
} else if (options.occurrence) {
|
|
215
|
+
const idx = parseInt(options.occurrence, 10);
|
|
216
|
+
if (Number.isNaN(idx) || idx < 1) {
|
|
217
|
+
console.error('--occurrence must be a positive integer');
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
// Events from CalendarView are already individual occurrences
|
|
221
|
+
if (idx > events.length) {
|
|
222
|
+
console.error(
|
|
223
|
+
`Invalid occurrence index: ${idx}. Only ${events.length} occurrence(s) found in the date range.`
|
|
224
|
+
);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
const occEvent = events[idx - 1];
|
|
228
|
+
if (occEvent.Id !== options.id) {
|
|
229
|
+
console.error(`Occurrence ${idx} does not match the provided event ID ${options.id}.`);
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
occurrenceItemId = occEvent.Id;
|
|
233
|
+
targetEvent = occEvent;
|
|
234
|
+
}
|
|
235
|
+
console.log(`\nDeleting single occurrence: ${targetEvent!.Subject}`);
|
|
236
|
+
console.log(
|
|
237
|
+
` ${formatDate(targetEvent!.Start.DateTime)} ${formatTime(targetEvent!.Start.DateTime)} - ${formatTime(targetEvent!.End.DateTime)}`
|
|
238
|
+
);
|
|
239
|
+
} else if (!targetEvent) {
|
|
240
|
+
console.error(`Invalid event id: ${options.id}`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
} else {
|
|
243
|
+
// Full series delete
|
|
244
|
+
if (scope !== 'all') {
|
|
245
|
+
// future scope needs the occurrence ID too
|
|
246
|
+
console.log(`\nDeleting: ${targetEvent.Subject} (scope: ${scope})`);
|
|
247
|
+
} else {
|
|
248
|
+
console.log(`\nDeleting: ${targetEvent.Subject}`);
|
|
249
|
+
}
|
|
250
|
+
console.log(
|
|
251
|
+
` ${formatDate(targetEvent.Start.DateTime)} ${formatTime(targetEvent.Start.DateTime)} - ${formatTime(targetEvent.End.DateTime)}`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check if event has attendees (other than organizer)
|
|
256
|
+
const attendees = targetEvent!.Attendees?.filter((a) => a.EmailAddress?.Address && a.Type !== 'Resource') || [];
|
|
257
|
+
const hasAttendees = attendees.length > 0;
|
|
258
|
+
|
|
259
|
+
let deleteResult: Awaited<ReturnType<typeof deleteEvent>>;
|
|
260
|
+
let action: string;
|
|
261
|
+
|
|
262
|
+
if (hasAttendees && !options.forceDelete && scope === 'all') {
|
|
263
|
+
// Use cancel to send cancellation notices for full series
|
|
264
|
+
console.log(` Attendees: ${attendees.map((a) => a.EmailAddress?.Address).join(', ')}`);
|
|
265
|
+
console.log(` Sending cancellation notices...`);
|
|
266
|
+
deleteResult = await cancelEvent({
|
|
267
|
+
token: authResult.token!,
|
|
268
|
+
eventId: targetEvent!.Id,
|
|
269
|
+
comment: options.message,
|
|
270
|
+
mailbox: options.mailbox
|
|
271
|
+
});
|
|
272
|
+
action = 'cancelled';
|
|
273
|
+
} else {
|
|
274
|
+
// Delete with or without notification based on forceDelete flag
|
|
275
|
+
deleteResult = await deleteEvent({
|
|
276
|
+
token: authResult.token!,
|
|
277
|
+
eventId: targetEvent!.Id,
|
|
278
|
+
occurrenceItemId,
|
|
279
|
+
scope,
|
|
280
|
+
mailbox: options.mailbox,
|
|
281
|
+
forceDelete: options.forceDelete,
|
|
282
|
+
comment: options.message
|
|
283
|
+
});
|
|
284
|
+
action = 'deleted';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (!deleteResult.ok) {
|
|
288
|
+
if (options.json) {
|
|
289
|
+
console.log(JSON.stringify({ error: deleteResult.error?.message || `Failed to ${action} event` }, null, 2));
|
|
290
|
+
} else {
|
|
291
|
+
console.error(`\nError: ${deleteResult.error?.message || `Failed to ${action} event`}`);
|
|
292
|
+
}
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (options.json) {
|
|
297
|
+
console.log(
|
|
298
|
+
JSON.stringify(
|
|
299
|
+
{
|
|
300
|
+
success: true,
|
|
301
|
+
action,
|
|
302
|
+
event: targetEvent!.Subject,
|
|
303
|
+
attendeesNotified: hasAttendees && !options.forceDelete ? attendees.length : 0,
|
|
304
|
+
...(deleteResult.info ? { info: deleteResult.info } : {})
|
|
305
|
+
},
|
|
306
|
+
null,
|
|
307
|
+
2
|
|
308
|
+
)
|
|
309
|
+
);
|
|
310
|
+
} else {
|
|
311
|
+
if (deleteResult.info) {
|
|
312
|
+
console.warn(`\nNote: ${deleteResult.info}\n`);
|
|
313
|
+
}
|
|
314
|
+
if (hasAttendees && !options.forceDelete) {
|
|
315
|
+
console.log(`\n\u2713 Event cancelled. ${attendees.length} attendee(s) notified.\n`);
|
|
316
|
+
} else {
|
|
317
|
+
console.log('\n\u2713 Event deleted.\n');
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
);
|