-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.mjs
More file actions
191 lines (179 loc) · 7.41 KB
/
Copy pathcli.mjs
File metadata and controls
191 lines (179 loc) · 7.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env node
// mailpress entry point. Handles arg parsing and routes to the wizard,
// poll loop, or doctor. Single top-level try/catch so unexpected errors
// land in the log instead of crashing silently.
import { createLogger } from "./lib/log.mjs";
import { loadConfig, validateConfig, resolveInRoot, configPath } from "./lib/config.mjs";
const USAGE = `\
mailpress — Outlook-to-printer relay
Usage:
mailpress run setup wizard if not configured, otherwise poll
mailpress --setup (re)run the interactive setup wizard
mailpress --test interactive: test print, switch printer, etc.
mailpress --once process current unread messages and exit
mailpress --doctor run diagnostic checks and exit
mailpress --consent just re-do the OAuth consent flow
mailpress --uninstall remove the Windows scheduled task
mailpress --help this message
mailpress --version print version and exit
Environment:
MAILPRESS_HOME override the install root (where config + logs live)
`;
function parseArgs(argv) {
const flags = new Set(argv.slice(2));
if (flags.has("--help") || flags.has("-h")) return { cmd: "help" };
if (flags.has("--version") || flags.has("-v")) return { cmd: "version" };
if (flags.has("--setup")) return { cmd: "setup" };
if (flags.has("--doctor")) return { cmd: "doctor" };
if (flags.has("--consent")) return { cmd: "consent" };
if (flags.has("--uninstall")) return { cmd: "uninstall" };
if (flags.has("--test")) return { cmd: "test" };
if (flags.has("--once")) return { cmd: "once" };
return { cmd: "auto" };
}
async function main() {
const args = parseArgs(process.argv);
if (args.cmd === "help") {
process.stdout.write(USAGE);
return 0;
}
if (args.cmd === "version") {
process.stdout.write("mailpress 0.2.0\n");
return 0;
}
// Load config first so we know which log file to write to. Wizard mode
// gets its own setup log. Poll mode gets the main log.
let cfg = null;
try { cfg = loadConfig(); } catch (e) {
// Config corrupt — wizard can repair, others should fail loudly.
if (args.cmd !== "setup" && args.cmd !== "doctor" && args.cmd !== "auto") {
process.stderr.write(`config error: ${e.message}\n`);
return 2;
}
}
const isWizard = args.cmd === "setup" || args.cmd === "consent" || (args.cmd === "auto" && !cfg);
const logPath = isWizard
? resolveInRoot(cfg?.setupLogPath || "./mailpress-setup.log")
: resolveInRoot(cfg?.logPath || "./mailpress.log");
const log = createLogger({ filePath: logPath, minLevel: process.env.MAILPRESS_DEBUG ? "DEBUG" : "INFO", pretty: true });
log.debug(`mailpress cli: cmd=${args.cmd} platform=${process.platform} node=${process.versions.node}`);
log.debug(`config path: ${configPath()}`);
log.debug(`log path: ${logPath}`);
try {
switch (args.cmd) {
case "setup": {
const { runWizard } = await import("./lib/wizard.mjs");
await runWizard({ log });
return 0;
}
case "consent": {
const cfgNow = loadConfig();
const errs = validateConfig(cfgNow);
if (errs.length) {
log.error("config invalid:", errs.join("; "));
log.say("Run `mailpress --setup` first.");
return 2;
}
const { runConsent } = await import("./lib/oauth.mjs");
const { MailClient } = await import("./lib/outlook.mjs");
log.heading("OAuth consent");
// Same UPN safeguard as the wizard.
const safeHint = cfgNow.mailAddress && !cfgNow.mailAddress.includes("#EXT#")
? cfgNow.mailAddress : undefined;
const tok = await runConsent({
clientId: cfgNow.oauthClientId,
mailAddress: safeHint,
log,
});
// Verify the just-exchanged token belongs to the configured
// account BEFORE persisting it. Without this, a user re-running
// --consent could silently bind mailpress to a different account
// (browser was signed into a personal Hotmail instead of the
// office Outlook), and the next poll would print someone else's
// email until they noticed.
const verifyClient = new MailClient(cfgNow, { log });
verifyClient.setInMemoryToken(tok.access_token, tok.expires_in);
const prof = await verifyClient.profile();
const addr = prof?.mail || prof?.userPrincipalName || "";
if (!addr) {
log.error("Graph /me returned no mail address; refusing to save token");
return 1;
}
if (cfgNow.mailAddress && addr.toLowerCase() !== cfgNow.mailAddress.toLowerCase()) {
log.error(`signed in as ${addr}, but config says ${cfgNow.mailAddress}`);
log.error("Refusing to save token. Re-run --consent and pick the configured account, or run --setup to change the account.");
return 1;
}
new MailClient(cfgNow, { log }).saveToken(tok);
log.success(`token saved (authenticated as ${addr})`);
return 0;
}
case "doctor": {
const { runDoctor } = await import("./lib/doctor.mjs");
const { failed } = await runDoctor({ log });
return failed === 0 ? 0 : 1;
}
case "uninstall": {
const { uninstallTask } = await import("./lib/task.mjs");
const out = await uninstallTask();
log.success(`scheduled task: ${out}`);
return 0;
}
case "test": {
const { runTest } = await import("./lib/test.mjs");
return await runTest({ log });
}
case "once":
case "auto": {
const { MailClient } = await import("./lib/outlook.mjs");
const needsWizard =
!cfg ||
validateConfig(cfg).length > 0 ||
!new MailClient(cfg, { log }).hasToken();
if (needsWizard) {
// Scheduled Task / cron / piped invocations have no TTY. Refuse
// to launch the interactive wizard in that case — it would hang
// forever waiting for stdin. Exit 2 so the operator can spot
// the failure in the task history.
if (!process.stdin.isTTY) {
log.error("config is missing or invalid, and there is no terminal to run the wizard in.");
log.error("Run `mailpress --setup` from a console to configure.");
return 2;
}
const { runWizard } = await import("./lib/wizard.mjs");
await runWizard({ log });
try { cfg = loadConfig(); }
catch (e) {
log.error("post-wizard config still unreadable:", e.message);
return 2;
}
if (!cfg || validateConfig(cfg).length > 0) {
log.error("setup did not complete; aborting");
return 2;
}
}
const { runPoll } = await import("./lib/poll.mjs");
await runPoll(cfg, { log, runOnce: args.cmd === "once" });
return 0;
}
default:
process.stdout.write(USAGE);
return 1;
}
} catch (e) {
log.fatal(e);
process.stderr.write(`\nFatal: ${e.message}\nSee log: ${logPath}\n`);
// Exit 2 means "re-run --setup or --consent" so Scheduled Task /
// monitoring scripts can distinguish auth failure from generic crashes.
const { MailAuthError } = await import("./lib/outlook.mjs");
if (e instanceof MailAuthError && e.fatal) return 2;
return 1;
}
}
main().then(
(code) => process.exit(code ?? 0),
(e) => {
process.stderr.write(`Unhandled: ${e.stack || e.message}\n`);
process.exit(1);
},
);