@griffinwork40/outlook-cli 0.0.0-stage → 1.0.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 +21 -0
- package/README.md +324 -2
- package/SETUP.md +199 -0
- package/dist/cli.js +3286 -0
- package/dist/cli.js.map +1 -0
- package/package.json +59 -4
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3286 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
|
|
6
|
+
// src/env.ts
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var ENV_PATH = join(homedir(), ".afk", "config", "afk.env");
|
|
11
|
+
function parseEnvLines(content) {
|
|
12
|
+
const pairs = [];
|
|
13
|
+
for (const raw of content.split("\n")) {
|
|
14
|
+
const line = raw.trim();
|
|
15
|
+
if (line === "" || line.startsWith("#")) continue;
|
|
16
|
+
const eqIdx = line.indexOf("=");
|
|
17
|
+
if (eqIdx === -1) continue;
|
|
18
|
+
const key = line.slice(0, eqIdx).trim();
|
|
19
|
+
if (key === "") continue;
|
|
20
|
+
let value = line.slice(eqIdx + 1).trim();
|
|
21
|
+
if (value.length >= 2 && (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'"))) {
|
|
22
|
+
value = value.slice(1, -1);
|
|
23
|
+
}
|
|
24
|
+
pairs.push([key, value]);
|
|
25
|
+
}
|
|
26
|
+
return pairs;
|
|
27
|
+
}
|
|
28
|
+
function loadEnvFile(envPath = ENV_PATH) {
|
|
29
|
+
let content;
|
|
30
|
+
try {
|
|
31
|
+
content = readFileSync(envPath, "utf-8");
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const [key, value] of parseEnvLines(content)) {
|
|
36
|
+
if (process.env[key] === void 0) {
|
|
37
|
+
process.env[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/errors.ts
|
|
43
|
+
var OutlookCliError = class extends Error {
|
|
44
|
+
exitCode;
|
|
45
|
+
constructor(message, exitCode = 1) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "OutlookCliError";
|
|
48
|
+
this.exitCode = exitCode;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var MissingCredentialsError = class extends OutlookCliError {
|
|
52
|
+
missing;
|
|
53
|
+
constructor(missing) {
|
|
54
|
+
const list = missing.join(", ");
|
|
55
|
+
super(
|
|
56
|
+
`Missing required environment variable(s): ${list}
|
|
57
|
+
|
|
58
|
+
GRAPH_CLIENT_ID comes from a one-time Azure app registration.
|
|
59
|
+
See SETUP.md for the 5-step click-path, then export it:
|
|
60
|
+
export GRAPH_CLIENT_ID=<application-client-id>`,
|
|
61
|
+
3
|
|
62
|
+
);
|
|
63
|
+
this.name = "MissingCredentialsError";
|
|
64
|
+
this.missing = missing;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var NotAuthenticatedError = class extends OutlookCliError {
|
|
68
|
+
constructor(detail) {
|
|
69
|
+
super(
|
|
70
|
+
`Not signed in${detail ? `: ${detail}` : "."}
|
|
71
|
+
|
|
72
|
+
Run: outlook auth login`,
|
|
73
|
+
3
|
|
74
|
+
);
|
|
75
|
+
this.name = "NotAuthenticatedError";
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var InsufficientScopeError = class extends OutlookCliError {
|
|
79
|
+
constructor(action) {
|
|
80
|
+
super(
|
|
81
|
+
`This session was signed in with --readonly, so it cannot ${action}.
|
|
82
|
+
|
|
83
|
+
Re-authorize with full access: outlook auth login`,
|
|
84
|
+
3
|
|
85
|
+
);
|
|
86
|
+
this.name = "InsufficientScopeError";
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var PolicyRefusedError = class extends OutlookCliError {
|
|
90
|
+
rule;
|
|
91
|
+
constructor(rule, reason) {
|
|
92
|
+
super(`Refused by mutation policy (${rule}): ${reason}`, 4);
|
|
93
|
+
this.name = "PolicyRefusedError";
|
|
94
|
+
this.rule = rule;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
var UsageError = class extends OutlookCliError {
|
|
98
|
+
constructor(message) {
|
|
99
|
+
super(message, 2);
|
|
100
|
+
this.name = "UsageError";
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
var GraphError = class extends OutlookCliError {
|
|
104
|
+
status;
|
|
105
|
+
code;
|
|
106
|
+
graphMessage;
|
|
107
|
+
requestId;
|
|
108
|
+
retryAfter;
|
|
109
|
+
constructor(params) {
|
|
110
|
+
const hint = GRAPH_ERROR_HINTS[params.code];
|
|
111
|
+
super(
|
|
112
|
+
`Graph API error ${params.status} (${params.code}): ${params.message}` + (hint ? `
|
|
113
|
+
\u2192 ${hint}` : "") + (params.requestId ? `
|
|
114
|
+
request-id: ${params.requestId}` : ""),
|
|
115
|
+
1
|
|
116
|
+
);
|
|
117
|
+
this.name = "GraphError";
|
|
118
|
+
this.status = params.status;
|
|
119
|
+
this.code = params.code;
|
|
120
|
+
this.graphMessage = params.message;
|
|
121
|
+
this.requestId = params.requestId;
|
|
122
|
+
this.retryAfter = params.retryAfter;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
var GRAPH_ERROR_HINTS = {
|
|
126
|
+
InvalidAuthenticationToken: "Access token expired or malformed. Run: outlook auth login",
|
|
127
|
+
ErrorAccessDenied: "The signed-in account lacks permission. Confirm the Mail.ReadWrite / Mail.Send scopes were consented at login.",
|
|
128
|
+
ErrorItemNotFound: "No message with that id. Note: message ids change when a message moves between folders unless immutable ids are used.",
|
|
129
|
+
ErrorInvalidUser: "The mailbox does not exist or is not licensed for mail.",
|
|
130
|
+
ErrorInvalidRecipients: "One or more recipient addresses were rejected. Check for typos and that each is a routable address.",
|
|
131
|
+
ErrorMessageSizeExceeded: "Message plus attachments exceeded the mailbox send limit. Use fewer or smaller attachments.",
|
|
132
|
+
ErrorAttachmentSizeExceeded: "Inline attachments must be under 3 MB. Larger files need an upload session (not supported in v1).",
|
|
133
|
+
ErrorMailboxStoreUnavailable: "The mailbox is temporarily unavailable. Retry shortly.",
|
|
134
|
+
ErrorQuotaExceeded: "Mailbox is over quota.",
|
|
135
|
+
ErrorFolderNotFound: "No folder with that name or id. Run: outlook folders list",
|
|
136
|
+
ErrorInvalidIdMalformed: "That id is not a valid Graph id. Ids are opaque \u2014 copy them verbatim from a list/get.",
|
|
137
|
+
InefficientFilter: "The $filter/$orderby combination is too complex. Properties in $orderby must also appear in $filter, in the same order.",
|
|
138
|
+
activityLimitReached: "Throttled by Outlook (429). The client backs off automatically; if this persists, reduce request volume.",
|
|
139
|
+
TooManyRequests: "Throttled by Outlook (429). Back off and retry.",
|
|
140
|
+
ErrorTooManyObjectsOpened: "Too many concurrent mailbox operations. Reduce concurrency to 4 or fewer.",
|
|
141
|
+
ErrorSendAsDenied: "The account may not send as that address. Check Send As permissions.",
|
|
142
|
+
MailboxNotEnabledForRESTAPI: "This mailbox cannot be accessed via the REST API (often an on-prem or unlicensed mailbox)."
|
|
143
|
+
};
|
|
144
|
+
function parseGraphError(status, bodyText, retryAfter) {
|
|
145
|
+
let code = `HTTP${status}`;
|
|
146
|
+
let message = bodyText.slice(0, 500) || "no response body";
|
|
147
|
+
let requestId;
|
|
148
|
+
try {
|
|
149
|
+
const parsed = JSON.parse(bodyText);
|
|
150
|
+
const root = parsed;
|
|
151
|
+
if (typeof root.error === "string") {
|
|
152
|
+
code = root.error;
|
|
153
|
+
if (typeof root.error_description === "string") {
|
|
154
|
+
message = root.error_description;
|
|
155
|
+
}
|
|
156
|
+
} else if (root.error && typeof root.error === "object") {
|
|
157
|
+
const err = root.error;
|
|
158
|
+
if (typeof err.code === "string") code = err.code;
|
|
159
|
+
if (typeof err.message === "string") message = err.message;
|
|
160
|
+
const inner = err.innerError ?? err.innererror;
|
|
161
|
+
const rid = inner?.["request-id"] ?? inner?.["client-request-id"];
|
|
162
|
+
if (typeof rid === "string") requestId = rid;
|
|
163
|
+
}
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
return new GraphError({ status, code, message, requestId, retryAfter });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/args.ts
|
|
170
|
+
var TRUE_VALUES = /* @__PURE__ */ new Set(["true", "1", "yes", "on"]);
|
|
171
|
+
var FALSE_VALUES = /* @__PURE__ */ new Set(["false", "0", "no", "off", ""]);
|
|
172
|
+
function looksLikeOption(tok) {
|
|
173
|
+
if (tok === void 0) return false;
|
|
174
|
+
if (tok === "-") return false;
|
|
175
|
+
return tok.startsWith("-") && tok.length > 1;
|
|
176
|
+
}
|
|
177
|
+
function parseArgs(argv) {
|
|
178
|
+
const positionals = [];
|
|
179
|
+
const store = /* @__PURE__ */ new Map();
|
|
180
|
+
function record(name, value) {
|
|
181
|
+
const existing = store.get(name);
|
|
182
|
+
if (existing) existing.push(value);
|
|
183
|
+
else store.set(name, [value]);
|
|
184
|
+
}
|
|
185
|
+
let i = 0;
|
|
186
|
+
let terminated = false;
|
|
187
|
+
while (i < argv.length) {
|
|
188
|
+
const tok = argv[i];
|
|
189
|
+
if (tok === void 0) break;
|
|
190
|
+
if (!terminated && tok === "--") {
|
|
191
|
+
terminated = true;
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (terminated) {
|
|
196
|
+
positionals.push(tok);
|
|
197
|
+
i++;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (tok.startsWith("--") && tok.length > 2) {
|
|
201
|
+
const body = tok.slice(2);
|
|
202
|
+
const eq = body.indexOf("=");
|
|
203
|
+
if (eq !== -1) {
|
|
204
|
+
record(body.slice(0, eq), body.slice(eq + 1));
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (body.startsWith("no-") && body.length > 3) {
|
|
209
|
+
record(body.slice(3), false);
|
|
210
|
+
i++;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const next = argv[i + 1];
|
|
214
|
+
if (next !== void 0 && !looksLikeOption(next)) {
|
|
215
|
+
record(body, next);
|
|
216
|
+
i += 2;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
record(body, true);
|
|
220
|
+
i++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (tok.startsWith("-") && tok.length > 1) {
|
|
224
|
+
const body = tok.slice(1);
|
|
225
|
+
const eq = body.indexOf("=");
|
|
226
|
+
if (eq !== -1) {
|
|
227
|
+
record(body.slice(0, eq), body.slice(eq + 1));
|
|
228
|
+
i++;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const next = argv[i + 1];
|
|
232
|
+
if (next !== void 0 && !looksLikeOption(next)) {
|
|
233
|
+
record(body, next);
|
|
234
|
+
i += 2;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
record(body, true);
|
|
238
|
+
i++;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
positionals.push(tok);
|
|
242
|
+
i++;
|
|
243
|
+
}
|
|
244
|
+
function lastValue(name) {
|
|
245
|
+
const list = store.get(name);
|
|
246
|
+
if (!list || list.length === 0) return void 0;
|
|
247
|
+
return list[list.length - 1];
|
|
248
|
+
}
|
|
249
|
+
const api = {
|
|
250
|
+
positionals,
|
|
251
|
+
raw: [...argv],
|
|
252
|
+
has(name) {
|
|
253
|
+
return store.has(name);
|
|
254
|
+
},
|
|
255
|
+
str(name, fallback) {
|
|
256
|
+
const v = lastValue(name);
|
|
257
|
+
return typeof v === "string" ? v : fallback;
|
|
258
|
+
},
|
|
259
|
+
strAll(name) {
|
|
260
|
+
const list = store.get(name);
|
|
261
|
+
if (!list) return [];
|
|
262
|
+
return list.filter((v) => typeof v === "string");
|
|
263
|
+
},
|
|
264
|
+
num(name) {
|
|
265
|
+
const v = lastValue(name);
|
|
266
|
+
if (v === void 0) return void 0;
|
|
267
|
+
if (typeof v === "boolean") {
|
|
268
|
+
throw new UsageError(`--${name} requires a numeric value`);
|
|
269
|
+
}
|
|
270
|
+
if (v.trim() === "") {
|
|
271
|
+
throw new UsageError(`--${name} expects a numeric value, got ""`);
|
|
272
|
+
}
|
|
273
|
+
const n = Number(v);
|
|
274
|
+
if (Number.isNaN(n)) {
|
|
275
|
+
throw new UsageError(`--${name} expects a numeric value, got "${v}"`);
|
|
276
|
+
}
|
|
277
|
+
return n;
|
|
278
|
+
},
|
|
279
|
+
/**
|
|
280
|
+
* Presence-based, but NEVER value-blind.
|
|
281
|
+
*
|
|
282
|
+
* The old implementation returned `last !== false`, which meant any string
|
|
283
|
+
* value was truthy — so `--yes=false` (a user trying to DECLINE an
|
|
284
|
+
* irreversible purge) read as `true` and the deletion proceeded. Explicit
|
|
285
|
+
* false spellings must win, and anything ambiguous must be a loud error
|
|
286
|
+
* rather than a guess, because the guess that gets this wrong deletes
|
|
287
|
+
* mail. A bare `--flag` is still `true`; `--no-flag` is still `false`.
|
|
288
|
+
*/
|
|
289
|
+
bool(name) {
|
|
290
|
+
const v = lastValue(name);
|
|
291
|
+
if (v === void 0) return false;
|
|
292
|
+
if (typeof v === "boolean") return v;
|
|
293
|
+
const normalized = v.trim().toLowerCase();
|
|
294
|
+
if (TRUE_VALUES.has(normalized)) return true;
|
|
295
|
+
if (FALSE_VALUES.has(normalized)) return false;
|
|
296
|
+
throw new UsageError(
|
|
297
|
+
`--${name} does not take a value (got "${v}").
|
|
298
|
+
If "${v}" is a positional argument, put it before the flag: \`... ${v} --${name}\`.`
|
|
299
|
+
);
|
|
300
|
+
},
|
|
301
|
+
require(name) {
|
|
302
|
+
const v = lastValue(name);
|
|
303
|
+
if (typeof v === "string") return v;
|
|
304
|
+
throw new UsageError(`Missing required flag: --${name}`);
|
|
305
|
+
},
|
|
306
|
+
requirePositional(index, label) {
|
|
307
|
+
const v = positionals[index];
|
|
308
|
+
if (v === void 0) {
|
|
309
|
+
throw new UsageError(`Missing required argument: ${label}`);
|
|
310
|
+
}
|
|
311
|
+
return v;
|
|
312
|
+
},
|
|
313
|
+
names() {
|
|
314
|
+
return [...store.keys()];
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
return api;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/config.ts
|
|
321
|
+
import { homedir as homedir2 } from "os";
|
|
322
|
+
import { join as join2 } from "path";
|
|
323
|
+
function defaultTokenCachePath(env = {}) {
|
|
324
|
+
const override = trimmed(env["GRAPH_TOKEN_CACHE"]);
|
|
325
|
+
if (override) return override;
|
|
326
|
+
const afkHome = trimmed(env["AFK_HOME"]) ?? join2(homedir2(), ".afk");
|
|
327
|
+
return join2(afkHome, "state", "outlook", "token.json");
|
|
328
|
+
}
|
|
329
|
+
function resolveConfig(env, needs = ["clientId"]) {
|
|
330
|
+
const missing = [];
|
|
331
|
+
const clientId = trimmed(env["GRAPH_CLIENT_ID"]);
|
|
332
|
+
if (needs.includes("clientId") && !clientId) {
|
|
333
|
+
missing.push("GRAPH_CLIENT_ID");
|
|
334
|
+
}
|
|
335
|
+
const clientSecret = trimmed(env["GRAPH_CLIENT_SECRET"]);
|
|
336
|
+
if (needs.includes("clientSecret") && !clientSecret) {
|
|
337
|
+
missing.push("GRAPH_CLIENT_SECRET");
|
|
338
|
+
}
|
|
339
|
+
if (missing.length > 0) {
|
|
340
|
+
throw new MissingCredentialsError(missing);
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
clientId: clientId ?? "",
|
|
344
|
+
tenantId: trimmed(env["GRAPH_TENANT_ID"]) ?? "common",
|
|
345
|
+
...clientSecret ? { clientSecret } : {},
|
|
346
|
+
tokenCachePath: defaultTokenCachePath(env)
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function authorityBase(tenantId) {
|
|
350
|
+
return `https://login.microsoftonline.com/${encodeURIComponent(tenantId)}/oauth2/v2.0`;
|
|
351
|
+
}
|
|
352
|
+
function trimmed(value) {
|
|
353
|
+
if (typeof value !== "string") return void 0;
|
|
354
|
+
const t = value.trim();
|
|
355
|
+
return t.length > 0 ? t : void 0;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/types.ts
|
|
359
|
+
var WELL_KNOWN_FOLDERS = [
|
|
360
|
+
"archive",
|
|
361
|
+
"clutter",
|
|
362
|
+
"conflicts",
|
|
363
|
+
"conversationhistory",
|
|
364
|
+
"deleteditems",
|
|
365
|
+
"drafts",
|
|
366
|
+
"inbox",
|
|
367
|
+
"junkemail",
|
|
368
|
+
"localfailures",
|
|
369
|
+
"msgfolderroot",
|
|
370
|
+
"outbox",
|
|
371
|
+
"recoverableitemsdeletions",
|
|
372
|
+
"scheduled",
|
|
373
|
+
"searchfolders",
|
|
374
|
+
"sentitems",
|
|
375
|
+
"serverfailures",
|
|
376
|
+
"syncissues"
|
|
377
|
+
];
|
|
378
|
+
function isWellKnownFolder(name) {
|
|
379
|
+
return WELL_KNOWN_FOLDERS.includes(name.toLowerCase());
|
|
380
|
+
}
|
|
381
|
+
var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
|
|
382
|
+
var REQUEST_TIMEOUT_MS = 6e4;
|
|
383
|
+
var DEFAULT_SCOPES = [
|
|
384
|
+
"offline_access",
|
|
385
|
+
"User.Read",
|
|
386
|
+
"Mail.ReadWrite",
|
|
387
|
+
"Mail.Send",
|
|
388
|
+
"MailboxSettings.ReadWrite",
|
|
389
|
+
"Contacts.Read",
|
|
390
|
+
"Calendars.Read",
|
|
391
|
+
"Files.Read.All",
|
|
392
|
+
"Sites.Read.All"
|
|
393
|
+
];
|
|
394
|
+
var READONLY_SCOPES = [
|
|
395
|
+
"offline_access",
|
|
396
|
+
"User.Read",
|
|
397
|
+
"Mail.Read",
|
|
398
|
+
"MailboxSettings.Read"
|
|
399
|
+
];
|
|
400
|
+
function scopesForMode(mode) {
|
|
401
|
+
return mode === "readonly" ? READONLY_SCOPES : DEFAULT_SCOPES;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/identityFetch.ts
|
|
405
|
+
var FORM_HEADERS = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
406
|
+
function postForm(url, params, fetchImpl) {
|
|
407
|
+
return fetchImpl(url, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: FORM_HEADERS,
|
|
410
|
+
body: params.toString(),
|
|
411
|
+
redirect: "error",
|
|
412
|
+
// Per ATTEMPT, deliberately not per operation: the device-code poll
|
|
413
|
+
// legitimately runs for the lifetime of the code, so each request inside
|
|
414
|
+
// that loop gets a fresh timeout rather than sharing one deadline.
|
|
415
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/deviceCode.ts
|
|
420
|
+
var DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
421
|
+
var DEFAULT_EXPIRES_IN_SEC = 900;
|
|
422
|
+
var DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
423
|
+
var SLOW_DOWN_INCREMENT_MS = 5e3;
|
|
424
|
+
async function requestDeviceCode(cfg, scopes, fetchImpl = fetch) {
|
|
425
|
+
const url = `${authorityBase(cfg.tenantId)}/devicecode`;
|
|
426
|
+
const params = new URLSearchParams({
|
|
427
|
+
client_id: cfg.clientId,
|
|
428
|
+
scope: scopes.join(" ")
|
|
429
|
+
});
|
|
430
|
+
const res = await postForm(url, params, fetchImpl);
|
|
431
|
+
const bodyText = await res.text().catch(() => "");
|
|
432
|
+
if (!res.ok) {
|
|
433
|
+
throw parseGraphError(res.status, bodyText);
|
|
434
|
+
}
|
|
435
|
+
const parsed = parseJsonSafe(bodyText);
|
|
436
|
+
if (!isDeviceCodeResponse(parsed)) {
|
|
437
|
+
throw new OutlookCliError(
|
|
438
|
+
"The devicecode endpoint returned a 200 response with an unrecognized body shape."
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return parsed;
|
|
442
|
+
}
|
|
443
|
+
async function pollForToken(cfg, deviceCode, opts = {}) {
|
|
444
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
445
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
446
|
+
const deadlineMs = (opts.expiresInSec ?? DEFAULT_EXPIRES_IN_SEC) * 1e3;
|
|
447
|
+
let intervalMs = (opts.intervalSec ?? DEFAULT_POLL_INTERVAL_SEC) * 1e3;
|
|
448
|
+
const url = `${authorityBase(cfg.tenantId)}/token`;
|
|
449
|
+
const params = new URLSearchParams({
|
|
450
|
+
grant_type: DEVICE_CODE_GRANT_TYPE,
|
|
451
|
+
client_id: cfg.clientId,
|
|
452
|
+
device_code: deviceCode
|
|
453
|
+
});
|
|
454
|
+
let elapsedMs = 0;
|
|
455
|
+
for (; ; ) {
|
|
456
|
+
const res = await postForm(url, params, fetchImpl);
|
|
457
|
+
const bodyText = await res.text().catch(() => "");
|
|
458
|
+
if (res.ok) {
|
|
459
|
+
const parsed = parseJsonSafe(bodyText);
|
|
460
|
+
if (!isTokenResponse(parsed)) {
|
|
461
|
+
throw new OutlookCliError(
|
|
462
|
+
"The token endpoint returned a 200 response with an unrecognized body shape."
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
return parsed;
|
|
466
|
+
}
|
|
467
|
+
const code = extractErrorCode(bodyText);
|
|
468
|
+
switch (code) {
|
|
469
|
+
case "authorization_pending":
|
|
470
|
+
break;
|
|
471
|
+
case "slow_down":
|
|
472
|
+
intervalMs += SLOW_DOWN_INCREMENT_MS;
|
|
473
|
+
break;
|
|
474
|
+
case "expired_token":
|
|
475
|
+
throw new OutlookCliError(
|
|
476
|
+
"The device code expired before sign-in was completed. Run login again."
|
|
477
|
+
);
|
|
478
|
+
case "authorization_declined":
|
|
479
|
+
throw new OutlookCliError("Sign-in was declined.");
|
|
480
|
+
case "bad_verification_code":
|
|
481
|
+
throw new OutlookCliError(
|
|
482
|
+
"The device code was not recognized by the server. This should not happen in normal operation \u2014 it indicates a bug in the polling loop, not a transient condition."
|
|
483
|
+
);
|
|
484
|
+
default:
|
|
485
|
+
throw parseGraphError(res.status, bodyText);
|
|
486
|
+
}
|
|
487
|
+
if (elapsedMs + intervalMs >= deadlineMs) {
|
|
488
|
+
throw new OutlookCliError(
|
|
489
|
+
"The device code expired before sign-in was completed (local deadline reached). Run login again."
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
await sleep(intervalMs);
|
|
493
|
+
elapsedMs += intervalMs;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
function parseJsonSafe(text) {
|
|
497
|
+
try {
|
|
498
|
+
return JSON.parse(text);
|
|
499
|
+
} catch {
|
|
500
|
+
return void 0;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function extractErrorCode(bodyText) {
|
|
504
|
+
const parsed = parseJsonSafe(bodyText);
|
|
505
|
+
if (parsed !== null && typeof parsed === "object" && "error" in parsed) {
|
|
506
|
+
const value = parsed["error"];
|
|
507
|
+
if (typeof value === "string") return value;
|
|
508
|
+
}
|
|
509
|
+
return void 0;
|
|
510
|
+
}
|
|
511
|
+
function isDeviceCodeResponse(value) {
|
|
512
|
+
if (typeof value !== "object" || value === null) return false;
|
|
513
|
+
const v = value;
|
|
514
|
+
return typeof v["device_code"] === "string" && typeof v["user_code"] === "string" && typeof v["verification_uri"] === "string" && typeof v["expires_in"] === "number" && typeof v["interval"] === "number" && (v["message"] === void 0 || typeof v["message"] === "string");
|
|
515
|
+
}
|
|
516
|
+
function isTokenResponse(value) {
|
|
517
|
+
if (typeof value !== "object" || value === null) return false;
|
|
518
|
+
const v = value;
|
|
519
|
+
return typeof v["access_token"] === "string" && typeof v["expires_in"] === "number" && (v["refresh_token"] === void 0 || typeof v["refresh_token"] === "string") && (v["scope"] === void 0 || typeof v["scope"] === "string") && (v["token_type"] === void 0 || typeof v["token_type"] === "string") && (v["id_token"] === void 0 || typeof v["id_token"] === "string");
|
|
520
|
+
}
|
|
521
|
+
function defaultSleep(ms) {
|
|
522
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/auth.ts
|
|
526
|
+
var REFRESH_SKEW_MS = 6e4;
|
|
527
|
+
function createAuthenticator(cfg, store, deps = {}) {
|
|
528
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
529
|
+
const sleep = deps.sleep ?? defaultSleep2;
|
|
530
|
+
const now = deps.now ?? Date.now;
|
|
531
|
+
async function refresh() {
|
|
532
|
+
const cached = await store.load();
|
|
533
|
+
if (!cached) throw new NotAuthenticatedError();
|
|
534
|
+
if (!cached.refreshToken) {
|
|
535
|
+
throw new NotAuthenticatedError("cached session has no refresh token; run login again");
|
|
536
|
+
}
|
|
537
|
+
const mode = cached.mode ?? "full";
|
|
538
|
+
const scopes = cached.grantScopes ?? scopesForMode(mode);
|
|
539
|
+
const tokenResponse = await exchangeRefreshToken(
|
|
540
|
+
cfg,
|
|
541
|
+
cached.refreshToken,
|
|
542
|
+
scopes,
|
|
543
|
+
fetchImpl
|
|
544
|
+
);
|
|
545
|
+
const nextTokens = toTokenSet(tokenResponse, now(), cached);
|
|
546
|
+
await store.save(nextTokens);
|
|
547
|
+
return nextTokens;
|
|
548
|
+
}
|
|
549
|
+
return {
|
|
550
|
+
async getAccessToken() {
|
|
551
|
+
const cached = await store.load();
|
|
552
|
+
if (!cached) throw new NotAuthenticatedError();
|
|
553
|
+
if (cached.expiresAt - now() < REFRESH_SKEW_MS) {
|
|
554
|
+
const refreshed = await refresh();
|
|
555
|
+
return refreshed.accessToken;
|
|
556
|
+
}
|
|
557
|
+
return cached.accessToken;
|
|
558
|
+
},
|
|
559
|
+
async login(onPrompt, opts = {}) {
|
|
560
|
+
const mode = opts.readOnly ? "readonly" : "full";
|
|
561
|
+
const custom = opts.scopes !== void 0 ? [...opts.scopes] : void 0;
|
|
562
|
+
const deviceCode = await requestDeviceCode(
|
|
563
|
+
cfg,
|
|
564
|
+
custom ?? scopesForMode(mode),
|
|
565
|
+
fetchImpl
|
|
566
|
+
);
|
|
567
|
+
onPrompt(deviceCode);
|
|
568
|
+
const tokenResponse = await pollForToken(cfg, deviceCode.device_code, {
|
|
569
|
+
intervalSec: deviceCode.interval,
|
|
570
|
+
expiresInSec: deviceCode.expires_in,
|
|
571
|
+
fetchImpl,
|
|
572
|
+
sleep
|
|
573
|
+
});
|
|
574
|
+
const tokens = {
|
|
575
|
+
...toTokenSet(tokenResponse, now()),
|
|
576
|
+
mode,
|
|
577
|
+
...custom !== void 0 ? { grantScopes: custom } : {}
|
|
578
|
+
};
|
|
579
|
+
await store.save(tokens);
|
|
580
|
+
return tokens;
|
|
581
|
+
},
|
|
582
|
+
async logout() {
|
|
583
|
+
await store.clear();
|
|
584
|
+
},
|
|
585
|
+
refresh,
|
|
586
|
+
async status() {
|
|
587
|
+
const cached = await store.load();
|
|
588
|
+
if (!cached) return { signedIn: false, expired: false };
|
|
589
|
+
return {
|
|
590
|
+
signedIn: true,
|
|
591
|
+
expired: now() >= cached.expiresAt,
|
|
592
|
+
expiresAt: cached.expiresAt,
|
|
593
|
+
...cached.account !== void 0 ? { account: cached.account } : {},
|
|
594
|
+
...cached.scope !== void 0 ? { scopes: cached.scope } : {},
|
|
595
|
+
...cached.mode !== void 0 ? { mode: cached.mode } : {},
|
|
596
|
+
...cached.grantScopes !== void 0 ? { grantScopes: cached.grantScopes } : {}
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
async function exchangeRefreshToken(cfg, refreshToken, scopes, fetchImpl) {
|
|
602
|
+
const url = `${authorityBase(cfg.tenantId)}/token`;
|
|
603
|
+
const params = new URLSearchParams({
|
|
604
|
+
grant_type: "refresh_token",
|
|
605
|
+
client_id: cfg.clientId,
|
|
606
|
+
refresh_token: refreshToken,
|
|
607
|
+
scope: scopes.join(" ")
|
|
608
|
+
});
|
|
609
|
+
const res = await postForm(url, params, fetchImpl);
|
|
610
|
+
const bodyText = await res.text().catch(() => "");
|
|
611
|
+
if (!res.ok) {
|
|
612
|
+
throw parseGraphError(res.status, bodyText);
|
|
613
|
+
}
|
|
614
|
+
let parsed;
|
|
615
|
+
try {
|
|
616
|
+
parsed = JSON.parse(bodyText);
|
|
617
|
+
} catch {
|
|
618
|
+
throw new OutlookCliError(
|
|
619
|
+
"The token endpoint returned a 200 response with an unparseable body during refresh."
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
if (!isTokenResponse2(parsed)) {
|
|
623
|
+
throw new OutlookCliError(
|
|
624
|
+
"The token endpoint returned a 200 response with an unrecognized body shape during refresh."
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
return parsed;
|
|
628
|
+
}
|
|
629
|
+
function toTokenSet(tokenResponse, nowMs, previous) {
|
|
630
|
+
const account = tokenResponse.id_token ? decodeIdTokenAccount(tokenResponse.id_token) ?? previous?.account : previous?.account;
|
|
631
|
+
const refreshToken = tokenResponse.refresh_token ?? previous?.refreshToken;
|
|
632
|
+
const scope = tokenResponse.scope ?? previous?.scope;
|
|
633
|
+
const tokenType = tokenResponse.token_type ?? previous?.tokenType;
|
|
634
|
+
const mode = previous?.mode;
|
|
635
|
+
const grantScopes = previous?.grantScopes;
|
|
636
|
+
return {
|
|
637
|
+
accessToken: tokenResponse.access_token,
|
|
638
|
+
expiresAt: nowMs + tokenResponse.expires_in * 1e3,
|
|
639
|
+
...refreshToken !== void 0 ? { refreshToken } : {},
|
|
640
|
+
...scope !== void 0 ? { scope } : {},
|
|
641
|
+
...tokenType !== void 0 ? { tokenType } : {},
|
|
642
|
+
...account !== void 0 ? { account } : {},
|
|
643
|
+
...mode !== void 0 ? { mode } : {},
|
|
644
|
+
...grantScopes !== void 0 ? { grantScopes } : {}
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
function decodeIdTokenAccount(idToken) {
|
|
648
|
+
try {
|
|
649
|
+
const parts = idToken.split(".");
|
|
650
|
+
if (parts.length !== 3) return void 0;
|
|
651
|
+
const payloadSegment = parts[1];
|
|
652
|
+
if (!payloadSegment) return void 0;
|
|
653
|
+
const json = Buffer.from(payloadSegment, "base64url").toString("utf8");
|
|
654
|
+
const claims = JSON.parse(json);
|
|
655
|
+
if (typeof claims !== "object" || claims === null) return void 0;
|
|
656
|
+
const c = claims;
|
|
657
|
+
const candidates = [c["preferred_username"], c["email"], c["upn"], c["name"]];
|
|
658
|
+
for (const candidate of candidates) {
|
|
659
|
+
if (typeof candidate === "string" && candidate.length > 0) return candidate;
|
|
660
|
+
}
|
|
661
|
+
return void 0;
|
|
662
|
+
} catch {
|
|
663
|
+
return void 0;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
function isTokenResponse2(value) {
|
|
667
|
+
if (typeof value !== "object" || value === null) return false;
|
|
668
|
+
const v = value;
|
|
669
|
+
return typeof v["access_token"] === "string" && typeof v["expires_in"] === "number";
|
|
670
|
+
}
|
|
671
|
+
function defaultSleep2(ms) {
|
|
672
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/client.ts
|
|
676
|
+
var UntrustedUrlError = class extends OutlookCliError {
|
|
677
|
+
constructor(message) {
|
|
678
|
+
super(message, 1);
|
|
679
|
+
this.name = "UntrustedUrlError";
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
var RETRYABLE_METHODS = /* @__PURE__ */ new Set(["GET", "DELETE", "PATCH"]);
|
|
683
|
+
var RETRYABLE_STATUSES = /* @__PURE__ */ new Set([429, 503, 504]);
|
|
684
|
+
var DEFAULT_MAX_RETRIES = 4;
|
|
685
|
+
var BASE_BACKOFF_MS = 500;
|
|
686
|
+
var MAX_BACKOFF_MS = 3e4;
|
|
687
|
+
var ABSOLUTE_URL_RE = /^https?:\/\//i;
|
|
688
|
+
var GraphClient = class {
|
|
689
|
+
getToken;
|
|
690
|
+
fetchImpl;
|
|
691
|
+
sleep;
|
|
692
|
+
baseUrl;
|
|
693
|
+
maxRetries;
|
|
694
|
+
/** Derived from baseUrl, so a national-cloud endpoint stays configurable. */
|
|
695
|
+
allowedHost;
|
|
696
|
+
constructor(deps) {
|
|
697
|
+
this.getToken = deps.getToken;
|
|
698
|
+
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
699
|
+
this.sleep = deps.sleep ?? defaultSleep3;
|
|
700
|
+
this.baseUrl = deps.baseUrl ?? GRAPH_BASE;
|
|
701
|
+
this.maxRetries = deps.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
702
|
+
this.allowedHost = new URL(this.baseUrl).host;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Pure request-spec builder — no `getToken()` call, no network. This is
|
|
706
|
+
* what `--dry-run` renders. Authorization is attached later, only inside
|
|
707
|
+
* the actual send path.
|
|
708
|
+
*/
|
|
709
|
+
buildSpec(method, path, body, query) {
|
|
710
|
+
const upperMethod = method.toUpperCase();
|
|
711
|
+
const url = this.buildUrl(path, query);
|
|
712
|
+
const headers = {};
|
|
713
|
+
if (upperMethod === "GET") {
|
|
714
|
+
headers["Prefer"] = 'IdType="ImmutableId"';
|
|
715
|
+
}
|
|
716
|
+
if (body !== void 0) {
|
|
717
|
+
headers["Content-Type"] = "application/json";
|
|
718
|
+
}
|
|
719
|
+
const spec = { method: upperMethod, url, headers };
|
|
720
|
+
if (body !== void 0) spec.body = body;
|
|
721
|
+
return spec;
|
|
722
|
+
}
|
|
723
|
+
async request(spec) {
|
|
724
|
+
const res = await this.executeWithRetry(spec);
|
|
725
|
+
return parseSuccessBody(res);
|
|
726
|
+
}
|
|
727
|
+
/** Raw Response, for binary reads (e.g. attachment/$value download). */
|
|
728
|
+
async raw(spec) {
|
|
729
|
+
return this.executeWithRetry(spec);
|
|
730
|
+
}
|
|
731
|
+
async get(path, query) {
|
|
732
|
+
return this.request(this.buildSpec("GET", path, void 0, query));
|
|
733
|
+
}
|
|
734
|
+
async post(path, body) {
|
|
735
|
+
return this.request(this.buildSpec("POST", path, body));
|
|
736
|
+
}
|
|
737
|
+
async patch(path, body) {
|
|
738
|
+
return this.request(this.buildSpec("PATCH", path, body));
|
|
739
|
+
}
|
|
740
|
+
async del(path) {
|
|
741
|
+
await this.request(this.buildSpec("DELETE", path));
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* The single place a bearer token is attached to an outbound request — and
|
|
745
|
+
* therefore the only correct place to decide whether this URL is allowed to
|
|
746
|
+
* receive one.
|
|
747
|
+
*
|
|
748
|
+
* `buildUrl` passes an absolute `path` straight through so a server-issued
|
|
749
|
+
* `@odata.nextLink` can be followed verbatim. That link is response data: a
|
|
750
|
+
* spoofed, MITM'd, or malformed one pointing at `http://attacker.example`
|
|
751
|
+
* previously received the live mailbox token, in cleartext, and a stolen
|
|
752
|
+
* Graph token replays against real Graph regardless of who presents it.
|
|
753
|
+
* Validating here rather than in `buildUrl` means no future caller can
|
|
754
|
+
* reintroduce the hole by constructing a spec some other way.
|
|
755
|
+
*/
|
|
756
|
+
assertTokenMayBeSent(url) {
|
|
757
|
+
let parsed;
|
|
758
|
+
try {
|
|
759
|
+
parsed = new URL(url);
|
|
760
|
+
} catch {
|
|
761
|
+
throw new UntrustedUrlError(`Refusing to send credentials to a malformed URL: ${url}`);
|
|
762
|
+
}
|
|
763
|
+
if (parsed.protocol !== "https:") {
|
|
764
|
+
throw new UntrustedUrlError(
|
|
765
|
+
`Refusing to send credentials over ${parsed.protocol}// \u2014 HTTPS is required (${url})`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
if (parsed.host !== this.allowedHost) {
|
|
769
|
+
throw new UntrustedUrlError(
|
|
770
|
+
`Refusing to send credentials to '${parsed.host}' \u2014 only '${this.allowedHost}' is authorized for this client.`
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async send(spec) {
|
|
775
|
+
this.assertTokenMayBeSent(spec.url);
|
|
776
|
+
const token = await this.getToken();
|
|
777
|
+
const headers = {
|
|
778
|
+
...spec.headers ?? {},
|
|
779
|
+
Authorization: `Bearer ${token}`
|
|
780
|
+
};
|
|
781
|
+
const init = {
|
|
782
|
+
method: spec.method,
|
|
783
|
+
headers,
|
|
784
|
+
// A 3xx to another origin would otherwise replay the Authorization
|
|
785
|
+
// header there. Graph does not redirect its API responses; treat one as
|
|
786
|
+
// hostile rather than following it.
|
|
787
|
+
redirect: "error",
|
|
788
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
|
|
789
|
+
};
|
|
790
|
+
if (spec.body !== void 0) {
|
|
791
|
+
init.body = JSON.stringify(spec.body);
|
|
792
|
+
}
|
|
793
|
+
return this.fetchImpl(spec.url, init);
|
|
794
|
+
}
|
|
795
|
+
async executeWithRetry(spec) {
|
|
796
|
+
const method = spec.method.toUpperCase();
|
|
797
|
+
const retryEligible = RETRYABLE_METHODS.has(method);
|
|
798
|
+
let attempt = 0;
|
|
799
|
+
for (; ; ) {
|
|
800
|
+
const res = await this.send(spec);
|
|
801
|
+
if (res.ok) return res;
|
|
802
|
+
const willRetry = retryEligible && RETRYABLE_STATUSES.has(res.status) && attempt < this.maxRetries;
|
|
803
|
+
if (!willRetry) {
|
|
804
|
+
const bodyText = await res.text().catch(() => "");
|
|
805
|
+
const retryAfter = parseRetryAfterSeconds(res.headers.get("retry-after"));
|
|
806
|
+
throw parseGraphError(res.status, bodyText, retryAfter);
|
|
807
|
+
}
|
|
808
|
+
const delayMs = computeDelayMs(res.headers.get("retry-after"), attempt);
|
|
809
|
+
await this.sleep(delayMs);
|
|
810
|
+
attempt += 1;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
buildUrl(path, query) {
|
|
814
|
+
const isAbsolute2 = ABSOLUTE_URL_RE.test(path);
|
|
815
|
+
const base = isAbsolute2 ? path : joinUrl(this.baseUrl, path);
|
|
816
|
+
if (isAbsolute2 || !query) return base;
|
|
817
|
+
const qs = buildQueryString(query);
|
|
818
|
+
if (!qs) return base;
|
|
819
|
+
return `${base}${base.includes("?") ? "&" : "?"}${qs}`;
|
|
820
|
+
}
|
|
821
|
+
};
|
|
822
|
+
function joinUrl(base, path) {
|
|
823
|
+
const trimmedBase = base.endsWith("/") ? base.slice(0, -1) : base;
|
|
824
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
825
|
+
return `${trimmedBase}${normalizedPath}`;
|
|
826
|
+
}
|
|
827
|
+
function buildQueryString(query) {
|
|
828
|
+
const parts = [];
|
|
829
|
+
for (const [key, rawValue] of Object.entries(query)) {
|
|
830
|
+
if (rawValue === void 0 || rawValue === null) continue;
|
|
831
|
+
parts.push(`${encodeQueryKey(key)}=${encodeURIComponent(String(rawValue))}`);
|
|
832
|
+
}
|
|
833
|
+
return parts.join("&");
|
|
834
|
+
}
|
|
835
|
+
function encodeQueryKey(key) {
|
|
836
|
+
return key.startsWith("$") ? `$${encodeURIComponent(key.slice(1))}` : encodeURIComponent(key);
|
|
837
|
+
}
|
|
838
|
+
function parseRetryAfterSeconds(header) {
|
|
839
|
+
if (header == null) return void 0;
|
|
840
|
+
const trimmed3 = header.trim();
|
|
841
|
+
if (trimmed3.length === 0) return void 0;
|
|
842
|
+
const asNumber = Number(trimmed3);
|
|
843
|
+
if (Number.isFinite(asNumber) && asNumber >= 0) return asNumber;
|
|
844
|
+
const asDate = Date.parse(trimmed3);
|
|
845
|
+
if (!Number.isNaN(asDate)) {
|
|
846
|
+
return Math.max(0, (asDate - Date.now()) / 1e3);
|
|
847
|
+
}
|
|
848
|
+
return void 0;
|
|
849
|
+
}
|
|
850
|
+
function computeDelayMs(retryAfterHeader, attempt) {
|
|
851
|
+
const seconds = parseRetryAfterSeconds(retryAfterHeader);
|
|
852
|
+
if (seconds !== void 0) return seconds * 1e3;
|
|
853
|
+
return fullJitterBackoffMs(attempt);
|
|
854
|
+
}
|
|
855
|
+
function fullJitterBackoffMs(attempt) {
|
|
856
|
+
const cap = Math.min(MAX_BACKOFF_MS, BASE_BACKOFF_MS * 2 ** attempt);
|
|
857
|
+
return Math.random() * cap;
|
|
858
|
+
}
|
|
859
|
+
function defaultSleep3(ms) {
|
|
860
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
861
|
+
}
|
|
862
|
+
async function parseSuccessBody(res) {
|
|
863
|
+
if (res.status === 204) return void 0;
|
|
864
|
+
const text = await res.text();
|
|
865
|
+
if (!text) return void 0;
|
|
866
|
+
try {
|
|
867
|
+
return JSON.parse(text);
|
|
868
|
+
} catch {
|
|
869
|
+
return void 0;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/flagSpec.ts
|
|
874
|
+
var GLOBAL_FLAGS = ["json", "dry-run", "help", "h"];
|
|
875
|
+
var COMPOSE_FLAGS = [
|
|
876
|
+
"to",
|
|
877
|
+
"cc",
|
|
878
|
+
"bcc",
|
|
879
|
+
"subject",
|
|
880
|
+
"body",
|
|
881
|
+
"body-file",
|
|
882
|
+
"html",
|
|
883
|
+
"attach",
|
|
884
|
+
"signature"
|
|
885
|
+
];
|
|
886
|
+
var COMMAND_FLAGS = {
|
|
887
|
+
auth: ["readonly", "scopes"],
|
|
888
|
+
me: [],
|
|
889
|
+
list: ["folder", "limit", "all", "unread"],
|
|
890
|
+
get: ["html"],
|
|
891
|
+
search: ["limit", "all", "folder"],
|
|
892
|
+
folders: ["parent", "all", "name"],
|
|
893
|
+
attachments: ["all", "out", "force"],
|
|
894
|
+
contacts: ["top", "all", "limit", "select", "filter"],
|
|
895
|
+
send: [...COMPOSE_FLAGS, "save-sent"],
|
|
896
|
+
reply: ["body", "signature"],
|
|
897
|
+
"reply-all": ["body", "signature"],
|
|
898
|
+
forward: ["body", "to", "signature"],
|
|
899
|
+
draft: [...COMPOSE_FLAGS, "limit", "all"],
|
|
900
|
+
move: ["folder"],
|
|
901
|
+
delete: [],
|
|
902
|
+
purge: ["yes"],
|
|
903
|
+
"mark-read": [],
|
|
904
|
+
"mark-unread": [],
|
|
905
|
+
flag: [],
|
|
906
|
+
unflag: [],
|
|
907
|
+
rules: ["name", "folder", "from", "sender-contains", "mark-read", "stop-processing", "enabled", "sequence"]
|
|
908
|
+
};
|
|
909
|
+
function editDistance(a, b) {
|
|
910
|
+
const rows = a.length + 1;
|
|
911
|
+
const cols = b.length + 1;
|
|
912
|
+
let prev = Array.from({ length: cols }, (_, j) => j);
|
|
913
|
+
for (let i = 1; i < rows; i++) {
|
|
914
|
+
const curr = [i];
|
|
915
|
+
for (let j = 1; j < cols; j++) {
|
|
916
|
+
const substitution = (prev[j - 1] ?? 0) + (a[i - 1] === b[j - 1] ? 0 : 1);
|
|
917
|
+
const insertion = (curr[j - 1] ?? 0) + 1;
|
|
918
|
+
const deletion = (prev[j] ?? 0) + 1;
|
|
919
|
+
curr[j] = Math.min(substitution, insertion, deletion);
|
|
920
|
+
}
|
|
921
|
+
prev = curr;
|
|
922
|
+
}
|
|
923
|
+
return prev[cols - 1] ?? Math.max(a.length, b.length);
|
|
924
|
+
}
|
|
925
|
+
function suggest(unknown, allowed) {
|
|
926
|
+
let best;
|
|
927
|
+
let bestScore = Number.POSITIVE_INFINITY;
|
|
928
|
+
for (const candidate of allowed) {
|
|
929
|
+
const score = editDistance(unknown.toLowerCase(), candidate.toLowerCase());
|
|
930
|
+
if (score < bestScore) {
|
|
931
|
+
bestScore = score;
|
|
932
|
+
best = candidate;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
const ceiling = Math.min(3, Math.max(1, Math.floor(unknown.length / 2)));
|
|
936
|
+
return best !== void 0 && bestScore <= ceiling ? best : void 0;
|
|
937
|
+
}
|
|
938
|
+
function assertKnownFlags(commandName, used) {
|
|
939
|
+
const own = COMMAND_FLAGS[commandName];
|
|
940
|
+
if (own === void 0) {
|
|
941
|
+
throw new UsageError(
|
|
942
|
+
`Internal: no flag specification registered for command '${commandName}'.`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
const allowed = [...GLOBAL_FLAGS, ...own];
|
|
946
|
+
const allowedSet = new Set(allowed);
|
|
947
|
+
const unknown = used.filter((name) => !allowedSet.has(name));
|
|
948
|
+
if (unknown.length === 0) return;
|
|
949
|
+
const first = unknown[0] ?? "";
|
|
950
|
+
const hint = suggest(first, allowed);
|
|
951
|
+
const lines = [
|
|
952
|
+
`Unknown flag for '${commandName}': --${unknown.map((u) => u).join(", --")}`
|
|
953
|
+
];
|
|
954
|
+
if (hint !== void 0) lines.push(`Did you mean --${hint}?`);
|
|
955
|
+
lines.push(
|
|
956
|
+
"",
|
|
957
|
+
`Accepted: ${allowed.map((f) => `--${f}`).join(" ")}`,
|
|
958
|
+
`Run 'outlook ${commandName} --help' for usage.`
|
|
959
|
+
);
|
|
960
|
+
throw new UsageError(lines.join("\n"));
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// src/redact.ts
|
|
964
|
+
var SECRET_KEYS = /* @__PURE__ */ new Set([
|
|
965
|
+
"access_token",
|
|
966
|
+
"refresh_token",
|
|
967
|
+
"id_token",
|
|
968
|
+
"device_code",
|
|
969
|
+
"client_secret",
|
|
970
|
+
"accesstoken",
|
|
971
|
+
"refreshtoken",
|
|
972
|
+
"authorization",
|
|
973
|
+
"code_verifier"
|
|
974
|
+
]);
|
|
975
|
+
var JWT_RE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
|
|
976
|
+
var BEARER_RE = /\b(Bearer\s+)[A-Za-z0-9._~+/-]{20,}=*/gi;
|
|
977
|
+
var OPAQUE_TOKEN_RE = /\b[0-9A-Za-z]\.[A-Za-z0-9_-]{40,}/g;
|
|
978
|
+
var MASK = "[REDACTED]";
|
|
979
|
+
function maskSecret(value) {
|
|
980
|
+
if (!value) return MASK;
|
|
981
|
+
if (value.length <= 8) return MASK;
|
|
982
|
+
return `${MASK}(****${value.slice(-4)})`;
|
|
983
|
+
}
|
|
984
|
+
function redactText(text) {
|
|
985
|
+
if (!text) return text;
|
|
986
|
+
return text.replace(JWT_RE, MASK).replace(BEARER_RE, (_m, p1) => `${p1}${MASK}`).replace(OPAQUE_TOKEN_RE, MASK);
|
|
987
|
+
}
|
|
988
|
+
function redactObject(input) {
|
|
989
|
+
return walk(input, 0);
|
|
990
|
+
}
|
|
991
|
+
function walk(value, depth) {
|
|
992
|
+
if (depth > 12) return MASK;
|
|
993
|
+
if (typeof value === "string") return redactText(value);
|
|
994
|
+
if (value === null || typeof value !== "object") return value;
|
|
995
|
+
if (Array.isArray(value)) {
|
|
996
|
+
return value.map((item) => walk(item, depth + 1));
|
|
997
|
+
}
|
|
998
|
+
const out = {};
|
|
999
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1000
|
+
if (SECRET_KEYS.has(key.toLowerCase())) {
|
|
1001
|
+
out[key] = typeof val === "string" ? maskSecret(val) : MASK;
|
|
1002
|
+
} else {
|
|
1003
|
+
out[key] = walk(val, depth + 1);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
1008
|
+
function redactHeaders(headers) {
|
|
1009
|
+
const out = {};
|
|
1010
|
+
for (const [key, val] of Object.entries(headers)) {
|
|
1011
|
+
out[key] = SECRET_KEYS.has(key.toLowerCase()) ? MASK : redactText(val);
|
|
1012
|
+
}
|
|
1013
|
+
return out;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// src/tokenStore.ts
|
|
1017
|
+
import { randomBytes } from "crypto";
|
|
1018
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1019
|
+
import { dirname, join as join3 } from "path";
|
|
1020
|
+
|
|
1021
|
+
// src/scopes.ts
|
|
1022
|
+
var SCOPE_RE = /^[A-Za-z0-9._:/-]+$/;
|
|
1023
|
+
var OFFLINE_ACCESS = "offline_access";
|
|
1024
|
+
function parseScopeList(raw) {
|
|
1025
|
+
const tokens = raw.split(/[\s,]+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1026
|
+
if (tokens.length === 0) {
|
|
1027
|
+
throw new UsageError("--scopes needs at least one scope, e.g. --scopes Mail.ReadWrite");
|
|
1028
|
+
}
|
|
1029
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1030
|
+
const scopes = [];
|
|
1031
|
+
for (const token of tokens) {
|
|
1032
|
+
if (!SCOPE_RE.test(token)) {
|
|
1033
|
+
throw new UsageError(`--scopes: "${token}" is not a valid scope name.`);
|
|
1034
|
+
}
|
|
1035
|
+
const key = token.toLowerCase();
|
|
1036
|
+
if (seen.has(key)) continue;
|
|
1037
|
+
seen.add(key);
|
|
1038
|
+
scopes.push(token);
|
|
1039
|
+
}
|
|
1040
|
+
if (!seen.has(OFFLINE_ACCESS)) scopes.unshift(OFFLINE_ACCESS);
|
|
1041
|
+
return scopes;
|
|
1042
|
+
}
|
|
1043
|
+
function isScopeArray(value) {
|
|
1044
|
+
return Array.isArray(value) && value.length > 0 && value.every((s) => typeof s === "string" && SCOPE_RE.test(s));
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// src/tokenStore.ts
|
|
1048
|
+
var FILE_MODE = 384;
|
|
1049
|
+
var DIR_MODE = 448;
|
|
1050
|
+
var FILE_MODE_IS_ENFORCED = process.platform !== "win32";
|
|
1051
|
+
function createTokenStore(path) {
|
|
1052
|
+
return {
|
|
1053
|
+
path,
|
|
1054
|
+
async load() {
|
|
1055
|
+
try {
|
|
1056
|
+
const raw = await readFile(path, "utf8");
|
|
1057
|
+
const parsed = JSON.parse(raw);
|
|
1058
|
+
return isTokenSet(parsed) ? parsed : null;
|
|
1059
|
+
} catch {
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
1062
|
+
},
|
|
1063
|
+
/**
|
|
1064
|
+
* Atomic: write a fresh temp file, lock it down, then rename over the
|
|
1065
|
+
* target. rename(2) is atomic within a directory, so a reader either sees
|
|
1066
|
+
* the whole old token set or the whole new one — never a half-written
|
|
1067
|
+
* file. The previous in-place writeFile could leave a truncated cache
|
|
1068
|
+
* behind if the process died mid-write, or interleave two concurrent
|
|
1069
|
+
* refreshes into corrupt JSON, either of which forces a re-login.
|
|
1070
|
+
*
|
|
1071
|
+
* The temp file carries the restrictive mode from creation, so the secret
|
|
1072
|
+
* is never briefly world-readable in the window before chmod lands.
|
|
1073
|
+
*
|
|
1074
|
+
* The temp NAME is random and the open is exclusive. The previous
|
|
1075
|
+
* `.${pid}-${Date.now()}.token.tmp` was guessable, and writeFile's default
|
|
1076
|
+
* O_CREAT|O_TRUNC follows a symlink — so a link pre-planted at that path
|
|
1077
|
+
* would have received the token at whatever destination it pointed to.
|
|
1078
|
+
* 'wx' (O_CREAT|O_EXCL) refuses to open anything that already exists.
|
|
1079
|
+
*/
|
|
1080
|
+
async save(tokens) {
|
|
1081
|
+
const dir = dirname(path);
|
|
1082
|
+
await mkdir(dir, { recursive: true, mode: DIR_MODE });
|
|
1083
|
+
const tmp = join3(dir, `.${randomBytes(8).toString("hex")}.token.tmp`);
|
|
1084
|
+
try {
|
|
1085
|
+
await writeFile(tmp, JSON.stringify(tokens, null, 2), {
|
|
1086
|
+
mode: FILE_MODE,
|
|
1087
|
+
flag: "wx"
|
|
1088
|
+
});
|
|
1089
|
+
await chmod(tmp, FILE_MODE);
|
|
1090
|
+
await rename(tmp, path);
|
|
1091
|
+
} catch (error) {
|
|
1092
|
+
if (error.code !== "EEXIST") {
|
|
1093
|
+
await rm(tmp, { force: true }).catch(() => void 0);
|
|
1094
|
+
}
|
|
1095
|
+
throw error;
|
|
1096
|
+
}
|
|
1097
|
+
},
|
|
1098
|
+
async clear() {
|
|
1099
|
+
await rm(path, { force: true });
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function isTokenSet(value) {
|
|
1104
|
+
if (typeof value !== "object" || value === null) return false;
|
|
1105
|
+
const v = value;
|
|
1106
|
+
if (typeof v["accessToken"] !== "string" || v["accessToken"].length === 0) return false;
|
|
1107
|
+
if (typeof v["expiresAt"] !== "number" || !Number.isFinite(v["expiresAt"])) return false;
|
|
1108
|
+
if (v["refreshToken"] !== void 0 && typeof v["refreshToken"] !== "string") return false;
|
|
1109
|
+
if (v["scope"] !== void 0 && typeof v["scope"] !== "string") return false;
|
|
1110
|
+
if (v["tokenType"] !== void 0 && typeof v["tokenType"] !== "string") return false;
|
|
1111
|
+
if (v["account"] !== void 0 && typeof v["account"] !== "string") return false;
|
|
1112
|
+
if (v["mode"] !== void 0 && v["mode"] !== "readonly" && v["mode"] !== "full") {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
if (v["grantScopes"] !== void 0 && !isScopeArray(v["grantScopes"])) return false;
|
|
1116
|
+
return true;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/commands/attachments.ts
|
|
1120
|
+
import { access, mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
1121
|
+
import { basename as basename2, dirname as dirname3, isAbsolute, join as join4, relative, resolve as resolve2, sep } from "path";
|
|
1122
|
+
|
|
1123
|
+
// src/dryRun.ts
|
|
1124
|
+
function renderRequest(spec) {
|
|
1125
|
+
const lines = [];
|
|
1126
|
+
lines.push(`${spec.method} ${spec.url}`);
|
|
1127
|
+
const headers = redactHeaders(spec.headers ?? {});
|
|
1128
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1129
|
+
lines.push(`${key}: ${value}`);
|
|
1130
|
+
}
|
|
1131
|
+
if (spec.body !== void 0) {
|
|
1132
|
+
lines.push("");
|
|
1133
|
+
lines.push(JSON.stringify(redactObject(spec.body), null, 2));
|
|
1134
|
+
}
|
|
1135
|
+
return lines.join("\n");
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// src/policyGuard.ts
|
|
1139
|
+
import { dirname as dirname2, resolve } from "path";
|
|
1140
|
+
|
|
1141
|
+
// src/policy.ts
|
|
1142
|
+
import { readFile as readFile2, stat } from "fs/promises";
|
|
1143
|
+
var POLICY_ENV = "OUTLOOK_POLICY";
|
|
1144
|
+
var TOP_KEYS = /* @__PURE__ */ new Set(["allowCommands", "move"]);
|
|
1145
|
+
var MOVE_KEYS = /* @__PURE__ */ new Set(["allowedDestinations", "denySenders"]);
|
|
1146
|
+
var DENY_KEYS = /* @__PURE__ */ new Set(["file", "maxAgeHours", "destinations"]);
|
|
1147
|
+
function stripBom(text) {
|
|
1148
|
+
return text.charCodeAt(0) === 65279 ? text.slice(1) : text;
|
|
1149
|
+
}
|
|
1150
|
+
function isRecord(v) {
|
|
1151
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1152
|
+
}
|
|
1153
|
+
function isStringList(v, allowEmpty) {
|
|
1154
|
+
return Array.isArray(v) && (allowEmpty || v.length > 0) && v.every((s) => typeof s === "string" && s.trim().length > 0);
|
|
1155
|
+
}
|
|
1156
|
+
function unknownKey(obj, known) {
|
|
1157
|
+
return Object.keys(obj).find((k) => !known.has(k) && !k.startsWith("$"));
|
|
1158
|
+
}
|
|
1159
|
+
function validatePolicy(v) {
|
|
1160
|
+
if (!isRecord(v)) return "top level must be a JSON object";
|
|
1161
|
+
const extra = unknownKey(v, TOP_KEYS);
|
|
1162
|
+
if (extra !== void 0) return `unknown key "${extra}"`;
|
|
1163
|
+
if (!isStringList(v["allowCommands"], true)) {
|
|
1164
|
+
return '"allowCommands" must be an array of command names (it may be empty)';
|
|
1165
|
+
}
|
|
1166
|
+
const move = v["move"];
|
|
1167
|
+
if (move === void 0) return void 0;
|
|
1168
|
+
if (!isRecord(move)) return '"move" must be an object';
|
|
1169
|
+
const extraMove = unknownKey(move, MOVE_KEYS);
|
|
1170
|
+
if (extraMove !== void 0) return `unknown key "move.${extraMove}"`;
|
|
1171
|
+
const allowed = move["allowedDestinations"];
|
|
1172
|
+
if (allowed !== void 0 && !isStringList(allowed, true)) {
|
|
1173
|
+
return '"move.allowedDestinations" must be an array of folder ids or well-known names';
|
|
1174
|
+
}
|
|
1175
|
+
const deny = move["denySenders"];
|
|
1176
|
+
if (deny === void 0) return void 0;
|
|
1177
|
+
if (!isRecord(deny)) return '"move.denySenders" must be an object';
|
|
1178
|
+
const extraDeny = unknownKey(deny, DENY_KEYS);
|
|
1179
|
+
if (extraDeny !== void 0) return `unknown key "move.denySenders.${extraDeny}"`;
|
|
1180
|
+
if (typeof deny["file"] !== "string" || deny["file"].trim() === "") {
|
|
1181
|
+
return '"move.denySenders.file" must be a path';
|
|
1182
|
+
}
|
|
1183
|
+
const denyFile = deny["file"];
|
|
1184
|
+
if (denyFile.startsWith("/") || denyFile.includes("\\") || denyFile.split("/").some((seg) => seg === "..")) {
|
|
1185
|
+
return `"move.denySenders.file" must be a relative path with no '..' segments or backslashes`;
|
|
1186
|
+
}
|
|
1187
|
+
const maxAge = deny["maxAgeHours"];
|
|
1188
|
+
if (typeof maxAge !== "number" || !Number.isFinite(maxAge) || maxAge <= 0) {
|
|
1189
|
+
return '"move.denySenders.maxAgeHours" must be a positive number';
|
|
1190
|
+
}
|
|
1191
|
+
if (!isStringList(deny["destinations"], false)) {
|
|
1192
|
+
return '"move.denySenders.destinations" must be a non-empty array of folder ids';
|
|
1193
|
+
}
|
|
1194
|
+
return void 0;
|
|
1195
|
+
}
|
|
1196
|
+
async function loadPolicy(path) {
|
|
1197
|
+
let raw;
|
|
1198
|
+
try {
|
|
1199
|
+
raw = await readFile2(path, "utf8");
|
|
1200
|
+
} catch (error) {
|
|
1201
|
+
const code = error.code ?? "read error";
|
|
1202
|
+
return { ok: false, reason: `cannot read policy file ${path} (${code})` };
|
|
1203
|
+
}
|
|
1204
|
+
let parsed;
|
|
1205
|
+
try {
|
|
1206
|
+
parsed = JSON.parse(stripBom(raw));
|
|
1207
|
+
} catch {
|
|
1208
|
+
return { ok: false, reason: `policy file ${path} is not valid JSON` };
|
|
1209
|
+
}
|
|
1210
|
+
const problem = validatePolicy(parsed);
|
|
1211
|
+
if (problem !== void 0) {
|
|
1212
|
+
return { ok: false, reason: `policy file ${path} is invalid: ${problem}` };
|
|
1213
|
+
}
|
|
1214
|
+
return { ok: true, value: parsed };
|
|
1215
|
+
}
|
|
1216
|
+
async function loadDenyList(file, maxAgeHours, nowMs) {
|
|
1217
|
+
let raw;
|
|
1218
|
+
try {
|
|
1219
|
+
raw = await readFile2(file, "utf8");
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
const code = error.code ?? "read error";
|
|
1222
|
+
return { ok: false, reason: `cannot read deny list ${file} (${code})` };
|
|
1223
|
+
}
|
|
1224
|
+
let parsed;
|
|
1225
|
+
try {
|
|
1226
|
+
parsed = JSON.parse(stripBom(raw));
|
|
1227
|
+
} catch {
|
|
1228
|
+
return { ok: false, reason: `deny list ${file} is not valid JSON` };
|
|
1229
|
+
}
|
|
1230
|
+
let addresses;
|
|
1231
|
+
let builtAt;
|
|
1232
|
+
if (Array.isArray(parsed)) {
|
|
1233
|
+
addresses = parsed;
|
|
1234
|
+
} else if (isRecord(parsed)) {
|
|
1235
|
+
addresses = parsed["addresses"];
|
|
1236
|
+
builtAt = parsed["built_at"];
|
|
1237
|
+
}
|
|
1238
|
+
if (!isStringList(addresses, true)) {
|
|
1239
|
+
return {
|
|
1240
|
+
ok: false,
|
|
1241
|
+
reason: `deny list ${file} must be an array of addresses or an object with an "addresses" array`
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1244
|
+
let stampMs;
|
|
1245
|
+
if (builtAt !== void 0) {
|
|
1246
|
+
stampMs = typeof builtAt === "string" ? Date.parse(builtAt) : Number.NaN;
|
|
1247
|
+
if (Number.isNaN(stampMs)) {
|
|
1248
|
+
return { ok: false, reason: `deny list ${file} has an unparseable built_at` };
|
|
1249
|
+
}
|
|
1250
|
+
} else {
|
|
1251
|
+
try {
|
|
1252
|
+
stampMs = (await stat(file)).mtimeMs;
|
|
1253
|
+
} catch {
|
|
1254
|
+
return { ok: false, reason: `cannot stat deny list ${file}` };
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
const ageHours = (nowMs - stampMs) / 36e5;
|
|
1258
|
+
if (ageHours > maxAgeHours) {
|
|
1259
|
+
return {
|
|
1260
|
+
ok: false,
|
|
1261
|
+
reason: `deny list ${file} is ${ageHours.toFixed(1)}h old (maxAgeHours ${maxAgeHours}); rebuild it`
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
return { ok: true, value: new Set(addresses.map((a) => a.trim().toLowerCase())) };
|
|
1265
|
+
}
|
|
1266
|
+
function sameFolder(a, b) {
|
|
1267
|
+
if (a === b) return true;
|
|
1268
|
+
return isWellKnownFolder(a) && isWellKnownFolder(b) && a.toLowerCase() === b.toLowerCase();
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/policyGuard.ts
|
|
1272
|
+
function policyPath(env) {
|
|
1273
|
+
const value = env?.[POLICY_ENV]?.trim();
|
|
1274
|
+
return value ? value : void 0;
|
|
1275
|
+
}
|
|
1276
|
+
function normalizeCommand(name) {
|
|
1277
|
+
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
1278
|
+
}
|
|
1279
|
+
function commandAllowed(policy, command, sub) {
|
|
1280
|
+
const allowed = new Set(policy.allowCommands.map(normalizeCommand));
|
|
1281
|
+
const base = normalizeCommand(command);
|
|
1282
|
+
if (allowed.has(base)) return true;
|
|
1283
|
+
return sub !== void 0 && allowed.has(normalizeCommand(`${base} ${sub}`));
|
|
1284
|
+
}
|
|
1285
|
+
function rawPath(url) {
|
|
1286
|
+
return url.replace(/^https?:\/\/[^/]*/i, "").split(/[?#]/)[0] ?? "";
|
|
1287
|
+
}
|
|
1288
|
+
function unsafePathReason(url) {
|
|
1289
|
+
for (const segment of rawPath(url).split("/")) {
|
|
1290
|
+
let decoded;
|
|
1291
|
+
try {
|
|
1292
|
+
decoded = decodeURIComponent(segment);
|
|
1293
|
+
} catch {
|
|
1294
|
+
return "request path contains a malformed percent-encoding";
|
|
1295
|
+
}
|
|
1296
|
+
if (decoded === "." || decoded === ".." || decoded.includes("\\") || decoded.includes("/")) {
|
|
1297
|
+
return "request path contains a dot segment, backslash or encoded slash";
|
|
1298
|
+
}
|
|
1299
|
+
if (decoded.includes("%")) {
|
|
1300
|
+
return "request path contains a double-encoded percent sequence";
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return void 0;
|
|
1304
|
+
}
|
|
1305
|
+
function purgeTarget(spec) {
|
|
1306
|
+
if (spec.method.toUpperCase() !== "DELETE") return void 0;
|
|
1307
|
+
const path = rawPath(spec.url);
|
|
1308
|
+
const match = /\/me\/messages\/([^/]+)$/.exec(path);
|
|
1309
|
+
if (!match) return void 0;
|
|
1310
|
+
return { destination: "deleteditems", messageId: match[1] };
|
|
1311
|
+
}
|
|
1312
|
+
function moveTarget(spec) {
|
|
1313
|
+
if (spec.method.toUpperCase() !== "POST") return void 0;
|
|
1314
|
+
const path = rawPath(spec.url);
|
|
1315
|
+
if (!path.endsWith("/move")) return void 0;
|
|
1316
|
+
const body = spec.body;
|
|
1317
|
+
const destination = typeof body?.destinationId === "string" ? body.destinationId : "";
|
|
1318
|
+
const match = /\/me\/messages\/([^/]+)\/move$/.exec(path);
|
|
1319
|
+
return { destination, messageId: match?.[1] };
|
|
1320
|
+
}
|
|
1321
|
+
async function evaluatePolicy(input) {
|
|
1322
|
+
const path = policyPath(input.env);
|
|
1323
|
+
if (path === void 0) return void 0;
|
|
1324
|
+
const refuse = (rule, reason) => ({
|
|
1325
|
+
policyPath: path,
|
|
1326
|
+
allowed: false,
|
|
1327
|
+
rule,
|
|
1328
|
+
reason
|
|
1329
|
+
});
|
|
1330
|
+
const loaded = await loadPolicy(path);
|
|
1331
|
+
if (!loaded.ok) return refuse("policy-file", loaded.reason);
|
|
1332
|
+
const policy = loaded.value;
|
|
1333
|
+
if (input.command === void 0) {
|
|
1334
|
+
return refuse("allowCommands", "cannot determine which command issued this request");
|
|
1335
|
+
}
|
|
1336
|
+
if (!commandAllowed(policy, input.command, input.subcommand)) {
|
|
1337
|
+
const label = input.subcommand ? `${input.command} ${input.subcommand}` : input.command;
|
|
1338
|
+
return refuse("allowCommands", `'${label}' is not in allowCommands`);
|
|
1339
|
+
}
|
|
1340
|
+
const pathProblem = unsafePathReason(input.spec.url);
|
|
1341
|
+
if (pathProblem !== void 0) return refuse("request-path", pathProblem);
|
|
1342
|
+
const move = moveTarget(input.spec) ?? purgeTarget(input.spec);
|
|
1343
|
+
if (move === void 0) return { policyPath: path, allowed: true };
|
|
1344
|
+
const { destination, messageId } = move;
|
|
1345
|
+
if (destination === "") return refuse("purge", "purge request could not determine destination");
|
|
1346
|
+
const allowedDestinations = policy.move?.allowedDestinations;
|
|
1347
|
+
if (allowedDestinations !== void 0 && !allowedDestinations.some((d) => sameFolder(d, destination))) {
|
|
1348
|
+
return refuse(
|
|
1349
|
+
"move.allowedDestinations",
|
|
1350
|
+
`destination '${destination}' is not in move.allowedDestinations`
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
const deny = policy.move?.denySenders;
|
|
1354
|
+
if (deny === void 0 || !deny.destinations.some((d) => sameFolder(d, destination))) {
|
|
1355
|
+
return { policyPath: path, allowed: true };
|
|
1356
|
+
}
|
|
1357
|
+
const now = input.now ?? Date.now;
|
|
1358
|
+
const listFile = resolve(dirname2(path), deny.file);
|
|
1359
|
+
const list = await loadDenyList(listFile, deny.maxAgeHours, now());
|
|
1360
|
+
if (!list.ok) return refuse("move.denySenders", `${list.reason}; moves to '${destination}' are refused`);
|
|
1361
|
+
if (messageId === void 0) {
|
|
1362
|
+
return refuse("move.denySenders", "cannot identify the message, so its sender cannot be checked");
|
|
1363
|
+
}
|
|
1364
|
+
if (input.dryRun) return { policyPath: path, allowed: true, senderCheck: "skipped-dry-run" };
|
|
1365
|
+
const sender = (await input.lookupSender(messageId))?.trim().toLowerCase();
|
|
1366
|
+
if (!sender) return refuse("move.denySenders", "the message has no sender address to check");
|
|
1367
|
+
if (list.value.has(sender)) {
|
|
1368
|
+
return refuse("move.denySenders", `sender ${sender} is on the deny list for '${destination}'`);
|
|
1369
|
+
}
|
|
1370
|
+
return { policyPath: path, allowed: true, senderCheck: "passed" };
|
|
1371
|
+
}
|
|
1372
|
+
async function verdictFor(ctx, spec) {
|
|
1373
|
+
return evaluatePolicy({
|
|
1374
|
+
env: ctx.env,
|
|
1375
|
+
command: ctx.command,
|
|
1376
|
+
subcommand: ctx.args.positionals[0],
|
|
1377
|
+
spec,
|
|
1378
|
+
dryRun: ctx.dryRun,
|
|
1379
|
+
lookupSender: async (messageId) => {
|
|
1380
|
+
const message = await ctx.client.get(
|
|
1381
|
+
`/me/messages/${messageId}`,
|
|
1382
|
+
{ $select: "from" }
|
|
1383
|
+
);
|
|
1384
|
+
return message?.from?.emailAddress?.address;
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
}
|
|
1388
|
+
async function enforcePolicy(ctx, spec) {
|
|
1389
|
+
const verdict = await verdictFor(ctx, spec);
|
|
1390
|
+
if (verdict === void 0 || verdict.allowed) return;
|
|
1391
|
+
const rule = verdict.rule ?? "policy";
|
|
1392
|
+
const reason = verdict.reason ?? "refused";
|
|
1393
|
+
if (ctx.json) {
|
|
1394
|
+
ctx.out(
|
|
1395
|
+
JSON.stringify(
|
|
1396
|
+
{ error: { code: "PolicyRefused", rule, message: reason, policy: verdict.policyPath } },
|
|
1397
|
+
null,
|
|
1398
|
+
2
|
|
1399
|
+
)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
throw new PolicyRefusedError(rule, reason);
|
|
1403
|
+
}
|
|
1404
|
+
async function dryRunVerdict(ctx, spec) {
|
|
1405
|
+
return verdictFor({ ...ctx, dryRun: true }, spec);
|
|
1406
|
+
}
|
|
1407
|
+
function renderVerdict(verdict) {
|
|
1408
|
+
const head = `Policy (${verdict.policyPath}):`;
|
|
1409
|
+
if (!verdict.allowed) return `${head} would REFUSE (${verdict.rule}): ${verdict.reason}`;
|
|
1410
|
+
if (verdict.senderCheck === "skipped-dry-run") {
|
|
1411
|
+
return `${head} would ALLOW unless the message's sender is on the deny list (the sender lookup is a network call, so --dry-run skipped it).`;
|
|
1412
|
+
}
|
|
1413
|
+
return `${head} would ALLOW this request.`;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// src/command.ts
|
|
1417
|
+
async function guardMutation(ctx, spec, exec) {
|
|
1418
|
+
if (ctx.dryRun) {
|
|
1419
|
+
const verdict = await dryRunVerdict(ctx, spec);
|
|
1420
|
+
if (ctx.json) {
|
|
1421
|
+
ctx.out(
|
|
1422
|
+
JSON.stringify(
|
|
1423
|
+
{
|
|
1424
|
+
dryRun: true,
|
|
1425
|
+
request: redactSpecForJson(spec),
|
|
1426
|
+
...verdict === void 0 ? {} : { policy: verdictJson(verdict) }
|
|
1427
|
+
},
|
|
1428
|
+
null,
|
|
1429
|
+
2
|
|
1430
|
+
)
|
|
1431
|
+
);
|
|
1432
|
+
} else {
|
|
1433
|
+
ctx.out("DRY RUN \u2014 nothing was sent. The request below is what would go out:");
|
|
1434
|
+
ctx.out("");
|
|
1435
|
+
ctx.out(renderRequest(spec));
|
|
1436
|
+
if (verdict !== void 0) {
|
|
1437
|
+
ctx.out("");
|
|
1438
|
+
ctx.out(renderVerdict(verdict));
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
return void 0;
|
|
1442
|
+
}
|
|
1443
|
+
await assertWriteScope(ctx, spec);
|
|
1444
|
+
await enforcePolicy(ctx, spec);
|
|
1445
|
+
return exec();
|
|
1446
|
+
}
|
|
1447
|
+
function verdictJson(verdict) {
|
|
1448
|
+
return {
|
|
1449
|
+
path: verdict.policyPath,
|
|
1450
|
+
allowed: verdict.allowed,
|
|
1451
|
+
...verdict.rule === void 0 ? {} : { rule: verdict.rule },
|
|
1452
|
+
...verdict.reason === void 0 ? {} : { reason: verdict.reason },
|
|
1453
|
+
...verdict.senderCheck === void 0 ? {} : { senderCheck: verdict.senderCheck }
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
async function assertWriteScope(ctx, spec) {
|
|
1457
|
+
const status = await ctx.auth.status();
|
|
1458
|
+
if (!status.signedIn || status.mode !== "readonly") return;
|
|
1459
|
+
throw new InsufficientScopeError(`${spec.method} ${spec.url}`);
|
|
1460
|
+
}
|
|
1461
|
+
function redactSpecForJson(spec) {
|
|
1462
|
+
const { method, url, body } = spec;
|
|
1463
|
+
return {
|
|
1464
|
+
method,
|
|
1465
|
+
url,
|
|
1466
|
+
...body === void 0 ? {} : { body: redactObject(body) }
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function emit(ctx, data, human) {
|
|
1470
|
+
ctx.out(ctx.json ? JSON.stringify(data, null, 2) : human());
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// src/mime.ts
|
|
1474
|
+
import { readFile as readFile3, stat as stat2 } from "fs/promises";
|
|
1475
|
+
import { basename, extname } from "path";
|
|
1476
|
+
var MAX_INLINE_ATTACHMENT_BYTES = 3 * 1024 * 1024;
|
|
1477
|
+
var CONTENT_TYPES = {
|
|
1478
|
+
".pdf": "application/pdf",
|
|
1479
|
+
".png": "image/png",
|
|
1480
|
+
".jpg": "image/jpeg",
|
|
1481
|
+
".jpeg": "image/jpeg",
|
|
1482
|
+
".gif": "image/gif",
|
|
1483
|
+
".txt": "text/plain",
|
|
1484
|
+
".md": "text/markdown",
|
|
1485
|
+
".csv": "text/csv",
|
|
1486
|
+
".json": "application/json",
|
|
1487
|
+
".zip": "application/zip",
|
|
1488
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1489
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1490
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1491
|
+
".html": "text/html",
|
|
1492
|
+
".htm": "text/html"
|
|
1493
|
+
};
|
|
1494
|
+
var DEFAULT_CONTENT_TYPE = "application/octet-stream";
|
|
1495
|
+
function guessContentType(filename) {
|
|
1496
|
+
const ext = extname(filename).toLowerCase();
|
|
1497
|
+
return CONTENT_TYPES[ext] ?? DEFAULT_CONTENT_TYPE;
|
|
1498
|
+
}
|
|
1499
|
+
function formatBytes(n) {
|
|
1500
|
+
const mb = n / (1024 * 1024);
|
|
1501
|
+
return `${mb.toFixed(2)} MB (${n} bytes)`;
|
|
1502
|
+
}
|
|
1503
|
+
async function readFileAttachment(path) {
|
|
1504
|
+
const stats = await stat2(path);
|
|
1505
|
+
if (stats.size > MAX_INLINE_ATTACHMENT_BYTES) {
|
|
1506
|
+
throw new OutlookCliError(
|
|
1507
|
+
`Attachment "${path}" is ${formatBytes(stats.size)}, which exceeds the 3 MB inline-attachment limit. Files between 3 MB and 150 MB require a Graph upload session (createUploadSession), which this CLI does not yet support. Choose a smaller file or split the content.`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
const buf = await readFile3(path);
|
|
1511
|
+
const name = basename(path);
|
|
1512
|
+
return {
|
|
1513
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
1514
|
+
name,
|
|
1515
|
+
contentType: guessContentType(name),
|
|
1516
|
+
contentBytes: buf.toString("base64")
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
var HTML_ENTITIES = {
|
|
1520
|
+
"&": "&",
|
|
1521
|
+
"<": "<",
|
|
1522
|
+
">": ">",
|
|
1523
|
+
""": '"',
|
|
1524
|
+
"'": "'",
|
|
1525
|
+
" ": " "
|
|
1526
|
+
};
|
|
1527
|
+
var ENTITY_RE = /&(?:amp|lt|gt|quot|#39|nbsp);/g;
|
|
1528
|
+
function htmlToText(html) {
|
|
1529
|
+
if (!html) return "";
|
|
1530
|
+
let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
|
|
1531
|
+
text = text.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p\s*>/gi, "\n\n").replace(/<\/(div|tr|li|h[1-6])\s*>/gi, "\n").replace(/<[^>]+>/g, "");
|
|
1532
|
+
text = text.replace(ENTITY_RE, (m) => HTML_ENTITIES[m] ?? m);
|
|
1533
|
+
text = text.split("\n").map((line) => line.replace(/[ \t]+$/, "")).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
1534
|
+
return text;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// src/output.ts
|
|
1538
|
+
var DATE_WIDTH = 20;
|
|
1539
|
+
var FROM_WIDTH = 24;
|
|
1540
|
+
var SUBJECT_WIDTH = 40;
|
|
1541
|
+
function recipientLabel(recipient) {
|
|
1542
|
+
const address = recipient?.emailAddress?.address;
|
|
1543
|
+
if (!address) return void 0;
|
|
1544
|
+
const name = recipient.emailAddress.name;
|
|
1545
|
+
return name ? `${name} <${address}>` : address;
|
|
1546
|
+
}
|
|
1547
|
+
function formatRecipients(recipients) {
|
|
1548
|
+
if (!recipients || recipients.length === 0) return "(none)";
|
|
1549
|
+
return recipients.map((r) => recipientLabel(r) ?? "(unknown)").join(", ");
|
|
1550
|
+
}
|
|
1551
|
+
function pad2(n) {
|
|
1552
|
+
return n.toString().padStart(2, "0");
|
|
1553
|
+
}
|
|
1554
|
+
function formatDate(iso) {
|
|
1555
|
+
if (!iso) return "(no date)";
|
|
1556
|
+
const d = new Date(iso);
|
|
1557
|
+
if (Number.isNaN(d.getTime())) return "(no date)";
|
|
1558
|
+
const y = d.getUTCFullYear();
|
|
1559
|
+
const mo = pad2(d.getUTCMonth() + 1);
|
|
1560
|
+
const day = pad2(d.getUTCDate());
|
|
1561
|
+
const h = pad2(d.getUTCHours());
|
|
1562
|
+
const mi = pad2(d.getUTCMinutes());
|
|
1563
|
+
return `${y}-${mo}-${day} ${h}:${mi} UTC`;
|
|
1564
|
+
}
|
|
1565
|
+
function truncate(text, max) {
|
|
1566
|
+
if (max <= 0) return "";
|
|
1567
|
+
if (text.length <= max) return text;
|
|
1568
|
+
if (max === 1) return text.slice(0, 1);
|
|
1569
|
+
return `${text.slice(0, max - 1)}\u2026`;
|
|
1570
|
+
}
|
|
1571
|
+
function subjectOf(message) {
|
|
1572
|
+
return message.subject && message.subject.trim().length > 0 ? message.subject : "(no subject)";
|
|
1573
|
+
}
|
|
1574
|
+
function fromOf(message) {
|
|
1575
|
+
return recipientLabel(message.from ?? message.sender) ?? "(unknown sender)";
|
|
1576
|
+
}
|
|
1577
|
+
function renderMessageList(messages) {
|
|
1578
|
+
if (messages.length === 0) return "(no messages)";
|
|
1579
|
+
const header = [
|
|
1580
|
+
" ".padEnd(1),
|
|
1581
|
+
"DATE".padEnd(DATE_WIDTH),
|
|
1582
|
+
"FROM".padEnd(FROM_WIDTH),
|
|
1583
|
+
"SUBJECT".padEnd(SUBJECT_WIDTH),
|
|
1584
|
+
" ".padEnd(1),
|
|
1585
|
+
"ID"
|
|
1586
|
+
].join(" ");
|
|
1587
|
+
const rows = messages.map((m) => {
|
|
1588
|
+
const unread = m.isRead === false ? "*" : " ";
|
|
1589
|
+
const attach = m.hasAttachments ? "@" : " ";
|
|
1590
|
+
const date = formatDate(m.receivedDateTime ?? m.sentDateTime);
|
|
1591
|
+
const from = truncate(fromOf(m), FROM_WIDTH);
|
|
1592
|
+
const subject = truncate(subjectOf(m), SUBJECT_WIDTH);
|
|
1593
|
+
return [
|
|
1594
|
+
unread,
|
|
1595
|
+
date.padEnd(DATE_WIDTH),
|
|
1596
|
+
from.padEnd(FROM_WIDTH),
|
|
1597
|
+
subject.padEnd(SUBJECT_WIDTH),
|
|
1598
|
+
attach,
|
|
1599
|
+
m.id
|
|
1600
|
+
].join(" ");
|
|
1601
|
+
});
|
|
1602
|
+
return [header, ...rows].join("\n");
|
|
1603
|
+
}
|
|
1604
|
+
function readState(message) {
|
|
1605
|
+
if (message.isRead === false) return "no";
|
|
1606
|
+
if (message.isRead === true) return "yes";
|
|
1607
|
+
return "unknown";
|
|
1608
|
+
}
|
|
1609
|
+
function renderBody(message, opts) {
|
|
1610
|
+
const body = message.body;
|
|
1611
|
+
if (!body || !body.content) {
|
|
1612
|
+
return message.bodyPreview && message.bodyPreview.trim().length > 0 ? message.bodyPreview : "(no body)";
|
|
1613
|
+
}
|
|
1614
|
+
if (body.contentType === "html") {
|
|
1615
|
+
return opts?.html ? body.content : htmlToText(body.content);
|
|
1616
|
+
}
|
|
1617
|
+
return body.content;
|
|
1618
|
+
}
|
|
1619
|
+
function renderMessage(message, opts) {
|
|
1620
|
+
const lines = [
|
|
1621
|
+
`Subject: ${subjectOf(message)}`,
|
|
1622
|
+
`From: ${fromOf(message)}`,
|
|
1623
|
+
`To: ${formatRecipients(message.toRecipients)}`
|
|
1624
|
+
];
|
|
1625
|
+
if (message.ccRecipients && message.ccRecipients.length > 0) {
|
|
1626
|
+
lines.push(`Cc: ${formatRecipients(message.ccRecipients)}`);
|
|
1627
|
+
}
|
|
1628
|
+
lines.push(
|
|
1629
|
+
`Date: ${formatDate(message.receivedDateTime ?? message.sentDateTime)}`,
|
|
1630
|
+
`Importance: ${message.importance ?? "normal"}`,
|
|
1631
|
+
`Read: ${readState(message)}`,
|
|
1632
|
+
`Attachments: ${message.hasAttachments ? "yes" : "no"}`,
|
|
1633
|
+
`Id: ${message.id}`,
|
|
1634
|
+
"",
|
|
1635
|
+
renderBody(message, opts)
|
|
1636
|
+
);
|
|
1637
|
+
return lines.join("\n");
|
|
1638
|
+
}
|
|
1639
|
+
function renderFolders(folders) {
|
|
1640
|
+
if (folders.length === 0) return "(no folders)";
|
|
1641
|
+
return folders.map((f) => {
|
|
1642
|
+
const name = f.displayName && f.displayName.trim() ? f.displayName : "(unnamed folder)";
|
|
1643
|
+
const counts = [];
|
|
1644
|
+
if (f.unreadItemCount !== void 0) counts.push(`${f.unreadItemCount} unread`);
|
|
1645
|
+
if (f.totalItemCount !== void 0) counts.push(`${f.totalItemCount} total`);
|
|
1646
|
+
const suffix = counts.length > 0 ? ` (${counts.join(", ")})` : "";
|
|
1647
|
+
return `${name}${suffix} \u2014 ${f.id}`;
|
|
1648
|
+
}).join("\n");
|
|
1649
|
+
}
|
|
1650
|
+
function attachmentKind(odataType) {
|
|
1651
|
+
if (!odataType) return "unknown";
|
|
1652
|
+
const normalized = odataType.replace(/^#/, "");
|
|
1653
|
+
switch (normalized) {
|
|
1654
|
+
case "microsoft.graph.fileAttachment":
|
|
1655
|
+
return "file";
|
|
1656
|
+
case "microsoft.graph.itemAttachment":
|
|
1657
|
+
return "item";
|
|
1658
|
+
case "microsoft.graph.referenceAttachment":
|
|
1659
|
+
return "reference";
|
|
1660
|
+
default:
|
|
1661
|
+
return normalized;
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
function formatSize(bytes) {
|
|
1665
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1666
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1667
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1668
|
+
}
|
|
1669
|
+
function renderAttachments(attachments) {
|
|
1670
|
+
if (attachments.length === 0) return "(no attachments)";
|
|
1671
|
+
return attachments.map((a) => {
|
|
1672
|
+
const name = a.name && a.name.trim() ? a.name : "(unnamed attachment)";
|
|
1673
|
+
const kind = attachmentKind(a["@odata.type"]);
|
|
1674
|
+
const size = a.size !== void 0 ? formatSize(a.size) : "(unknown size)";
|
|
1675
|
+
const inline = a.isInline ? " [inline]" : "";
|
|
1676
|
+
return `${name} \u2014 ${kind}, ${size}${inline} \u2014 ${a.id}`;
|
|
1677
|
+
}).join("\n");
|
|
1678
|
+
}
|
|
1679
|
+
function renderProfile(profile) {
|
|
1680
|
+
const lines = [
|
|
1681
|
+
`Name: ${profile.displayName ?? "(unknown)"}`,
|
|
1682
|
+
`Mail: ${profile.mail ?? profile.userPrincipalName ?? "(none)"}`
|
|
1683
|
+
];
|
|
1684
|
+
if (profile.jobTitle) lines.push(`Job title: ${profile.jobTitle}`);
|
|
1685
|
+
lines.push(`Id: ${profile.id}`);
|
|
1686
|
+
return lines.join("\n");
|
|
1687
|
+
}
|
|
1688
|
+
var NAME_WIDTH = 24;
|
|
1689
|
+
var PHONE_WIDTH = 18;
|
|
1690
|
+
var EMAIL_WIDTH = 28;
|
|
1691
|
+
var COMPANY_WIDTH = 20;
|
|
1692
|
+
function primaryPhone(contact) {
|
|
1693
|
+
if (contact.mobilePhone) return contact.mobilePhone;
|
|
1694
|
+
if (contact.businessPhones && contact.businessPhones.length > 0) return contact.businessPhones[0];
|
|
1695
|
+
if (contact.homePhones && contact.homePhones.length > 0) return contact.homePhones[0];
|
|
1696
|
+
return "";
|
|
1697
|
+
}
|
|
1698
|
+
function primaryEmail(contact) {
|
|
1699
|
+
if (contact.emailAddresses && contact.emailAddresses.length > 0) {
|
|
1700
|
+
return contact.emailAddresses[0].address ?? "";
|
|
1701
|
+
}
|
|
1702
|
+
return "";
|
|
1703
|
+
}
|
|
1704
|
+
function renderContactList(contacts) {
|
|
1705
|
+
if (contacts.length === 0) return "(no contacts)";
|
|
1706
|
+
const header = [
|
|
1707
|
+
"NAME".padEnd(NAME_WIDTH),
|
|
1708
|
+
"PHONE".padEnd(PHONE_WIDTH),
|
|
1709
|
+
"EMAIL".padEnd(EMAIL_WIDTH),
|
|
1710
|
+
"COMPANY".padEnd(COMPANY_WIDTH),
|
|
1711
|
+
"ID"
|
|
1712
|
+
].join(" ");
|
|
1713
|
+
const rows = contacts.map((c) => {
|
|
1714
|
+
const name = truncate(c.displayName ?? "(unnamed)", NAME_WIDTH);
|
|
1715
|
+
const phone = truncate(primaryPhone(c), PHONE_WIDTH);
|
|
1716
|
+
const email = truncate(primaryEmail(c), EMAIL_WIDTH);
|
|
1717
|
+
const company = truncate(c.companyName ?? "", COMPANY_WIDTH);
|
|
1718
|
+
return [
|
|
1719
|
+
name.padEnd(NAME_WIDTH),
|
|
1720
|
+
phone.padEnd(PHONE_WIDTH),
|
|
1721
|
+
email.padEnd(EMAIL_WIDTH),
|
|
1722
|
+
company.padEnd(COMPANY_WIDTH),
|
|
1723
|
+
c.id
|
|
1724
|
+
].join(" ");
|
|
1725
|
+
});
|
|
1726
|
+
return [header, ...rows].join("\n");
|
|
1727
|
+
}
|
|
1728
|
+
function allPhones(contact) {
|
|
1729
|
+
const parts = [];
|
|
1730
|
+
if (contact.mobilePhone) parts.push(`${contact.mobilePhone} (mobile)`);
|
|
1731
|
+
if (contact.businessPhones) {
|
|
1732
|
+
for (const p of contact.businessPhones) parts.push(`${p} (business)`);
|
|
1733
|
+
}
|
|
1734
|
+
if (contact.homePhones) {
|
|
1735
|
+
for (const p of contact.homePhones) parts.push(`${p} (home)`);
|
|
1736
|
+
}
|
|
1737
|
+
return parts.length > 0 ? parts.join(", ") : "(none)";
|
|
1738
|
+
}
|
|
1739
|
+
function allEmails(contact) {
|
|
1740
|
+
if (!contact.emailAddresses || contact.emailAddresses.length === 0) return "(none)";
|
|
1741
|
+
return contact.emailAddresses.map((e) => {
|
|
1742
|
+
const addr = e.address ?? "(unknown)";
|
|
1743
|
+
return e.name ? `${e.name} <${addr}>` : addr;
|
|
1744
|
+
}).join(", ");
|
|
1745
|
+
}
|
|
1746
|
+
function renderContact(contact) {
|
|
1747
|
+
const lines = [
|
|
1748
|
+
`Name: ${contact.displayName ?? "(unnamed)"}`,
|
|
1749
|
+
`Email: ${allEmails(contact)}`,
|
|
1750
|
+
`Phone: ${allPhones(contact)}`
|
|
1751
|
+
];
|
|
1752
|
+
if (contact.companyName) lines.push(`Company: ${contact.companyName}`);
|
|
1753
|
+
if (contact.jobTitle) lines.push(`Job title: ${contact.jobTitle}`);
|
|
1754
|
+
if (contact.department) lines.push(`Department: ${contact.department}`);
|
|
1755
|
+
if (contact.personalNotes) lines.push(`Notes: ${contact.personalNotes}`);
|
|
1756
|
+
lines.push(`Id: ${contact.id}`);
|
|
1757
|
+
return lines.join("\n");
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// src/paginate.ts
|
|
1761
|
+
async function* pageItems(client, path, query) {
|
|
1762
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1763
|
+
let url = path;
|
|
1764
|
+
let urlQuery = query;
|
|
1765
|
+
while (url !== void 0) {
|
|
1766
|
+
if (seen.has(url)) {
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
seen.add(url);
|
|
1770
|
+
const spec = client.buildSpec("GET", url, void 0, urlQuery);
|
|
1771
|
+
const page = await client.request(spec);
|
|
1772
|
+
for (const item of page.value) {
|
|
1773
|
+
yield item;
|
|
1774
|
+
}
|
|
1775
|
+
url = page["@odata.nextLink"];
|
|
1776
|
+
urlQuery = void 0;
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
async function collectItems(client, path, opts) {
|
|
1780
|
+
const { query, limit, all } = opts ?? {};
|
|
1781
|
+
if (limit === void 0 && !all) {
|
|
1782
|
+
const spec = client.buildSpec("GET", path, void 0, query);
|
|
1783
|
+
const page = await client.request(spec);
|
|
1784
|
+
return [...page.value];
|
|
1785
|
+
}
|
|
1786
|
+
const results = [];
|
|
1787
|
+
for await (const item of pageItems(client, path, query)) {
|
|
1788
|
+
results.push(item);
|
|
1789
|
+
if (limit !== void 0 && results.length >= limit) break;
|
|
1790
|
+
}
|
|
1791
|
+
return results;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/safeFilename.ts
|
|
1795
|
+
var ILLEGAL_CHARS_RE = /[<>:"|?*\u0000-\u001f]/g;
|
|
1796
|
+
var RESERVED_NAMES = /* @__PURE__ */ new Set([
|
|
1797
|
+
"con",
|
|
1798
|
+
"prn",
|
|
1799
|
+
"aux",
|
|
1800
|
+
"nul",
|
|
1801
|
+
...Array.from({ length: 9 }, (_, i) => `com${i + 1}`),
|
|
1802
|
+
...Array.from({ length: 9 }, (_, i) => `lpt${i + 1}`)
|
|
1803
|
+
]);
|
|
1804
|
+
var MAX_COMPONENT_LENGTH = 180;
|
|
1805
|
+
function stem(name) {
|
|
1806
|
+
const dot = name.indexOf(".");
|
|
1807
|
+
return dot === -1 ? name : name.slice(0, dot);
|
|
1808
|
+
}
|
|
1809
|
+
function sanitizeFilename(name, fallback) {
|
|
1810
|
+
let out = name.replace(ILLEGAL_CHARS_RE, "_");
|
|
1811
|
+
out = out.replace(/[. ]+$/, "");
|
|
1812
|
+
out = out.trim();
|
|
1813
|
+
if (out === "" || out === "." || out === "..") return fallback;
|
|
1814
|
+
if (RESERVED_NAMES.has(stem(out).toLowerCase())) {
|
|
1815
|
+
out = `_${out}`;
|
|
1816
|
+
}
|
|
1817
|
+
if (out.length > MAX_COMPONENT_LENGTH) {
|
|
1818
|
+
const lastDot = out.lastIndexOf(".");
|
|
1819
|
+
const ext = lastDot > 0 ? out.slice(lastDot, lastDot + 16) : "";
|
|
1820
|
+
out = out.slice(0, MAX_COMPONENT_LENGTH - ext.length) + ext;
|
|
1821
|
+
}
|
|
1822
|
+
out = out.replace(/[. ]+$/, "");
|
|
1823
|
+
return out === "" ? fallback : out;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// src/commands/attachments.ts
|
|
1827
|
+
function withoutContent(a) {
|
|
1828
|
+
const { contentBytes: _omitted, ...rest } = a;
|
|
1829
|
+
return rest;
|
|
1830
|
+
}
|
|
1831
|
+
var FILE_TYPE = "microsoft.graph.fileAttachment";
|
|
1832
|
+
function normalizedType(a) {
|
|
1833
|
+
return (a["@odata.type"] ?? "").replace(/^#/, "");
|
|
1834
|
+
}
|
|
1835
|
+
function isFileAttachment(a) {
|
|
1836
|
+
return normalizedType(a) === FILE_TYPE;
|
|
1837
|
+
}
|
|
1838
|
+
function kindLabel(a) {
|
|
1839
|
+
const t = normalizedType(a);
|
|
1840
|
+
return t.startsWith("microsoft.graph.") ? t.slice("microsoft.graph.".length) : t || "unknown";
|
|
1841
|
+
}
|
|
1842
|
+
async function fetchAttachments(ctx, messageId) {
|
|
1843
|
+
const path = `/me/messages/${encodeURIComponent(messageId)}/attachments`;
|
|
1844
|
+
return collectItems(ctx.client, path, { all: true });
|
|
1845
|
+
}
|
|
1846
|
+
async function listHandler(ctx, idIndex) {
|
|
1847
|
+
const messageId = ctx.args.requirePositional(idIndex, "<messageId>");
|
|
1848
|
+
const attachments = await fetchAttachments(ctx, messageId);
|
|
1849
|
+
emit(ctx, attachments.map(withoutContent), () => renderAttachments(attachments));
|
|
1850
|
+
}
|
|
1851
|
+
function safeDestination(outDir, attachment) {
|
|
1852
|
+
const fallback = `attachment-${attachment.id}`;
|
|
1853
|
+
const raw = attachment.name && attachment.name.trim() ? attachment.name : fallback;
|
|
1854
|
+
const base = basename2(raw.replace(/\\/g, "/"));
|
|
1855
|
+
const name = sanitizeFilename(base, fallback);
|
|
1856
|
+
const dest = resolve2(outDir, name);
|
|
1857
|
+
const rel = relative(outDir, dest);
|
|
1858
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
1859
|
+
throw new UsageError(`Refusing to write outside --out directory: "${attachment.name}"`);
|
|
1860
|
+
}
|
|
1861
|
+
return dest;
|
|
1862
|
+
}
|
|
1863
|
+
function allocateDestinations(outDir, attachments) {
|
|
1864
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
1865
|
+
return attachments.map((attachment) => claim(safeDestination(outDir, attachment), claimed));
|
|
1866
|
+
}
|
|
1867
|
+
function claim(dest, claimed) {
|
|
1868
|
+
const key = (p) => p.toLowerCase();
|
|
1869
|
+
if (!claimed.has(key(dest))) {
|
|
1870
|
+
claimed.add(key(dest));
|
|
1871
|
+
return dest;
|
|
1872
|
+
}
|
|
1873
|
+
const dir = dirname3(dest);
|
|
1874
|
+
const base = basename2(dest);
|
|
1875
|
+
const dot = base.lastIndexOf(".");
|
|
1876
|
+
const stem2 = dot > 0 ? base.slice(0, dot) : base;
|
|
1877
|
+
const ext = dot > 0 ? base.slice(dot) : "";
|
|
1878
|
+
for (let n = 2; ; n++) {
|
|
1879
|
+
const candidate = join4(dir, `${stem2} (${n})${ext}`);
|
|
1880
|
+
if (!claimed.has(key(candidate))) {
|
|
1881
|
+
claimed.add(key(candidate));
|
|
1882
|
+
return candidate;
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
async function exists(p) {
|
|
1887
|
+
try {
|
|
1888
|
+
await access(p);
|
|
1889
|
+
return true;
|
|
1890
|
+
} catch {
|
|
1891
|
+
return false;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
async function downloadOne(dest, attachment, force) {
|
|
1895
|
+
if (!isFileAttachment(attachment)) {
|
|
1896
|
+
throw new UsageError(
|
|
1897
|
+
`Cannot download "${attachment.name}" \u2014 ${kindLabel(attachment)} has no downloadable bytes (docs/01 \xA77.4)`
|
|
1898
|
+
);
|
|
1899
|
+
}
|
|
1900
|
+
if (!force && await exists(dest)) {
|
|
1901
|
+
throw new UsageError(`Refusing to overwrite existing file: ${dest} (pass --force)`);
|
|
1902
|
+
}
|
|
1903
|
+
const buf = Buffer.from(attachment.contentBytes ?? "", "base64");
|
|
1904
|
+
await writeFile2(dest, buf);
|
|
1905
|
+
return { id: attachment.id, name: attachment.name, path: dest, size: buf.length };
|
|
1906
|
+
}
|
|
1907
|
+
async function downloadHandler(ctx) {
|
|
1908
|
+
const messageId = ctx.args.requirePositional(1, "<messageId>");
|
|
1909
|
+
const attachmentId = ctx.args.positionals[2];
|
|
1910
|
+
const all = ctx.args.bool("all");
|
|
1911
|
+
const force = ctx.args.bool("force");
|
|
1912
|
+
const outDir = resolve2(ctx.args.str("out", process.cwd()) ?? process.cwd());
|
|
1913
|
+
if (!all && !attachmentId) {
|
|
1914
|
+
throw new UsageError("outlook attachments download requires [attachmentId] or --all");
|
|
1915
|
+
}
|
|
1916
|
+
const attachments = await fetchAttachments(ctx, messageId);
|
|
1917
|
+
const targets = all ? attachments : attachments.filter((a) => a.id === attachmentId);
|
|
1918
|
+
if (!all && targets.length === 0) {
|
|
1919
|
+
throw new UsageError(`No attachment with id "${attachmentId}" on message ${messageId}`);
|
|
1920
|
+
}
|
|
1921
|
+
const destinations = allocateDestinations(outDir, targets);
|
|
1922
|
+
if (ctx.dryRun) {
|
|
1923
|
+
const planned = targets.map((a, i) => ({
|
|
1924
|
+
id: a.id,
|
|
1925
|
+
name: a.name,
|
|
1926
|
+
path: destinations[i]
|
|
1927
|
+
}));
|
|
1928
|
+
emit(
|
|
1929
|
+
ctx,
|
|
1930
|
+
{ dryRun: true, wouldWrite: planned },
|
|
1931
|
+
() => [
|
|
1932
|
+
"DRY RUN \u2014 nothing was written. These files would be created:",
|
|
1933
|
+
"",
|
|
1934
|
+
...planned.length === 0 ? ["(nothing to download)"] : planned.map((p) => ` ${p.path}`)
|
|
1935
|
+
].join("\n")
|
|
1936
|
+
);
|
|
1937
|
+
return;
|
|
1938
|
+
}
|
|
1939
|
+
await mkdir2(outDir, { recursive: true });
|
|
1940
|
+
const results = [];
|
|
1941
|
+
for (const [i, attachment] of targets.entries()) {
|
|
1942
|
+
results.push(await downloadOne(destinations[i], attachment, force));
|
|
1943
|
+
}
|
|
1944
|
+
emit(
|
|
1945
|
+
ctx,
|
|
1946
|
+
results,
|
|
1947
|
+
() => results.length === 0 ? "(nothing to download)" : results.map((r) => `${r.name} -> ${r.path} (${r.size} bytes)`).join("\n")
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
async function attachmentsHandler(ctx) {
|
|
1951
|
+
const sub = ctx.args.positionals[0];
|
|
1952
|
+
if (sub === "download") return downloadHandler(ctx);
|
|
1953
|
+
if (sub === "list") return listHandler(ctx, 1);
|
|
1954
|
+
return listHandler(ctx, 0);
|
|
1955
|
+
}
|
|
1956
|
+
var attachmentsCommand = {
|
|
1957
|
+
name: "attachments",
|
|
1958
|
+
summary: "List or download the attachments on a message",
|
|
1959
|
+
usage: "outlook attachments [list|download] <messageId> [attachmentId] [--all] [--out dir] [--force] [--dry-run] [--json]",
|
|
1960
|
+
mutating: true,
|
|
1961
|
+
handler: attachmentsHandler
|
|
1962
|
+
};
|
|
1963
|
+
|
|
1964
|
+
// src/commands/auth.ts
|
|
1965
|
+
var SUBCOMMANDS = ["login", "status", "logout", "refresh"];
|
|
1966
|
+
function renderFacts(facts) {
|
|
1967
|
+
const lines = [`Signed in: ${facts.signedIn ? "yes" : "no"}`];
|
|
1968
|
+
if (facts.expired !== void 0) lines.push(`Expired: ${facts.expired ? "yes" : "no"}`);
|
|
1969
|
+
if (facts.account) lines.push(`Account: ${facts.account}`);
|
|
1970
|
+
if (facts.expiresAt !== void 0) lines.push(`Expires: ${new Date(facts.expiresAt).toISOString()}`);
|
|
1971
|
+
if (facts.scopes) lines.push(`Scopes: ${facts.scopes}`);
|
|
1972
|
+
if (facts.grantScopes) lines.push(`Requested scopes (--scopes): ${facts.grantScopes.join(" ")}`);
|
|
1973
|
+
if (facts.mode) {
|
|
1974
|
+
lines.push(
|
|
1975
|
+
`Access: ${facts.mode === "readonly" ? "read-only (cannot send or delete)" : "full read/write"}`
|
|
1976
|
+
);
|
|
1977
|
+
}
|
|
1978
|
+
return lines.join("\n");
|
|
1979
|
+
}
|
|
1980
|
+
function factsFromTokenSet(tokens) {
|
|
1981
|
+
return {
|
|
1982
|
+
signedIn: true,
|
|
1983
|
+
...tokens.account !== void 0 ? { account: tokens.account } : {},
|
|
1984
|
+
expiresAt: tokens.expiresAt,
|
|
1985
|
+
...tokens.scope !== void 0 ? { scopes: tokens.scope } : {},
|
|
1986
|
+
...tokens.mode !== void 0 ? { mode: tokens.mode } : {},
|
|
1987
|
+
...tokens.grantScopes !== void 0 ? { grantScopes: tokens.grantScopes } : {}
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
function printDevicePrompt(ctx, device) {
|
|
1991
|
+
ctx.out(
|
|
1992
|
+
device.message ?? `To sign in, open ${device.verification_uri} and enter the code: ${device.user_code}`
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
async function loginHandler(ctx) {
|
|
1996
|
+
const readOnly = ctx.args.bool("readonly");
|
|
1997
|
+
const scopes = scopesFlag(ctx);
|
|
1998
|
+
if (readOnly && scopes !== void 0) {
|
|
1999
|
+
throw new UsageError("--readonly and --scopes cannot be combined; list the read scopes you want in --scopes.");
|
|
2000
|
+
}
|
|
2001
|
+
const tokens = await ctx.auth.login(
|
|
2002
|
+
(device) => printDevicePrompt(ctx, device),
|
|
2003
|
+
{ readOnly, ...scopes !== void 0 ? { scopes } : {} }
|
|
2004
|
+
);
|
|
2005
|
+
if (!FILE_MODE_IS_ENFORCED) {
|
|
2006
|
+
ctx.err(
|
|
2007
|
+
"warning: on Windows the token cache cannot be restricted to your user account by file mode alone. Restrict the containing directory with an ACL, or sign in with --readonly to limit what a stolen token can do."
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
const facts = factsFromTokenSet(tokens);
|
|
2011
|
+
emit(ctx, facts, () => `Signed in.
|
|
2012
|
+
${renderFacts(facts)}`);
|
|
2013
|
+
}
|
|
2014
|
+
async function statusHandler(ctx) {
|
|
2015
|
+
const status = await ctx.auth.status();
|
|
2016
|
+
emit(ctx, status, () => renderFacts(status));
|
|
2017
|
+
}
|
|
2018
|
+
async function logoutHandler(ctx) {
|
|
2019
|
+
await ctx.auth.logout();
|
|
2020
|
+
emit(ctx, { signedIn: false }, () => "Signed out.");
|
|
2021
|
+
}
|
|
2022
|
+
async function refreshHandler(ctx) {
|
|
2023
|
+
const tokens = await ctx.auth.refresh();
|
|
2024
|
+
const facts = factsFromTokenSet(tokens);
|
|
2025
|
+
emit(ctx, facts, () => `Token refreshed.
|
|
2026
|
+
${renderFacts(facts)}`);
|
|
2027
|
+
}
|
|
2028
|
+
function scopesFlag(ctx) {
|
|
2029
|
+
if (!ctx.args.has("scopes")) return void 0;
|
|
2030
|
+
const raw = ctx.args.str("scopes");
|
|
2031
|
+
if (raw === void 0) {
|
|
2032
|
+
throw new UsageError("--scopes requires a comma-separated list, e.g. --scopes Mail.ReadWrite");
|
|
2033
|
+
}
|
|
2034
|
+
return parseScopeList(raw);
|
|
2035
|
+
}
|
|
2036
|
+
async function authHandler(ctx) {
|
|
2037
|
+
const subcommand = ctx.args.positionals[0] ?? "status";
|
|
2038
|
+
if (subcommand !== "login" && ctx.args.has("scopes")) {
|
|
2039
|
+
throw new UsageError(
|
|
2040
|
+
'--scopes applies only to "outlook auth login"; refresh reuses the scope set recorded at login.'
|
|
2041
|
+
);
|
|
2042
|
+
}
|
|
2043
|
+
switch (subcommand) {
|
|
2044
|
+
case "login":
|
|
2045
|
+
return loginHandler(ctx);
|
|
2046
|
+
case "status":
|
|
2047
|
+
return statusHandler(ctx);
|
|
2048
|
+
case "logout":
|
|
2049
|
+
return logoutHandler(ctx);
|
|
2050
|
+
case "refresh":
|
|
2051
|
+
return refreshHandler(ctx);
|
|
2052
|
+
default:
|
|
2053
|
+
throw new UsageError(
|
|
2054
|
+
`Unknown "outlook auth" subcommand: "${subcommand}". Valid subcommands: ${SUBCOMMANDS.join(", ")}.`
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
var authCommand = {
|
|
2059
|
+
name: "auth",
|
|
2060
|
+
summary: "Manage local sign-in: login, status, logout, refresh",
|
|
2061
|
+
usage: "outlook auth [login|status|logout|refresh] [--readonly | --scopes <list>] [--json]\n\n --readonly On login: request Mail.Read instead of Mail.ReadWrite and\n drop Mail.Send, so the stored token cannot send, delete, or\n modify mail. Recommended for unattended/agent use.\n --scopes On login: request exactly these comma-separated scopes\n (offline_access is added if missing). The set is stored\n with the token and reused by refresh. Pair with\n GRAPH_TOKEN_CACHE to keep a narrow token in its own file.",
|
|
2062
|
+
mutating: false,
|
|
2063
|
+
handler: authHandler
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/commands/contacts.ts
|
|
2067
|
+
var SUBCOMMANDS2 = ["list", "get", "search"];
|
|
2068
|
+
var DEFAULT_SELECT = "id,displayName,emailAddresses,mobilePhone,businessPhones,homePhones,companyName,jobTitle";
|
|
2069
|
+
var DEFAULT_ORDERBY = "displayName asc";
|
|
2070
|
+
var MAX_TOP = 1e3;
|
|
2071
|
+
function readTop(ctx) {
|
|
2072
|
+
if (!ctx.args.has("top")) return 25;
|
|
2073
|
+
const top = ctx.args.num("top");
|
|
2074
|
+
if (top === void 0 || !Number.isInteger(top) || top < 1 || top > MAX_TOP) {
|
|
2075
|
+
throw new UsageError(`--top must be an integer between 1 and ${MAX_TOP}`);
|
|
2076
|
+
}
|
|
2077
|
+
return top;
|
|
2078
|
+
}
|
|
2079
|
+
function readLimit(ctx, fallback) {
|
|
2080
|
+
if (!ctx.args.has("limit")) return fallback;
|
|
2081
|
+
const limit = ctx.args.num("limit");
|
|
2082
|
+
if (limit === void 0 || !Number.isInteger(limit) || limit < 1 || limit > MAX_TOP) {
|
|
2083
|
+
throw new UsageError(`--limit must be an integer between 1 and ${MAX_TOP}`);
|
|
2084
|
+
}
|
|
2085
|
+
return limit;
|
|
2086
|
+
}
|
|
2087
|
+
async function listHandler2(ctx) {
|
|
2088
|
+
const top = readTop(ctx);
|
|
2089
|
+
const all = ctx.args.bool("all");
|
|
2090
|
+
const limit = readLimit(ctx, top);
|
|
2091
|
+
const select = ctx.args.str("select") ?? DEFAULT_SELECT;
|
|
2092
|
+
const query = {
|
|
2093
|
+
$select: select,
|
|
2094
|
+
$orderby: DEFAULT_ORDERBY,
|
|
2095
|
+
$top: top
|
|
2096
|
+
};
|
|
2097
|
+
const filter = ctx.args.str("filter");
|
|
2098
|
+
if (filter) query.$filter = filter;
|
|
2099
|
+
const contacts = await collectItems(ctx.client, "/me/contacts", {
|
|
2100
|
+
query,
|
|
2101
|
+
limit: all ? void 0 : limit,
|
|
2102
|
+
all
|
|
2103
|
+
});
|
|
2104
|
+
emit(ctx, contacts, () => renderContactList(contacts));
|
|
2105
|
+
}
|
|
2106
|
+
async function getHandler(ctx) {
|
|
2107
|
+
const id = ctx.args.requirePositional(1, "<contactId>");
|
|
2108
|
+
const path = `/me/contacts/${encodeURIComponent(id)}`;
|
|
2109
|
+
const contact = await ctx.client.get(path);
|
|
2110
|
+
emit(ctx, contact, () => renderContact(contact));
|
|
2111
|
+
}
|
|
2112
|
+
async function searchHandler(ctx) {
|
|
2113
|
+
const query = ctx.args.requirePositional(1, "<query>");
|
|
2114
|
+
const top = readTop(ctx);
|
|
2115
|
+
const escaped = query.replace(/'/g, "''");
|
|
2116
|
+
const filterExpr = `startswith(displayName,'${escaped}')`;
|
|
2117
|
+
const contacts = await collectItems(ctx.client, "/me/contacts", {
|
|
2118
|
+
query: {
|
|
2119
|
+
$select: DEFAULT_SELECT,
|
|
2120
|
+
$filter: filterExpr,
|
|
2121
|
+
$top: top
|
|
2122
|
+
},
|
|
2123
|
+
limit: top
|
|
2124
|
+
});
|
|
2125
|
+
emit(ctx, contacts, () => renderContactList(contacts));
|
|
2126
|
+
}
|
|
2127
|
+
async function contactsHandler(ctx) {
|
|
2128
|
+
const subcommand = ctx.args.positionals[0] ?? "list";
|
|
2129
|
+
switch (subcommand) {
|
|
2130
|
+
case "list":
|
|
2131
|
+
return listHandler2(ctx);
|
|
2132
|
+
case "get":
|
|
2133
|
+
return getHandler(ctx);
|
|
2134
|
+
case "search":
|
|
2135
|
+
return searchHandler(ctx);
|
|
2136
|
+
default:
|
|
2137
|
+
throw new UsageError(
|
|
2138
|
+
`Unknown "outlook contacts" subcommand: "${subcommand}". Valid subcommands: ${SUBCOMMANDS2.join(", ")}.`
|
|
2139
|
+
);
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
var contactsCommand = {
|
|
2143
|
+
name: "contacts",
|
|
2144
|
+
summary: "List, get, or search contacts (read-only)",
|
|
2145
|
+
usage: "outlook contacts [list|get|search] [<id|query>] [--top <n>] [--all] [--limit <n>] [--select <fields>] [--filter <expr>] [--json]",
|
|
2146
|
+
mutating: false,
|
|
2147
|
+
handler: contactsHandler
|
|
2148
|
+
};
|
|
2149
|
+
|
|
2150
|
+
// src/signature.ts
|
|
2151
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
2152
|
+
var SIGNATURE_TEXT_ENV = "OUTLOOK_SIGNATURE_FILE";
|
|
2153
|
+
var SIGNATURE_HTML_ENV = "OUTLOOK_SIGNATURE_HTML_FILE";
|
|
2154
|
+
var TEXT_DELIMITER = "-- ";
|
|
2155
|
+
function trimmed2(value) {
|
|
2156
|
+
const t = value?.trim();
|
|
2157
|
+
return t ? t : void 0;
|
|
2158
|
+
}
|
|
2159
|
+
function isHtmlPath(path) {
|
|
2160
|
+
return /\.html?$/i.test(path);
|
|
2161
|
+
}
|
|
2162
|
+
async function readSignatureFile(path, origin) {
|
|
2163
|
+
try {
|
|
2164
|
+
const content = await readFile4(path, "utf8");
|
|
2165
|
+
if (content.trim() === "") {
|
|
2166
|
+
throw new UsageError(
|
|
2167
|
+
`Signature file ${path} (from ${origin}) is empty. Add content, or pass --no-signature to send without one.`
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
return content;
|
|
2171
|
+
} catch (e) {
|
|
2172
|
+
if (e instanceof UsageError) throw e;
|
|
2173
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
2174
|
+
throw new UsageError(
|
|
2175
|
+
`Could not read signature file ${path} (from ${origin}): ${reason}
|
|
2176
|
+
Fix the path, or pass --no-signature to send without one.`
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
async function resolveSignature(args, env = {}) {
|
|
2181
|
+
if (args.has("signature")) {
|
|
2182
|
+
const path = args.str("signature");
|
|
2183
|
+
if (path === void 0) {
|
|
2184
|
+
if (!args.bool("signature")) return void 0;
|
|
2185
|
+
throw new UsageError(
|
|
2186
|
+
"--signature needs a file path (e.g. --signature ~/sig.txt). Use --no-signature to send without one."
|
|
2187
|
+
);
|
|
2188
|
+
}
|
|
2189
|
+
if (path.trim() === "" || /^(false|0|no|off)$/i.test(path.trim())) {
|
|
2190
|
+
return void 0;
|
|
2191
|
+
}
|
|
2192
|
+
const content = await readSignatureFile(path, "--signature");
|
|
2193
|
+
return isHtmlPath(path) ? { html: content } : { text: content };
|
|
2194
|
+
}
|
|
2195
|
+
const textPath = trimmed2(env[SIGNATURE_TEXT_ENV]);
|
|
2196
|
+
const htmlPath = trimmed2(env[SIGNATURE_HTML_ENV]);
|
|
2197
|
+
if (textPath === void 0 && htmlPath === void 0) return void 0;
|
|
2198
|
+
const sig = {};
|
|
2199
|
+
if (textPath !== void 0) {
|
|
2200
|
+
sig.text = await readSignatureFile(textPath, SIGNATURE_TEXT_ENV);
|
|
2201
|
+
}
|
|
2202
|
+
if (htmlPath !== void 0) {
|
|
2203
|
+
sig.html = await readSignatureFile(htmlPath, SIGNATURE_HTML_ENV);
|
|
2204
|
+
}
|
|
2205
|
+
return sig;
|
|
2206
|
+
}
|
|
2207
|
+
function signatureFlagRequested(args) {
|
|
2208
|
+
if (!args.has("signature")) return false;
|
|
2209
|
+
const v = args.str("signature");
|
|
2210
|
+
if (v === void 0) return args.bool("signature");
|
|
2211
|
+
return !(v.trim() === "" || /^(false|0|no|off)$/i.test(v.trim()));
|
|
2212
|
+
}
|
|
2213
|
+
function escapeHtml(s) {
|
|
2214
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2215
|
+
}
|
|
2216
|
+
function appendTextSignature(content, sig) {
|
|
2217
|
+
if (sig.text === void 0) {
|
|
2218
|
+
throw new UsageError(
|
|
2219
|
+
`Only an HTML signature is configured, but this message body is plain text. Set ${SIGNATURE_TEXT_ENV}, pass --html (send/draft), or pass --no-signature.`
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
const head = content.trimEnd();
|
|
2223
|
+
const tail = `${TEXT_DELIMITER}
|
|
2224
|
+
${sig.text.trimEnd()}`;
|
|
2225
|
+
return head === "" ? tail : `${head}
|
|
2226
|
+
|
|
2227
|
+
${tail}`;
|
|
2228
|
+
}
|
|
2229
|
+
function appendHtmlSignature(content, sig) {
|
|
2230
|
+
const html = sig.html?.trim() ?? escapeHtml((sig.text ?? "").trimEnd()).replace(/\r?\n/g, "<br>");
|
|
2231
|
+
const block = `<br><div class="outlook-cli-signature">${html}</div>`;
|
|
2232
|
+
const matches = [...content.matchAll(/<\/body>/gi)];
|
|
2233
|
+
const last = matches[matches.length - 1];
|
|
2234
|
+
if (last?.index === void 0) return `${content}${block}`;
|
|
2235
|
+
return `${content.slice(0, last.index)}${block}${content.slice(last.index)}`;
|
|
2236
|
+
}
|
|
2237
|
+
function applySignature(body, sig) {
|
|
2238
|
+
const content = body.contentType.toLowerCase() === "html" ? appendHtmlSignature(body.content, sig) : appendTextSignature(body.content, sig);
|
|
2239
|
+
return { contentType: body.contentType, content };
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/commands/compose.ts
|
|
2243
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
2244
|
+
function parseRecipients(values, flag) {
|
|
2245
|
+
const out = [];
|
|
2246
|
+
for (const value of values) {
|
|
2247
|
+
for (const part of value.split(",")) {
|
|
2248
|
+
const address = part.trim();
|
|
2249
|
+
if (address.length === 0) continue;
|
|
2250
|
+
if (!address.includes("@")) {
|
|
2251
|
+
throw new UsageError(
|
|
2252
|
+
`--${flag} received "${address}", which is not an email address.`
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
out.push({ emailAddress: { address } });
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
return out;
|
|
2259
|
+
}
|
|
2260
|
+
function recipientsFromArgs(args, flag) {
|
|
2261
|
+
return parseRecipients(args.strAll(flag), flag);
|
|
2262
|
+
}
|
|
2263
|
+
async function buildBody(args) {
|
|
2264
|
+
const inline = args.str("body");
|
|
2265
|
+
const file = args.str("body-file");
|
|
2266
|
+
if (inline !== void 0 && file !== void 0) {
|
|
2267
|
+
throw new UsageError("Use either --body or --body-file, not both.");
|
|
2268
|
+
}
|
|
2269
|
+
const content = file !== void 0 ? await readFile5(file, "utf8") : inline;
|
|
2270
|
+
if (content === void 0) return void 0;
|
|
2271
|
+
const contentType = args.bool("html") ? "html" : "text";
|
|
2272
|
+
return { contentType, content };
|
|
2273
|
+
}
|
|
2274
|
+
async function loadAttachments(args) {
|
|
2275
|
+
const out = [];
|
|
2276
|
+
for (const path of args.strAll("attach")) {
|
|
2277
|
+
out.push(await readFileAttachment(path));
|
|
2278
|
+
}
|
|
2279
|
+
return out;
|
|
2280
|
+
}
|
|
2281
|
+
async function composeMessage(args) {
|
|
2282
|
+
const message = {};
|
|
2283
|
+
const subject = args.str("subject");
|
|
2284
|
+
if (subject !== void 0) message.subject = subject;
|
|
2285
|
+
const body = await buildBody(args);
|
|
2286
|
+
if (body !== void 0) message.body = body;
|
|
2287
|
+
const to = recipientsFromArgs(args, "to");
|
|
2288
|
+
if (to.length > 0) message.toRecipients = to;
|
|
2289
|
+
const cc = recipientsFromArgs(args, "cc");
|
|
2290
|
+
if (cc.length > 0) message.ccRecipients = cc;
|
|
2291
|
+
const bcc = recipientsFromArgs(args, "bcc");
|
|
2292
|
+
if (bcc.length > 0) message.bccRecipients = bcc;
|
|
2293
|
+
const attachments = await loadAttachments(args);
|
|
2294
|
+
if (attachments.length > 0) message.attachments = attachments;
|
|
2295
|
+
return message;
|
|
2296
|
+
}
|
|
2297
|
+
function signMessage(message, signature, args) {
|
|
2298
|
+
if (signature === void 0 || Object.keys(message).length === 0) return;
|
|
2299
|
+
const base = message.body ?? {
|
|
2300
|
+
contentType: args.bool("html") ? "html" : "text",
|
|
2301
|
+
content: ""
|
|
2302
|
+
};
|
|
2303
|
+
message.body = applySignature(base, signature);
|
|
2304
|
+
}
|
|
2305
|
+
function saveToSentItems(args) {
|
|
2306
|
+
return args.has("save-sent") ? args.bool("save-sent") : true;
|
|
2307
|
+
}
|
|
2308
|
+
function describeRecipients(recipients) {
|
|
2309
|
+
if (!recipients || recipients.length === 0) return "(none)";
|
|
2310
|
+
return recipients.map((r) => r.emailAddress.address).join(", ");
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2313
|
+
// src/commands/draft.ts
|
|
2314
|
+
var DRAFTS_PATH = "/me/mailFolders/drafts/messages";
|
|
2315
|
+
var SUBCOMMANDS3 = ["list", "create", "update", "send"];
|
|
2316
|
+
async function listDrafts(ctx) {
|
|
2317
|
+
const limit = ctx.args.num("limit");
|
|
2318
|
+
const messages = await collectItems(ctx.client, DRAFTS_PATH, {
|
|
2319
|
+
query: {
|
|
2320
|
+
$select: "id,subject,toRecipients,lastModifiedDateTime,isDraft",
|
|
2321
|
+
$orderby: "lastModifiedDateTime desc"
|
|
2322
|
+
},
|
|
2323
|
+
...limit === void 0 ? {} : { limit },
|
|
2324
|
+
...ctx.args.bool("all") ? { all: true } : {}
|
|
2325
|
+
});
|
|
2326
|
+
emit(ctx, messages, () => renderMessageList(messages));
|
|
2327
|
+
}
|
|
2328
|
+
async function createDraft(ctx) {
|
|
2329
|
+
const message = await composeMessage(ctx.args);
|
|
2330
|
+
if (Object.keys(message).length === 0) {
|
|
2331
|
+
throw new UsageError(
|
|
2332
|
+
"Nothing to create. Provide at least --subject, --body or --to."
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
const signature = await resolveSignature(ctx.args, ctx.env);
|
|
2336
|
+
signMessage(message, signature, ctx.args);
|
|
2337
|
+
const spec = ctx.client.buildSpec("POST", "/me/messages", message);
|
|
2338
|
+
const created = await guardMutation(
|
|
2339
|
+
ctx,
|
|
2340
|
+
spec,
|
|
2341
|
+
() => ctx.client.post("/me/messages", message)
|
|
2342
|
+
);
|
|
2343
|
+
if (created === void 0) return;
|
|
2344
|
+
emit(
|
|
2345
|
+
ctx,
|
|
2346
|
+
{ ...created, signatureApplied: signature !== void 0 },
|
|
2347
|
+
() => `Draft created: ${created.id}
|
|
2348
|
+
Send it with: outlook draft send ${created.id}`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
async function updateDraft(ctx) {
|
|
2352
|
+
const id = ctx.args.requirePositional(1, "<draftId>");
|
|
2353
|
+
if (signatureFlagRequested(ctx.args)) {
|
|
2354
|
+
throw new UsageError(
|
|
2355
|
+
"draft update never adds a signature (it would duplicate the one added at create time). Include it in --body yourself if you need it."
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
const message = await composeMessage(ctx.args);
|
|
2359
|
+
if (Object.keys(message).length === 0) {
|
|
2360
|
+
throw new UsageError(
|
|
2361
|
+
"Nothing to update. Provide at least one of --subject, --body, --to, --cc, --bcc."
|
|
2362
|
+
);
|
|
2363
|
+
}
|
|
2364
|
+
const path = `/me/messages/${encodeURIComponent(id)}`;
|
|
2365
|
+
const spec = ctx.client.buildSpec("PATCH", path, message);
|
|
2366
|
+
const updated = await guardMutation(
|
|
2367
|
+
ctx,
|
|
2368
|
+
spec,
|
|
2369
|
+
() => ctx.client.patch(path, message)
|
|
2370
|
+
);
|
|
2371
|
+
if (updated === void 0) return;
|
|
2372
|
+
emit(ctx, updated, () => `Draft ${id} updated.`);
|
|
2373
|
+
}
|
|
2374
|
+
async function sendDraft(ctx) {
|
|
2375
|
+
const id = ctx.args.requirePositional(1, "<draftId>");
|
|
2376
|
+
const path = `/me/messages/${encodeURIComponent(id)}/send`;
|
|
2377
|
+
const spec = ctx.client.buildSpec("POST", path, void 0);
|
|
2378
|
+
const sent = await guardMutation(ctx, spec, async () => {
|
|
2379
|
+
await ctx.client.post(path);
|
|
2380
|
+
return true;
|
|
2381
|
+
});
|
|
2382
|
+
if (sent === void 0) return;
|
|
2383
|
+
emit(ctx, { sent: true, draftId: id }, () => `Draft ${id} sent.`);
|
|
2384
|
+
}
|
|
2385
|
+
var draftCommand = {
|
|
2386
|
+
name: "draft",
|
|
2387
|
+
summary: "Create, update, list and send drafts",
|
|
2388
|
+
usage: "outlook draft list [--limit N] [--all] [--json]\n outlook draft create --subject <s> [--to <addr>...] [--body <text>] [--html] [--attach <path>...] [--dry-run]\n outlook draft update <draftId> [--subject <s>] [--body <text>] [--to <addr>...] [--dry-run]\n outlook draft send <draftId> [--dry-run]",
|
|
2389
|
+
mutating: true,
|
|
2390
|
+
async handler(ctx) {
|
|
2391
|
+
const sub = ctx.args.positionals[0];
|
|
2392
|
+
switch (sub) {
|
|
2393
|
+
case "list":
|
|
2394
|
+
return listDrafts(ctx);
|
|
2395
|
+
case "create":
|
|
2396
|
+
return createDraft(ctx);
|
|
2397
|
+
case "update":
|
|
2398
|
+
return updateDraft(ctx);
|
|
2399
|
+
case "send":
|
|
2400
|
+
return sendDraft(ctx);
|
|
2401
|
+
default:
|
|
2402
|
+
throw new UsageError(
|
|
2403
|
+
`Unknown draft subcommand${sub ? ` "${sub}"` : ""}. Valid subcommands: ${SUBCOMMANDS3.join(", ")}.`
|
|
2404
|
+
);
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
};
|
|
2408
|
+
|
|
2409
|
+
// src/commands/flags.ts
|
|
2410
|
+
function messagePath(id) {
|
|
2411
|
+
return `/me/messages/${encodeURIComponent(id)}`;
|
|
2412
|
+
}
|
|
2413
|
+
async function patchMessage(ctx, id, body, describe) {
|
|
2414
|
+
const path = messagePath(id);
|
|
2415
|
+
const spec = ctx.client.buildSpec("PATCH", path, body);
|
|
2416
|
+
await guardMutation(ctx, spec, async () => {
|
|
2417
|
+
const updated = await ctx.client.patch(path, body);
|
|
2418
|
+
emit(ctx, { id, message: updated }, () => describe(updated));
|
|
2419
|
+
});
|
|
2420
|
+
}
|
|
2421
|
+
async function setRead(ctx, isRead) {
|
|
2422
|
+
const id = ctx.args.requirePositional(0, "<id>");
|
|
2423
|
+
await patchMessage(ctx, id, { isRead }, () => `Marked ${id} as ${isRead ? "read" : "unread"}.`);
|
|
2424
|
+
}
|
|
2425
|
+
var markReadCommand = {
|
|
2426
|
+
name: "mark-read",
|
|
2427
|
+
summary: "Mark a message as read.",
|
|
2428
|
+
usage: "outlook mark-read <id> [--dry-run] [--json]",
|
|
2429
|
+
mutating: true,
|
|
2430
|
+
handler: (ctx) => setRead(ctx, true)
|
|
2431
|
+
};
|
|
2432
|
+
var markUnreadCommand = {
|
|
2433
|
+
name: "mark-unread",
|
|
2434
|
+
summary: "Mark a message as unread.",
|
|
2435
|
+
usage: "outlook mark-unread <id> [--dry-run] [--json]",
|
|
2436
|
+
mutating: true,
|
|
2437
|
+
handler: (ctx) => setRead(ctx, false)
|
|
2438
|
+
};
|
|
2439
|
+
async function setFlag(ctx, flagStatus) {
|
|
2440
|
+
const id = ctx.args.requirePositional(0, "<id>");
|
|
2441
|
+
await patchMessage(
|
|
2442
|
+
ctx,
|
|
2443
|
+
id,
|
|
2444
|
+
{ flag: { flagStatus } },
|
|
2445
|
+
() => `Set flag on ${id} to "${flagStatus}".`
|
|
2446
|
+
);
|
|
2447
|
+
}
|
|
2448
|
+
var flagCommand = {
|
|
2449
|
+
name: "flag",
|
|
2450
|
+
summary: "Flag a message for follow-up.",
|
|
2451
|
+
usage: "outlook flag <id> [--dry-run] [--json]",
|
|
2452
|
+
mutating: true,
|
|
2453
|
+
handler: (ctx) => setFlag(ctx, "flagged")
|
|
2454
|
+
};
|
|
2455
|
+
var unflagCommand = {
|
|
2456
|
+
name: "unflag",
|
|
2457
|
+
summary: "Clear a message's follow-up flag.",
|
|
2458
|
+
usage: "outlook unflag <id> [--dry-run] [--json]",
|
|
2459
|
+
mutating: true,
|
|
2460
|
+
handler: (ctx) => setFlag(ctx, "notFlagged")
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
// src/commands/folders.ts
|
|
2464
|
+
function resolveFolderId(folder) {
|
|
2465
|
+
return isWellKnownFolder(folder) ? folder.toLowerCase() : folder;
|
|
2466
|
+
}
|
|
2467
|
+
function foldersListPath(parent) {
|
|
2468
|
+
if (parent === void 0) return "/me/mailFolders";
|
|
2469
|
+
return `/me/mailFolders/${encodeURIComponent(resolveFolderId(parent))}/childFolders`;
|
|
2470
|
+
}
|
|
2471
|
+
async function listHandler3(ctx) {
|
|
2472
|
+
const parent = ctx.args.str("parent");
|
|
2473
|
+
const path = foldersListPath(parent);
|
|
2474
|
+
const opts = ctx.args.bool("all") ? { all: true } : {};
|
|
2475
|
+
const folders = await collectItems(ctx.client, path, opts);
|
|
2476
|
+
emit(ctx, folders, () => renderFolders(folders));
|
|
2477
|
+
}
|
|
2478
|
+
async function createHandler(ctx) {
|
|
2479
|
+
if (ctx.args.has("parent")) {
|
|
2480
|
+
throw new UsageError(
|
|
2481
|
+
'outlook folders create does not accept --parent: Microsoft Graph only creates mail folders at the mailbox root (docs/02-write-endpoints.md \xA77 \u2014 "folders are always created at root"; parentFolderId is not an accepted request parameter on POST /me/mailFolders).'
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
const name = ctx.args.require("name");
|
|
2485
|
+
const body = { displayName: name };
|
|
2486
|
+
const path = "/me/mailFolders";
|
|
2487
|
+
const spec = ctx.client.buildSpec("POST", path, body);
|
|
2488
|
+
const created = await guardMutation(ctx, spec, async () => ctx.client.post(path, body));
|
|
2489
|
+
if (created === void 0) return;
|
|
2490
|
+
emit(ctx, created, () => `Created folder "${created.displayName}" (id: ${created.id})`);
|
|
2491
|
+
}
|
|
2492
|
+
async function foldersHandler(ctx) {
|
|
2493
|
+
const sub = ctx.args.positionals[0] ?? "list";
|
|
2494
|
+
switch (sub) {
|
|
2495
|
+
case "list":
|
|
2496
|
+
return listHandler3(ctx);
|
|
2497
|
+
case "create":
|
|
2498
|
+
return createHandler(ctx);
|
|
2499
|
+
default:
|
|
2500
|
+
throw new UsageError(`Unknown folders subcommand "${sub}". Expected "list" or "create".`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
var foldersCommand = {
|
|
2504
|
+
name: "folders",
|
|
2505
|
+
summary: "List or create mail folders",
|
|
2506
|
+
usage: "outlook folders [list] [--parent <name|id>] [--all] [--json]\n outlook folders create --name <text> [--dry-run] [--json]",
|
|
2507
|
+
mutating: true,
|
|
2508
|
+
handler: foldersHandler
|
|
2509
|
+
};
|
|
2510
|
+
|
|
2511
|
+
// src/commands/get.ts
|
|
2512
|
+
async function getHandler2(ctx) {
|
|
2513
|
+
const id = ctx.args.requirePositional(0, "<messageId>");
|
|
2514
|
+
const path = `/me/messages/${encodeURIComponent(id)}`;
|
|
2515
|
+
const message = await ctx.client.get(path);
|
|
2516
|
+
emit(ctx, message, () => renderMessage(message, { html: ctx.args.bool("html") }));
|
|
2517
|
+
}
|
|
2518
|
+
var getCommand = {
|
|
2519
|
+
name: "get",
|
|
2520
|
+
summary: "Fetch and display a single message",
|
|
2521
|
+
usage: "outlook get <messageId> [--html] [--json]",
|
|
2522
|
+
mutating: false,
|
|
2523
|
+
handler: getHandler2
|
|
2524
|
+
};
|
|
2525
|
+
|
|
2526
|
+
// src/commands/list.ts
|
|
2527
|
+
var SELECT = "subject,from,receivedDateTime,isRead,hasAttachments,bodyPreview";
|
|
2528
|
+
var ORDERBY = "receivedDateTime desc";
|
|
2529
|
+
var MAX_TOP2 = 1e3;
|
|
2530
|
+
function folderPath(folder) {
|
|
2531
|
+
const segment = isWellKnownFolder(folder) ? folder.toLowerCase() : encodeURIComponent(folder);
|
|
2532
|
+
return `/me/mailFolders/${segment}/messages`;
|
|
2533
|
+
}
|
|
2534
|
+
function readLimit2(ctx) {
|
|
2535
|
+
if (!ctx.args.has("limit")) return 25;
|
|
2536
|
+
const limit = ctx.args.num("limit");
|
|
2537
|
+
if (limit === void 0 || !Number.isInteger(limit) || limit < 1 || limit > MAX_TOP2) {
|
|
2538
|
+
throw new UsageError(`--limit must be an integer between 1 and ${MAX_TOP2}`);
|
|
2539
|
+
}
|
|
2540
|
+
return limit;
|
|
2541
|
+
}
|
|
2542
|
+
async function listHandler4(ctx) {
|
|
2543
|
+
const folder = ctx.args.str("folder", "inbox") ?? "inbox";
|
|
2544
|
+
const limit = readLimit2(ctx);
|
|
2545
|
+
const all = ctx.args.bool("all");
|
|
2546
|
+
const unread = ctx.args.bool("unread");
|
|
2547
|
+
const query = {
|
|
2548
|
+
$select: SELECT,
|
|
2549
|
+
$orderby: ORDERBY,
|
|
2550
|
+
$top: limit
|
|
2551
|
+
};
|
|
2552
|
+
if (unread) query.$filter = "isRead eq false";
|
|
2553
|
+
const messages = await collectItems(ctx.client, folderPath(folder), {
|
|
2554
|
+
query,
|
|
2555
|
+
limit,
|
|
2556
|
+
all
|
|
2557
|
+
});
|
|
2558
|
+
emit(ctx, messages, () => renderMessageList(messages));
|
|
2559
|
+
}
|
|
2560
|
+
var listCommand = {
|
|
2561
|
+
name: "list",
|
|
2562
|
+
summary: "List messages in a folder, newest first",
|
|
2563
|
+
usage: "outlook list [--folder <name|id>] [--limit <n>] [--all] [--unread] [--json]",
|
|
2564
|
+
mutating: false,
|
|
2565
|
+
handler: listHandler4
|
|
2566
|
+
};
|
|
2567
|
+
|
|
2568
|
+
// src/commands/me.ts
|
|
2569
|
+
var SELECT_FIELDS = "id,displayName,mail,userPrincipalName,jobTitle";
|
|
2570
|
+
async function meHandler(ctx) {
|
|
2571
|
+
const profile = await ctx.client.get("/me", { $select: SELECT_FIELDS });
|
|
2572
|
+
emit(ctx, profile, () => renderProfile(profile));
|
|
2573
|
+
}
|
|
2574
|
+
var meCommand = {
|
|
2575
|
+
name: "me",
|
|
2576
|
+
summary: "Show the signed-in user's profile \u2014 also a quick auth smoke-test",
|
|
2577
|
+
usage: "outlook me [--json]",
|
|
2578
|
+
mutating: false,
|
|
2579
|
+
handler: meHandler
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
// src/commands/move.ts
|
|
2583
|
+
var DELETED_ITEMS = "deleteditems";
|
|
2584
|
+
function messagePath2(id) {
|
|
2585
|
+
return `/me/messages/${encodeURIComponent(id)}`;
|
|
2586
|
+
}
|
|
2587
|
+
function resolveDestinationId(folder) {
|
|
2588
|
+
return isWellKnownFolder(folder) ? folder.toLowerCase() : folder;
|
|
2589
|
+
}
|
|
2590
|
+
async function moveMessage(ctx, id, destinationId) {
|
|
2591
|
+
const path = `${messagePath2(id)}/move`;
|
|
2592
|
+
const body = { destinationId };
|
|
2593
|
+
const spec = ctx.client.buildSpec("POST", path, body);
|
|
2594
|
+
await guardMutation(ctx, spec, async () => {
|
|
2595
|
+
const moved = await ctx.client.post(path, body);
|
|
2596
|
+
emit(
|
|
2597
|
+
ctx,
|
|
2598
|
+
{ movedFrom: id, destinationId, message: moved },
|
|
2599
|
+
() => `Moved message ${id} -> ${destinationId}. New id: ${moved.id} (the old id no longer resolves \u2014 use the new id for anything further).`
|
|
2600
|
+
);
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
async function deleteHandler(ctx) {
|
|
2604
|
+
const id = ctx.args.requirePositional(0, "<id>");
|
|
2605
|
+
await moveMessage(ctx, id, DELETED_ITEMS);
|
|
2606
|
+
}
|
|
2607
|
+
var deleteCommand = {
|
|
2608
|
+
name: "delete",
|
|
2609
|
+
summary: 'Move a message to Deleted Items (recoverable). The safe default "delete".',
|
|
2610
|
+
usage: "outlook delete <id> [--dry-run] [--json]",
|
|
2611
|
+
mutating: true,
|
|
2612
|
+
handler: deleteHandler
|
|
2613
|
+
};
|
|
2614
|
+
async function moveHandler(ctx) {
|
|
2615
|
+
const id = ctx.args.requirePositional(0, "<id>");
|
|
2616
|
+
const folder = ctx.args.require("folder");
|
|
2617
|
+
await moveMessage(ctx, id, resolveDestinationId(folder));
|
|
2618
|
+
}
|
|
2619
|
+
var moveCommand = {
|
|
2620
|
+
name: "move",
|
|
2621
|
+
summary: "Move a message to another folder (well-known name or folder id).",
|
|
2622
|
+
usage: "outlook move <id> --folder <name|id> [--dry-run] [--json]",
|
|
2623
|
+
mutating: true,
|
|
2624
|
+
handler: moveHandler
|
|
2625
|
+
};
|
|
2626
|
+
async function purgeHandler(ctx) {
|
|
2627
|
+
const id = ctx.args.requirePositional(0, "<id>");
|
|
2628
|
+
if (!ctx.args.bool("yes")) {
|
|
2629
|
+
throw new UsageError(
|
|
2630
|
+
`outlook purge issues a raw DELETE /me/messages/${id}. Microsoft's docs never state whether this is recoverable \u2014 it is treated as permanent. Pass --yes to confirm you understand this, or run \`outlook delete ${id}\` instead, which moves the message to Deleted Items and IS recoverable.`
|
|
2631
|
+
);
|
|
2632
|
+
}
|
|
2633
|
+
const path = messagePath2(id);
|
|
2634
|
+
const spec = ctx.client.buildSpec("DELETE", path);
|
|
2635
|
+
await guardMutation(ctx, spec, async () => {
|
|
2636
|
+
await ctx.client.del(path);
|
|
2637
|
+
emit(
|
|
2638
|
+
ctx,
|
|
2639
|
+
{ purged: true, id },
|
|
2640
|
+
() => `Purged message ${id} via raw DELETE. Recoverability is not documented by Microsoft \u2014 treat this as permanent.`
|
|
2641
|
+
);
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
var purgeCommand = {
|
|
2645
|
+
name: "purge",
|
|
2646
|
+
summary: "Permanently delete a message via raw DELETE. Requires --yes.",
|
|
2647
|
+
usage: "outlook purge <id> --yes [--dry-run] [--json]",
|
|
2648
|
+
mutating: true,
|
|
2649
|
+
handler: purgeHandler
|
|
2650
|
+
};
|
|
2651
|
+
|
|
2652
|
+
// src/commands/replyDraft.ts
|
|
2653
|
+
function buildReplyDraftSpecs(client, opts) {
|
|
2654
|
+
const basePath = `/me/messages/${encodeURIComponent(opts.messageId)}`;
|
|
2655
|
+
const createBody = {};
|
|
2656
|
+
if (opts.toRecipients) createBody["toRecipients"] = opts.toRecipients;
|
|
2657
|
+
const createSpec = client.buildSpec("POST", `${basePath}/${opts.action}`, createBody);
|
|
2658
|
+
const patchPreview = buildPatchBody(
|
|
2659
|
+
{ contentType: "html", content: "<html><body><!-- Exchange quoted body --></body></html>" },
|
|
2660
|
+
opts.userText,
|
|
2661
|
+
opts.signature
|
|
2662
|
+
);
|
|
2663
|
+
const patchSpec = client.buildSpec("PATCH", "/me/messages/{draftId}", patchPreview);
|
|
2664
|
+
const sendSpec = client.buildSpec("POST", "/me/messages/{draftId}/send", void 0);
|
|
2665
|
+
return { createSpec, patchSpec, sendSpec };
|
|
2666
|
+
}
|
|
2667
|
+
async function executeReplyDraft(client, opts) {
|
|
2668
|
+
const basePath = `/me/messages/${encodeURIComponent(opts.messageId)}`;
|
|
2669
|
+
const createBody = {};
|
|
2670
|
+
if (opts.toRecipients) createBody["toRecipients"] = opts.toRecipients;
|
|
2671
|
+
const draft = await client.post(
|
|
2672
|
+
`${basePath}/${opts.action}`,
|
|
2673
|
+
createBody
|
|
2674
|
+
);
|
|
2675
|
+
const draftId = draft?.id;
|
|
2676
|
+
if (!draftId) {
|
|
2677
|
+
throw new OutlookCliError(
|
|
2678
|
+
`${opts.action} did not return a draft id. The message may not have been sent.`
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
2681
|
+
try {
|
|
2682
|
+
const draftFull = await client.get(
|
|
2683
|
+
`/me/messages/${encodeURIComponent(draftId)}`,
|
|
2684
|
+
{ $select: "id,body" }
|
|
2685
|
+
);
|
|
2686
|
+
if (!draftFull?.body) {
|
|
2687
|
+
throw new OutlookCliError(
|
|
2688
|
+
"Exchange returned a draft with no body. Cannot patch reply."
|
|
2689
|
+
);
|
|
2690
|
+
}
|
|
2691
|
+
const draftBody = draftFull.body;
|
|
2692
|
+
const patchBody = buildPatchBody(draftBody, opts.userText, opts.signature);
|
|
2693
|
+
await client.patch(
|
|
2694
|
+
`/me/messages/${encodeURIComponent(draftId)}`,
|
|
2695
|
+
patchBody
|
|
2696
|
+
);
|
|
2697
|
+
await client.post(
|
|
2698
|
+
`/me/messages/${encodeURIComponent(draftId)}/send`,
|
|
2699
|
+
void 0
|
|
2700
|
+
);
|
|
2701
|
+
} catch (stepErr) {
|
|
2702
|
+
let cleanupErr;
|
|
2703
|
+
try {
|
|
2704
|
+
await client.del(`/me/messages/${encodeURIComponent(draftId)}`);
|
|
2705
|
+
} catch (e) {
|
|
2706
|
+
cleanupErr = e;
|
|
2707
|
+
}
|
|
2708
|
+
const base = stepErr instanceof Error ? stepErr.message : String(stepErr);
|
|
2709
|
+
if (cleanupErr !== void 0) {
|
|
2710
|
+
const cleanMsg = cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr);
|
|
2711
|
+
throw new OutlookCliError(
|
|
2712
|
+
`Failed to send reply (${base}). Cleanup also failed (${cleanMsg}). A draft was left behind (ORPHAN) \u2014 run \`outlook draft list\` to find and delete it.`
|
|
2713
|
+
);
|
|
2714
|
+
}
|
|
2715
|
+
throw new OutlookCliError(
|
|
2716
|
+
`Failed to send reply (${base}). The draft was deleted. No message was sent.`
|
|
2717
|
+
);
|
|
2718
|
+
}
|
|
2719
|
+
return draftId;
|
|
2720
|
+
}
|
|
2721
|
+
function buildPatchBody(draftBody, userText, sig) {
|
|
2722
|
+
const isHtml = draftBody.contentType.toLowerCase() === "html";
|
|
2723
|
+
const newBody = isHtml ? insertHtml(draftBody.content, userText, sig) : insertText(draftBody.content, userText, sig);
|
|
2724
|
+
return { body: { contentType: draftBody.contentType, content: newBody } };
|
|
2725
|
+
}
|
|
2726
|
+
function insertHtml(draftContent, userText, sig) {
|
|
2727
|
+
const textHtml = escapeHtml2(userText.trimEnd()).replace(/\r?\n/g, "<br>");
|
|
2728
|
+
const sigHtml = sig.html?.trim() ?? escapeHtml2((sig.text ?? "").trimEnd()).replace(/\r?\n/g, "<br>");
|
|
2729
|
+
const insertion = `<div>${textHtml}</div><br><div class="outlook-cli-signature">${sigHtml}</div><br>`;
|
|
2730
|
+
const quoteMatch = draftContent.match(/<div\s+id=["']divRplyFwdMsg["']/i);
|
|
2731
|
+
if (quoteMatch?.index !== void 0) {
|
|
2732
|
+
return draftContent.slice(0, quoteMatch.index) + insertion + draftContent.slice(quoteMatch.index);
|
|
2733
|
+
}
|
|
2734
|
+
const bodyMatch = [...draftContent.matchAll(/<body[^>]*>/gi)];
|
|
2735
|
+
const lastBody = bodyMatch[bodyMatch.length - 1];
|
|
2736
|
+
if (lastBody?.index !== void 0) {
|
|
2737
|
+
const afterBody = lastBody.index + lastBody[0].length;
|
|
2738
|
+
return draftContent.slice(0, afterBody) + insertion + draftContent.slice(afterBody);
|
|
2739
|
+
}
|
|
2740
|
+
return insertion + draftContent;
|
|
2741
|
+
}
|
|
2742
|
+
function insertText(draftContent, userText, sig) {
|
|
2743
|
+
if (sig.text === void 0) {
|
|
2744
|
+
const stripped = (sig.html ?? "").replace(/<[^>]*>/g, "").trim();
|
|
2745
|
+
return buildTextInsertion(draftContent, userText, stripped);
|
|
2746
|
+
}
|
|
2747
|
+
return buildTextInsertion(draftContent, userText, sig.text.trimEnd());
|
|
2748
|
+
}
|
|
2749
|
+
function buildTextInsertion(draftContent, userText, sigText) {
|
|
2750
|
+
const head = userText.trimEnd();
|
|
2751
|
+
const sigBlock = `${TEXT_DELIMITER}
|
|
2752
|
+
${sigText}`;
|
|
2753
|
+
const newContent = head === "" ? sigBlock : `${head}
|
|
2754
|
+
|
|
2755
|
+
${sigBlock}`;
|
|
2756
|
+
const sepMatch = draftContent.match(/^-{3,}[^-\r\n][^\r\n]*-{3,}/im);
|
|
2757
|
+
if (sepMatch?.index !== void 0) {
|
|
2758
|
+
return `${newContent}
|
|
2759
|
+
|
|
2760
|
+
${draftContent.slice(sepMatch.index)}`;
|
|
2761
|
+
}
|
|
2762
|
+
if (draftContent.trim() === "") return newContent;
|
|
2763
|
+
return `${newContent}
|
|
2764
|
+
|
|
2765
|
+
${draftContent}`;
|
|
2766
|
+
}
|
|
2767
|
+
function escapeHtml2(s) {
|
|
2768
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// src/commands/reply.ts
|
|
2772
|
+
var ACTION_PATH = {
|
|
2773
|
+
reply: "reply",
|
|
2774
|
+
replyAll: "replyAll",
|
|
2775
|
+
forward: "forward"
|
|
2776
|
+
};
|
|
2777
|
+
var CREATE_ACTION = {
|
|
2778
|
+
reply: "createReply",
|
|
2779
|
+
replyAll: "createReplyAll",
|
|
2780
|
+
forward: "createForward"
|
|
2781
|
+
};
|
|
2782
|
+
async function runReplyAction(ctx, kind, label) {
|
|
2783
|
+
const messageId = ctx.args.requirePositional(0, "<messageId>");
|
|
2784
|
+
const userText = ctx.args.str("body") ?? "";
|
|
2785
|
+
const signature = await resolveSignature(ctx.args, ctx.env);
|
|
2786
|
+
const toRecipients = kind === "forward" ? recipientsFromArgs(ctx.args, "to") : void 0;
|
|
2787
|
+
if (kind === "forward" && (!toRecipients || toRecipients.length === 0)) {
|
|
2788
|
+
throw new UsageError(
|
|
2789
|
+
"forward requires at least one recipient. Use --to <address>."
|
|
2790
|
+
);
|
|
2791
|
+
}
|
|
2792
|
+
const useHtmlFlow = signature !== void 0 && signature.html !== void 0;
|
|
2793
|
+
if (useHtmlFlow) {
|
|
2794
|
+
await runHtmlFlow(ctx, kind, label, messageId, userText, signature, toRecipients);
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
const comment = signature === void 0 ? userText : appendTextSignature(userText, signature);
|
|
2798
|
+
const payload = { comment };
|
|
2799
|
+
if (toRecipients) payload["toRecipients"] = toRecipients;
|
|
2800
|
+
const path = `/me/messages/${encodeURIComponent(messageId)}/${ACTION_PATH[kind]}`;
|
|
2801
|
+
const spec = ctx.client.buildSpec("POST", path, payload);
|
|
2802
|
+
const done = await guardMutation(ctx, spec, async () => {
|
|
2803
|
+
await ctx.client.post(path, payload);
|
|
2804
|
+
return true;
|
|
2805
|
+
});
|
|
2806
|
+
if (done === void 0) return;
|
|
2807
|
+
emit(
|
|
2808
|
+
ctx,
|
|
2809
|
+
{
|
|
2810
|
+
action: kind,
|
|
2811
|
+
messageId,
|
|
2812
|
+
sent: true,
|
|
2813
|
+
signatureApplied: signature !== void 0,
|
|
2814
|
+
htmlSignature: false
|
|
2815
|
+
},
|
|
2816
|
+
() => `${label} sent for message ${messageId}` + (toRecipients ? ` to ${describeRecipients(toRecipients)}` : "")
|
|
2817
|
+
);
|
|
2818
|
+
}
|
|
2819
|
+
async function runHtmlFlow(ctx, kind, label, messageId, userText, signature, toRecipients) {
|
|
2820
|
+
const specs = buildReplyDraftSpecs(ctx.client, {
|
|
2821
|
+
messageId,
|
|
2822
|
+
action: CREATE_ACTION[kind],
|
|
2823
|
+
userText,
|
|
2824
|
+
signature,
|
|
2825
|
+
toRecipients
|
|
2826
|
+
});
|
|
2827
|
+
if (ctx.dryRun) {
|
|
2828
|
+
ctx.out("DRY RUN \u2014 nothing was sent. The three requests below are what would go out:");
|
|
2829
|
+
ctx.out("");
|
|
2830
|
+
ctx.out("--- Step 1: create draft ---");
|
|
2831
|
+
ctx.out(renderRequest(specs.createSpec));
|
|
2832
|
+
ctx.out("");
|
|
2833
|
+
ctx.out("--- Step 2: patch body (representative; actual body depends on Exchange draft) ---");
|
|
2834
|
+
ctx.out(renderRequest(specs.patchSpec));
|
|
2835
|
+
ctx.out("");
|
|
2836
|
+
ctx.out("--- Step 3: send draft ---");
|
|
2837
|
+
ctx.out(renderRequest(specs.sendSpec));
|
|
2838
|
+
const verdict = await dryRunVerdict(ctx, specs.sendSpec);
|
|
2839
|
+
if (verdict !== void 0) {
|
|
2840
|
+
ctx.out("");
|
|
2841
|
+
ctx.out(renderVerdict(verdict));
|
|
2842
|
+
}
|
|
2843
|
+
return;
|
|
2844
|
+
}
|
|
2845
|
+
await enforcePolicy(ctx, specs.createSpec);
|
|
2846
|
+
await enforcePolicy(ctx, specs.patchSpec);
|
|
2847
|
+
await enforcePolicy(ctx, specs.sendSpec);
|
|
2848
|
+
const draftId = await executeReplyDraft(ctx.client, {
|
|
2849
|
+
messageId,
|
|
2850
|
+
action: CREATE_ACTION[kind],
|
|
2851
|
+
userText,
|
|
2852
|
+
signature,
|
|
2853
|
+
toRecipients
|
|
2854
|
+
});
|
|
2855
|
+
emit(
|
|
2856
|
+
ctx,
|
|
2857
|
+
{
|
|
2858
|
+
action: kind,
|
|
2859
|
+
messageId,
|
|
2860
|
+
sent: true,
|
|
2861
|
+
signatureApplied: true,
|
|
2862
|
+
htmlSignature: true,
|
|
2863
|
+
draftId
|
|
2864
|
+
},
|
|
2865
|
+
() => `${label} sent for message ${messageId}` + (toRecipients ? ` to ${describeRecipients(toRecipients)}` : "")
|
|
2866
|
+
);
|
|
2867
|
+
}
|
|
2868
|
+
var replyCommand = {
|
|
2869
|
+
name: "reply",
|
|
2870
|
+
summary: "Reply to the sender of a message",
|
|
2871
|
+
usage: "outlook reply <messageId> [--body <text>] [--signature <path> | --no-signature] [--dry-run] [--json]",
|
|
2872
|
+
mutating: true,
|
|
2873
|
+
handler: (ctx) => runReplyAction(ctx, "reply", "Reply")
|
|
2874
|
+
};
|
|
2875
|
+
var replyAllCommand = {
|
|
2876
|
+
name: "reply-all",
|
|
2877
|
+
summary: "Reply to all recipients of a message",
|
|
2878
|
+
usage: "outlook reply-all <messageId> [--body <text>] [--signature <path> | --no-signature] [--dry-run] [--json]",
|
|
2879
|
+
mutating: true,
|
|
2880
|
+
handler: (ctx) => runReplyAction(ctx, "replyAll", "Reply-all")
|
|
2881
|
+
};
|
|
2882
|
+
var forwardCommand = {
|
|
2883
|
+
name: "forward",
|
|
2884
|
+
summary: "Forward a message to other recipients",
|
|
2885
|
+
usage: "outlook forward <messageId> --to <addr> [--to <addr>...] [--body <text>] [--signature <path> | --no-signature] [--dry-run] [--json]",
|
|
2886
|
+
mutating: true,
|
|
2887
|
+
handler: (ctx) => runReplyAction(ctx, "forward", "Forward")
|
|
2888
|
+
};
|
|
2889
|
+
|
|
2890
|
+
// src/commands/rules.ts
|
|
2891
|
+
function addrList(addrs) {
|
|
2892
|
+
if (!addrs || addrs.length === 0) return "";
|
|
2893
|
+
return addrs.map((a) => a.emailAddress?.address ?? a.emailAddress?.name ?? "?").join(", ");
|
|
2894
|
+
}
|
|
2895
|
+
function summariseConditions(c) {
|
|
2896
|
+
if (!c) return "(any)";
|
|
2897
|
+
const parts = [];
|
|
2898
|
+
const from = addrList(c.fromAddresses);
|
|
2899
|
+
if (from) parts.push(`from: ${from}`);
|
|
2900
|
+
const to = addrList(c.sentToAddresses);
|
|
2901
|
+
if (to) parts.push(`to: ${to}`);
|
|
2902
|
+
if (c.subjectContains?.length) parts.push(`subject contains: ${c.subjectContains.join(", ")}`);
|
|
2903
|
+
if (c.bodyContains?.length) parts.push(`body contains: ${c.bodyContains.join(", ")}`);
|
|
2904
|
+
if (c.senderContains?.length) parts.push(`sender contains: ${c.senderContains.join(", ")}`);
|
|
2905
|
+
if (c.hasAttachments) parts.push("has attachments");
|
|
2906
|
+
if (c.importance) parts.push(`importance: ${c.importance}`);
|
|
2907
|
+
return parts.length > 0 ? parts.join("; ") : "(any)";
|
|
2908
|
+
}
|
|
2909
|
+
function summariseActions(a) {
|
|
2910
|
+
if (!a) return "(none)";
|
|
2911
|
+
const parts = [];
|
|
2912
|
+
if (a.moveToFolder) parts.push(`move to [${a.moveToFolder}]`);
|
|
2913
|
+
if (a.copyToFolder) parts.push(`copy to [${a.copyToFolder}]`);
|
|
2914
|
+
if (a.delete) parts.push("delete");
|
|
2915
|
+
if (a.permanentDelete) parts.push("permanent delete");
|
|
2916
|
+
if (a.markAsRead) parts.push("mark read");
|
|
2917
|
+
if (a.markImportance) parts.push(`mark ${a.markImportance}`);
|
|
2918
|
+
const fwd = addrList(a.forwardTo);
|
|
2919
|
+
if (fwd) parts.push(`forward to: ${fwd}`);
|
|
2920
|
+
const redir = addrList(a.redirectTo);
|
|
2921
|
+
if (redir) parts.push(`redirect to: ${redir}`);
|
|
2922
|
+
if (a.stopProcessingRules) parts.push("stop processing");
|
|
2923
|
+
return parts.length > 0 ? parts.join("; ") : "(none)";
|
|
2924
|
+
}
|
|
2925
|
+
function renderRules(rules) {
|
|
2926
|
+
if (rules.length === 0) return "(no rules)";
|
|
2927
|
+
const header = [
|
|
2928
|
+
"#".padEnd(3),
|
|
2929
|
+
"ENABLED".padEnd(8),
|
|
2930
|
+
"NAME".padEnd(28),
|
|
2931
|
+
"CONDITIONS".padEnd(36),
|
|
2932
|
+
"ACTIONS"
|
|
2933
|
+
].join(" ");
|
|
2934
|
+
const rows = rules.map((r) => [
|
|
2935
|
+
String(r.sequence ?? "-").padEnd(3),
|
|
2936
|
+
(r.isEnabled ? "yes" : "no").padEnd(8),
|
|
2937
|
+
(r.displayName ?? "(unnamed)").slice(0, 28).padEnd(28),
|
|
2938
|
+
summariseConditions(r.conditions).slice(0, 36).padEnd(36),
|
|
2939
|
+
summariseActions(r.actions)
|
|
2940
|
+
].join(" "));
|
|
2941
|
+
return [header, ...rows].join("\n");
|
|
2942
|
+
}
|
|
2943
|
+
function buildRuleBody(ctx) {
|
|
2944
|
+
const name = ctx.args.require("name");
|
|
2945
|
+
const folderId = ctx.args.require("folder");
|
|
2946
|
+
const fromAddrs = ctx.args.strAll("from");
|
|
2947
|
+
const senderContains = ctx.args.strAll("sender-contains");
|
|
2948
|
+
if (fromAddrs.length === 0 && senderContains.length === 0) {
|
|
2949
|
+
throw new UsageError(
|
|
2950
|
+
"At least one condition is required: --from <address> and/or --sender-contains <domain>. A rule with no condition would match every message."
|
|
2951
|
+
);
|
|
2952
|
+
}
|
|
2953
|
+
const conditions = {};
|
|
2954
|
+
if (fromAddrs.length > 0) {
|
|
2955
|
+
conditions.fromAddresses = fromAddrs.map((addr) => ({
|
|
2956
|
+
emailAddress: { address: addr }
|
|
2957
|
+
}));
|
|
2958
|
+
}
|
|
2959
|
+
if (senderContains.length > 0) {
|
|
2960
|
+
conditions.senderContains = senderContains;
|
|
2961
|
+
}
|
|
2962
|
+
const actions = {
|
|
2963
|
+
moveToFolder: folderId
|
|
2964
|
+
};
|
|
2965
|
+
if (ctx.args.bool("mark-read")) {
|
|
2966
|
+
actions.markAsRead = true;
|
|
2967
|
+
}
|
|
2968
|
+
if (!ctx.args.has("stop-processing") || ctx.args.bool("stop-processing")) {
|
|
2969
|
+
actions.stopProcessingRules = true;
|
|
2970
|
+
}
|
|
2971
|
+
const body = {
|
|
2972
|
+
displayName: name,
|
|
2973
|
+
conditions,
|
|
2974
|
+
actions,
|
|
2975
|
+
isEnabled: !ctx.args.has("enabled") || ctx.args.bool("enabled")
|
|
2976
|
+
};
|
|
2977
|
+
const seq = ctx.args.num("sequence");
|
|
2978
|
+
if (seq !== void 0) body.sequence = seq;
|
|
2979
|
+
return body;
|
|
2980
|
+
}
|
|
2981
|
+
async function createHandler2(ctx) {
|
|
2982
|
+
const body = buildRuleBody(ctx);
|
|
2983
|
+
const spec = ctx.client.buildSpec("POST", RULES_PATH, body);
|
|
2984
|
+
const created = await guardMutation(
|
|
2985
|
+
ctx,
|
|
2986
|
+
spec,
|
|
2987
|
+
async () => ctx.client.post(RULES_PATH, body)
|
|
2988
|
+
);
|
|
2989
|
+
if (created === void 0) return;
|
|
2990
|
+
emit(ctx, created, () => {
|
|
2991
|
+
const name = created.displayName ?? "(unnamed)";
|
|
2992
|
+
const seq = created.sequence ?? "?";
|
|
2993
|
+
const cond = summariseConditions(created.conditions);
|
|
2994
|
+
const acts = summariseActions(created.actions);
|
|
2995
|
+
return `Created rule #${seq} "${name}"
|
|
2996
|
+
Conditions: ${cond}
|
|
2997
|
+
Actions: ${acts}`;
|
|
2998
|
+
});
|
|
2999
|
+
}
|
|
3000
|
+
var RULES_PATH = "/me/mailFolders/inbox/messageRules";
|
|
3001
|
+
async function rulesHandler(ctx) {
|
|
3002
|
+
const sub = ctx.args.positionals[0] ?? "list";
|
|
3003
|
+
switch (sub) {
|
|
3004
|
+
case "list": {
|
|
3005
|
+
const response = await ctx.client.get(RULES_PATH);
|
|
3006
|
+
const rules = response.value;
|
|
3007
|
+
emit(ctx, rules, () => renderRules(rules));
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
case "create":
|
|
3011
|
+
return createHandler2(ctx);
|
|
3012
|
+
default: {
|
|
3013
|
+
throw new UsageError(
|
|
3014
|
+
`Unknown rules subcommand "${sub}". Expected "list" or "create".`
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
var rulesCommand = {
|
|
3020
|
+
name: "rules",
|
|
3021
|
+
summary: "List or create inbox message rules",
|
|
3022
|
+
usage: "outlook rules [list] [--json]\n outlook rules create --name <text> --folder <id>\n [--from <addr>...] [--sender-contains <domain>...]\n [--mark-read] [--stop-processing] [--enabled]\n (--stop-processing defaults to on; use --no-stop-processing to disable)\n [--dry-run] [--json]",
|
|
3023
|
+
mutating: true,
|
|
3024
|
+
handler: rulesHandler
|
|
3025
|
+
};
|
|
3026
|
+
|
|
3027
|
+
// src/commands/search.ts
|
|
3028
|
+
var MAX_RESULTS = 1e3;
|
|
3029
|
+
var SELECT2 = "subject,from,receivedDateTime,isRead,hasAttachments,bodyPreview";
|
|
3030
|
+
function folderSegment(folder) {
|
|
3031
|
+
return isWellKnownFolder(folder) ? folder.toLowerCase() : encodeURIComponent(folder);
|
|
3032
|
+
}
|
|
3033
|
+
function searchPath(folder) {
|
|
3034
|
+
if (folder === void 0) return "/me/messages";
|
|
3035
|
+
return `/me/mailFolders/${folderSegment(folder)}/messages`;
|
|
3036
|
+
}
|
|
3037
|
+
function quoteSearch(query) {
|
|
3038
|
+
const escaped = query.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
3039
|
+
return `"${escaped}"`;
|
|
3040
|
+
}
|
|
3041
|
+
function readLimit3(ctx) {
|
|
3042
|
+
if (!ctx.args.has("limit")) return 25;
|
|
3043
|
+
const limit = ctx.args.num("limit");
|
|
3044
|
+
if (limit === void 0 || !Number.isInteger(limit) || limit < 1 || limit > MAX_RESULTS) {
|
|
3045
|
+
throw new UsageError(
|
|
3046
|
+
`--limit must be an integer between 1 and ${MAX_RESULTS} \u2014 docs/01 \xA74a documents $search on messages as returning "up to 1,000 results"`
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
return limit;
|
|
3050
|
+
}
|
|
3051
|
+
async function searchHandler2(ctx) {
|
|
3052
|
+
const query = ctx.args.requirePositional(0, "<query>");
|
|
3053
|
+
const limit = readLimit3(ctx);
|
|
3054
|
+
const all = ctx.args.bool("all");
|
|
3055
|
+
const folder = ctx.args.str("folder");
|
|
3056
|
+
const messages = await collectItems(ctx.client, searchPath(folder), {
|
|
3057
|
+
query: { $search: quoteSearch(query), $select: SELECT2, $top: limit },
|
|
3058
|
+
limit,
|
|
3059
|
+
all
|
|
3060
|
+
});
|
|
3061
|
+
emit(ctx, messages, () => renderMessageList(messages));
|
|
3062
|
+
}
|
|
3063
|
+
var searchCommand = {
|
|
3064
|
+
name: "search",
|
|
3065
|
+
summary: "Search messages with KQL/$search",
|
|
3066
|
+
usage: "outlook search <query> [--limit <n>] [--all] [--folder <name|id>] [--json]",
|
|
3067
|
+
mutating: false,
|
|
3068
|
+
handler: searchHandler2
|
|
3069
|
+
};
|
|
3070
|
+
|
|
3071
|
+
// src/commands/send.ts
|
|
3072
|
+
var sendCommand = {
|
|
3073
|
+
name: "send",
|
|
3074
|
+
summary: "Send an email",
|
|
3075
|
+
usage: "outlook send --to <addr> [--to <addr>...] --subject <text> [--cc <addr>] [--bcc <addr>] [--body <text> | --body-file <path>] [--html] [--attach <path>...] [--signature <path> | --no-signature] [--no-save-sent] [--dry-run] [--json]",
|
|
3076
|
+
mutating: true,
|
|
3077
|
+
async handler(ctx) {
|
|
3078
|
+
const message = await composeMessage(ctx.args);
|
|
3079
|
+
if (!message.toRecipients || message.toRecipients.length === 0) {
|
|
3080
|
+
throw new UsageError(
|
|
3081
|
+
"At least one recipient is required. Use --to <address> (repeatable, or comma-separated)."
|
|
3082
|
+
);
|
|
3083
|
+
}
|
|
3084
|
+
if (message.subject === void 0 || message.subject.trim() === "") {
|
|
3085
|
+
throw new UsageError("--subject is required.");
|
|
3086
|
+
}
|
|
3087
|
+
const signature = await resolveSignature(ctx.args, ctx.env);
|
|
3088
|
+
signMessage(message, signature, ctx.args);
|
|
3089
|
+
const payload = {
|
|
3090
|
+
message,
|
|
3091
|
+
saveToSentItems: saveToSentItems(ctx.args)
|
|
3092
|
+
};
|
|
3093
|
+
const spec = ctx.client.buildSpec("POST", "/me/sendMail", payload);
|
|
3094
|
+
const sent = await guardMutation(ctx, spec, async () => {
|
|
3095
|
+
await ctx.client.post("/me/sendMail", payload);
|
|
3096
|
+
return true;
|
|
3097
|
+
});
|
|
3098
|
+
if (sent === void 0) return;
|
|
3099
|
+
const to = describeRecipients(message.toRecipients);
|
|
3100
|
+
emit(
|
|
3101
|
+
ctx,
|
|
3102
|
+
{
|
|
3103
|
+
sent: true,
|
|
3104
|
+
subject: message.subject,
|
|
3105
|
+
to: message.toRecipients.map((r) => r.emailAddress.address),
|
|
3106
|
+
attachments: message.attachments?.length ?? 0,
|
|
3107
|
+
savedToSentItems: payload.saveToSentItems,
|
|
3108
|
+
signatureApplied: signature !== void 0
|
|
3109
|
+
},
|
|
3110
|
+
() => `Sent "${message.subject}" to ${to}`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
};
|
|
3114
|
+
|
|
3115
|
+
// src/cli.ts
|
|
3116
|
+
loadEnvFile();
|
|
3117
|
+
var COMMANDS = [
|
|
3118
|
+
authCommand,
|
|
3119
|
+
meCommand,
|
|
3120
|
+
listCommand,
|
|
3121
|
+
getCommand,
|
|
3122
|
+
searchCommand,
|
|
3123
|
+
foldersCommand,
|
|
3124
|
+
attachmentsCommand,
|
|
3125
|
+
contactsCommand,
|
|
3126
|
+
sendCommand,
|
|
3127
|
+
replyCommand,
|
|
3128
|
+
replyAllCommand,
|
|
3129
|
+
forwardCommand,
|
|
3130
|
+
draftCommand,
|
|
3131
|
+
moveCommand,
|
|
3132
|
+
deleteCommand,
|
|
3133
|
+
purgeCommand,
|
|
3134
|
+
markReadCommand,
|
|
3135
|
+
markUnreadCommand,
|
|
3136
|
+
flagCommand,
|
|
3137
|
+
unflagCommand,
|
|
3138
|
+
rulesCommand
|
|
3139
|
+
];
|
|
3140
|
+
var REGISTRY = new Map(
|
|
3141
|
+
COMMANDS.map((command) => [command.name, command])
|
|
3142
|
+
);
|
|
3143
|
+
var GROUPS = [
|
|
3144
|
+
{ title: "Auth & identity", names: ["auth", "me"] },
|
|
3145
|
+
{
|
|
3146
|
+
title: "Read",
|
|
3147
|
+
names: ["list", "get", "search", "folders", "attachments"]
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
title: "People",
|
|
3151
|
+
names: ["contacts"]
|
|
3152
|
+
},
|
|
3153
|
+
{
|
|
3154
|
+
title: "Send",
|
|
3155
|
+
names: ["send", "reply", "reply-all", "forward", "draft"]
|
|
3156
|
+
},
|
|
3157
|
+
{
|
|
3158
|
+
title: "Organize",
|
|
3159
|
+
names: [
|
|
3160
|
+
"move",
|
|
3161
|
+
"delete",
|
|
3162
|
+
"purge",
|
|
3163
|
+
"mark-read",
|
|
3164
|
+
"mark-unread",
|
|
3165
|
+
"flag",
|
|
3166
|
+
"unflag",
|
|
3167
|
+
"rules"
|
|
3168
|
+
]
|
|
3169
|
+
}
|
|
3170
|
+
];
|
|
3171
|
+
function packageVersion() {
|
|
3172
|
+
try {
|
|
3173
|
+
const pkg = createRequire(import.meta.url)("../package.json");
|
|
3174
|
+
return pkg.version ?? "0.0.0";
|
|
3175
|
+
} catch {
|
|
3176
|
+
return "0.0.0";
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
function helpText() {
|
|
3180
|
+
const lines = [
|
|
3181
|
+
"outlook \u2014 Microsoft Graph Outlook Mail from the command line",
|
|
3182
|
+
"",
|
|
3183
|
+
"Usage: outlook <command> [options]",
|
|
3184
|
+
""
|
|
3185
|
+
];
|
|
3186
|
+
for (const group of GROUPS) {
|
|
3187
|
+
lines.push(`${group.title}:`);
|
|
3188
|
+
for (const name of group.names) {
|
|
3189
|
+
const command = REGISTRY.get(name);
|
|
3190
|
+
if (command === void 0) continue;
|
|
3191
|
+
const flag = command.mutating ? " [--dry-run]" : "";
|
|
3192
|
+
lines.push(` ${name.padEnd(13)} ${command.summary}${flag}`);
|
|
3193
|
+
}
|
|
3194
|
+
lines.push("");
|
|
3195
|
+
}
|
|
3196
|
+
lines.push(
|
|
3197
|
+
"Global options:",
|
|
3198
|
+
" --json Machine-readable output",
|
|
3199
|
+
" --dry-run On a mutating command: print the request, send nothing",
|
|
3200
|
+
" -h, --help This help, or `outlook <command> --help`",
|
|
3201
|
+
" -V, --version Print the version",
|
|
3202
|
+
"",
|
|
3203
|
+
"Environment:",
|
|
3204
|
+
" OUTLOOK_POLICY=<file> Restrict mutations to a JSON policy (README: Restricted agent mode)",
|
|
3205
|
+
"",
|
|
3206
|
+
"First run: export GRAPH_CLIENT_ID=<azure-app-id> && outlook auth login",
|
|
3207
|
+
"Setup walkthrough: SETUP.md",
|
|
3208
|
+
""
|
|
3209
|
+
);
|
|
3210
|
+
return lines.join("\n");
|
|
3211
|
+
}
|
|
3212
|
+
function buildContext(args, commandName) {
|
|
3213
|
+
const config = resolveConfig(process.env);
|
|
3214
|
+
const store = createTokenStore(config.tokenCachePath);
|
|
3215
|
+
const auth = createAuthenticator(config, store);
|
|
3216
|
+
const client = new GraphClient({
|
|
3217
|
+
getToken: () => auth.getAccessToken()
|
|
3218
|
+
});
|
|
3219
|
+
return {
|
|
3220
|
+
args,
|
|
3221
|
+
json: args.bool("json"),
|
|
3222
|
+
dryRun: args.bool("dry-run"),
|
|
3223
|
+
command: commandName,
|
|
3224
|
+
client,
|
|
3225
|
+
auth,
|
|
3226
|
+
env: process.env,
|
|
3227
|
+
out: (line) => {
|
|
3228
|
+
process.stdout.write(`${line}
|
|
3229
|
+
`);
|
|
3230
|
+
},
|
|
3231
|
+
err: (line) => {
|
|
3232
|
+
process.stderr.write(`${line}
|
|
3233
|
+
`);
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
async function run(argv) {
|
|
3238
|
+
const first = argv[0];
|
|
3239
|
+
if (first === void 0 || first === "help" || first === "--help" || first === "-h") {
|
|
3240
|
+
process.stdout.write(helpText());
|
|
3241
|
+
return 0;
|
|
3242
|
+
}
|
|
3243
|
+
if (first === "version" || first === "--version" || first === "-V") {
|
|
3244
|
+
process.stdout.write(`${packageVersion()}
|
|
3245
|
+
`);
|
|
3246
|
+
return 0;
|
|
3247
|
+
}
|
|
3248
|
+
const command = REGISTRY.get(first);
|
|
3249
|
+
if (command === void 0) {
|
|
3250
|
+
throw new UsageError(
|
|
3251
|
+
`Unknown command: ${first}
|
|
3252
|
+
|
|
3253
|
+
Run 'outlook help' for the command list.`
|
|
3254
|
+
);
|
|
3255
|
+
}
|
|
3256
|
+
const rest = argv.slice(1);
|
|
3257
|
+
const args = parseArgs(rest);
|
|
3258
|
+
if (args.bool("help") || args.bool("h")) {
|
|
3259
|
+
process.stdout.write(`${command.summary}
|
|
3260
|
+
|
|
3261
|
+
${command.usage}
|
|
3262
|
+
`);
|
|
3263
|
+
return 0;
|
|
3264
|
+
}
|
|
3265
|
+
assertKnownFlags(command.name, args.names());
|
|
3266
|
+
const ctx = buildContext(args, command.name);
|
|
3267
|
+
if (ctx.dryRun && !command.mutating) {
|
|
3268
|
+
ctx.err(
|
|
3269
|
+
`note: '${command.name}' does not change anything, so --dry-run has no effect.`
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
await command.handler(ctx);
|
|
3273
|
+
return 0;
|
|
3274
|
+
}
|
|
3275
|
+
function reportFailure(error) {
|
|
3276
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3277
|
+
process.stderr.write(`${redactText(message)}
|
|
3278
|
+
`);
|
|
3279
|
+
return error instanceof OutlookCliError ? error.exitCode : 1;
|
|
3280
|
+
}
|
|
3281
|
+
try {
|
|
3282
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
3283
|
+
} catch (error) {
|
|
3284
|
+
process.exitCode = reportFailure(error);
|
|
3285
|
+
}
|
|
3286
|
+
//# sourceMappingURL=cli.js.map
|