fluxmail 0.5.0 → 0.6.1
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/dist/accounts/googleAuth.d.ts +7 -3
- package/dist/accounts/googleAuth.d.ts.map +1 -1
- package/dist/accounts/googleAuth.js +8 -7
- package/dist/accounts/googleAuth.js.map +1 -1
- package/dist/accounts/microsoftAuth.d.ts +5 -2
- package/dist/accounts/microsoftAuth.d.ts.map +1 -1
- package/dist/accounts/microsoftAuth.js +24 -21
- package/dist/accounts/microsoftAuth.js.map +1 -1
- package/dist/accounts/registry.d.ts +3 -1
- package/dist/accounts/registry.d.ts.map +1 -1
- package/dist/accounts/registry.js +12 -6
- package/dist/accounts/registry.js.map +1 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +5 -2
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +382 -65
- package/dist/cli.js.map +1 -1
- package/dist/cliInstances.d.ts +14 -0
- package/dist/cliInstances.d.ts.map +1 -1
- package/dist/cliInstances.js +88 -5
- package/dist/cliInstances.js.map +1 -1
- package/dist/cliMail.d.ts +9 -0
- package/dist/cliMail.d.ts.map +1 -0
- package/dist/cliMail.js +450 -0
- package/dist/cliMail.js.map +1 -0
- package/dist/config.d.ts +89 -118
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +344 -369
- package/dist/config.js.map +1 -1
- package/dist/configMigration.d.ts +8 -0
- package/dist/configMigration.d.ts.map +1 -0
- package/dist/configMigration.js +234 -0
- package/dist/configMigration.js.map +1 -0
- package/dist/context.d.ts +8 -2
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -7
- package/dist/context.js.map +1 -1
- package/dist/deploymentConfig.d.ts +190 -0
- package/dist/deploymentConfig.d.ts.map +1 -0
- package/dist/deploymentConfig.js +539 -0
- package/dist/deploymentConfig.js.map +1 -0
- package/dist/http/admin.d.ts +19 -1
- package/dist/http/admin.d.ts.map +1 -1
- package/dist/http/admin.js +162 -15
- package/dist/http/admin.js.map +1 -1
- package/dist/http/app.d.ts +4 -1
- package/dist/http/app.d.ts.map +1 -1
- package/dist/http/app.js +44 -12
- package/dist/http/app.js.map +1 -1
- package/dist/http/identity.d.ts.map +1 -1
- package/dist/http/identity.js +5 -0
- package/dist/http/identity.js.map +1 -1
- package/dist/http/rest.d.ts +5 -1
- package/dist/http/rest.d.ts.map +1 -1
- package/dist/http/rest.js +76 -6
- package/dist/http/rest.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/instanceConfig.d.ts +36 -0
- package/dist/instanceConfig.d.ts.map +1 -0
- package/dist/instanceConfig.js +157 -0
- package/dist/instanceConfig.js.map +1 -0
- package/dist/licensing/activation.d.ts +2 -1
- package/dist/licensing/activation.d.ts.map +1 -1
- package/dist/licensing/activation.js +2 -3
- package/dist/licensing/activation.js.map +1 -1
- package/dist/licensing/entitlements.d.ts.map +1 -1
- package/dist/licensing/entitlements.js +2 -2
- package/dist/licensing/entitlements.js.map +1 -1
- package/dist/licensing/refresher.d.ts +6 -1
- package/dist/licensing/refresher.d.ts.map +1 -1
- package/dist/licensing/refresher.js +18 -7
- package/dist/licensing/refresher.js.map +1 -1
- package/dist/logging.d.ts +87 -0
- package/dist/logging.d.ts.map +1 -0
- package/dist/logging.js +612 -0
- package/dist/logging.js.map +1 -0
- package/dist/mcp/buildServer.d.ts +2 -0
- package/dist/mcp/buildServer.d.ts.map +1 -1
- package/dist/mcp/buildServer.js +13 -5
- package/dist/mcp/buildServer.js.map +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +3 -3
- package/dist/permissions.js.map +1 -1
- package/dist/scheduler/sendScheduler.d.ts +3 -1
- package/dist/scheduler/sendScheduler.d.ts.map +1 -1
- package/dist/scheduler/sendScheduler.js +14 -1
- package/dist/scheduler/sendScheduler.js.map +1 -1
- package/dist/service/emailService.d.ts +2 -1
- package/dist/service/emailService.d.ts.map +1 -1
- package/dist/service/emailService.js +3 -0
- package/dist/service/emailService.js.map +1 -1
- package/dist/storage/db.d.ts +5 -2
- package/dist/storage/db.d.ts.map +1 -1
- package/dist/storage/db.js +55 -2
- package/dist/storage/db.js.map +1 -1
- package/dist/telemetry.d.ts +3 -2
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +12 -19
- package/dist/telemetry.js.map +1 -1
- package/dist/updateNotifier.d.ts +18 -0
- package/dist/updateNotifier.d.ts.map +1 -0
- package/dist/updateNotifier.js +128 -0
- package/dist/updateNotifier.js.map +1 -0
- package/package.json +37 -5
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { realpathSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
3
3
|
import { emitKeypressEvents } from 'node:readline';
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { hostname } from 'node:os';
|
|
@@ -10,7 +10,8 @@ import { serve } from '@hono/node-server';
|
|
|
10
10
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
11
|
import { EmailError, isEmailError } from '@fluxmail/core';
|
|
12
12
|
import { createContext } from './context.js';
|
|
13
|
-
import {
|
|
13
|
+
import { configTomlPath, createConfigurationService, deploymentToml, readLoggingSettings, resolveDataDir, resolveStoreLocation, writeDeploymentConfig, } from './config.js';
|
|
14
|
+
import { migrateConfigurationFile, parseEnvContent, recognizedMigrationSettings } from './configMigration.js';
|
|
14
15
|
import { createApp } from './http/app.js';
|
|
15
16
|
import { buildMcpServer } from './mcp/buildServer.js';
|
|
16
17
|
import { runLoopbackFlow } from './accounts/googleAuth.js';
|
|
@@ -21,12 +22,16 @@ import { findMember } from './storage/members.js';
|
|
|
21
22
|
import { LICENSE_KEY_PATTERN } from './licensing/client.js';
|
|
22
23
|
import { checkLicenseState, getEntitlements } from './licensing/entitlements.js';
|
|
23
24
|
import { VERSION } from './version.js';
|
|
25
|
+
import { inspectStoreCompatibility, MIN_SUPPORTED_STORE_FORMAT, openDb, openReadonlyDb, } from './storage/db.js';
|
|
24
26
|
import { customPermissionPolicy, ADMIN_CAPABILITIES, FULL_PERMISSION_POLICY, isNamedPermissionProfile, MCP_CAPABILITIES, NAMED_PERMISSION_PROFILES, permissionPolicyForProfile, } from './permissions.js';
|
|
25
|
-
import { captureOperation, getTelemetry, installTelemetryStreamEndHandler, isTelemetryEnabled, setTelemetryEnabled, shutdownTelemetry, } from './telemetry.js';
|
|
27
|
+
import { captureOperation, getTelemetry, installTelemetryStreamEndHandler, isTelemetryEnabled, setTelemetryEnabled, shutdownTelemetry, telemetryDisabledInAnyEnvironment, } from './telemetry.js';
|
|
26
28
|
import { clearSessionToken, instanceClient, loadInstanceConfig, removeInstance, resolveInstance, saveLocalInstance, saveRemoteInstance, saveSessionToken, useInstance, } from './cliInstances.js';
|
|
27
|
-
import { authenticateBearer, recoverAdminPassword, setupInitialAdmin } from './auth.js';
|
|
29
|
+
import { authenticateBearer, normalizeAndValidatePassword, recoverAdminPassword, setupInitialAdmin } from './auth.js';
|
|
28
30
|
import { recordAdminAuditEvent } from './storage/adminAudit.js';
|
|
29
31
|
import { canManageOwnedAccount } from './authorization.js';
|
|
32
|
+
import { createCliUpdateNotifier } from './updateNotifier.js';
|
|
33
|
+
import { registerMailCommands } from './cliMail.js';
|
|
34
|
+
import { flushLogging, escapeLogTextForConsole, getLogger, logCodedFailure, logFailure, readLocalLogs, shutdownLogging, } from './logging.js';
|
|
30
35
|
function collectOption(value, previous) {
|
|
31
36
|
return [...previous, value];
|
|
32
37
|
}
|
|
@@ -81,11 +86,11 @@ function connectionSecurity(value, option) {
|
|
|
81
86
|
}
|
|
82
87
|
async function hiddenPrompt(label) {
|
|
83
88
|
if (!process.stdin.isTTY || !process.stdin.setRawMode) {
|
|
84
|
-
throw new EmailError('invalid_request', `${label}
|
|
89
|
+
throw new EmailError('invalid_request', `${label} requires an interactive terminal or an explicit non-interactive input.`);
|
|
85
90
|
}
|
|
91
|
+
const wasFlowing = process.stdin.readableFlowing === true;
|
|
86
92
|
emitKeypressEvents(process.stdin);
|
|
87
93
|
const wasRaw = process.stdin.isRaw;
|
|
88
|
-
const wasPaused = process.stdin.isPaused();
|
|
89
94
|
process.stdout.write(`${label}: `);
|
|
90
95
|
process.stdin.setRawMode(true);
|
|
91
96
|
process.stdin.resume();
|
|
@@ -94,7 +99,7 @@ async function hiddenPrompt(label) {
|
|
|
94
99
|
const finish = (error) => {
|
|
95
100
|
process.stdin.off('keypress', onKeypress);
|
|
96
101
|
process.stdin.setRawMode(wasRaw);
|
|
97
|
-
if (
|
|
102
|
+
if (!wasFlowing)
|
|
98
103
|
process.stdin.pause();
|
|
99
104
|
process.stdout.write('\n');
|
|
100
105
|
if (error)
|
|
@@ -136,6 +141,25 @@ async function textPrompt(label) {
|
|
|
136
141
|
async function loginPassword(label = 'Password') {
|
|
137
142
|
return process.env.FLUXMAIL_PASSWORD ?? hiddenPrompt(label);
|
|
138
143
|
}
|
|
144
|
+
async function newPassword(label, member, prompt) {
|
|
145
|
+
return withNewPassword(label, member, prompt, async (password) => password);
|
|
146
|
+
}
|
|
147
|
+
async function withNewPassword(label, member, prompt, submit) {
|
|
148
|
+
const configured = process.env.FLUXMAIL_PASSWORD;
|
|
149
|
+
if (configured !== undefined)
|
|
150
|
+
return submit(normalizeAndValidatePassword(configured, member));
|
|
151
|
+
for (;;) {
|
|
152
|
+
const password = await prompt(label);
|
|
153
|
+
try {
|
|
154
|
+
return await submit(normalizeAndValidatePassword(password, member));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (!(error instanceof EmailError) || error.code !== 'invalid_request')
|
|
158
|
+
throw error;
|
|
159
|
+
console.error(`Error: ${error.message}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
139
163
|
async function accountSecret(envName, label) {
|
|
140
164
|
if (!envName)
|
|
141
165
|
return hiddenPrompt(label);
|
|
@@ -144,6 +168,15 @@ async function accountSecret(envName, label) {
|
|
|
144
168
|
throw new EmailError('invalid_request', `${envName} is not set or is empty.`);
|
|
145
169
|
return value;
|
|
146
170
|
}
|
|
171
|
+
function readSecretFile(file) {
|
|
172
|
+
const value = readFileSync(file === '-' ? 0 : file, 'utf8').replace(/\r?\n$/, '');
|
|
173
|
+
if (!value)
|
|
174
|
+
throw new EmailError('invalid_request', 'The secret input is empty.');
|
|
175
|
+
return value;
|
|
176
|
+
}
|
|
177
|
+
async function secretInput(file, label) {
|
|
178
|
+
return file === undefined ? hiddenPrompt(label) : readSecretFile(file);
|
|
179
|
+
}
|
|
147
180
|
function planLine(ent) {
|
|
148
181
|
const members = `${ent.maxMembers} member${ent.maxMembers === 1 ? '' : 's'}`;
|
|
149
182
|
return `${ent.plan} (up to ${ent.maxAccounts} mailboxes, ${members})`;
|
|
@@ -155,11 +188,19 @@ function warnLicense(db, log = console.error) {
|
|
|
155
188
|
log(`Warning: ${warning}`);
|
|
156
189
|
}
|
|
157
190
|
const activeCliOperations = new WeakMap();
|
|
158
|
-
|
|
191
|
+
const activeUpdateNotifiers = new WeakMap();
|
|
192
|
+
function finishCliOperation(program, outcome, errorCode, error) {
|
|
159
193
|
const active = activeCliOperations.get(program);
|
|
160
194
|
if (!active)
|
|
161
195
|
return;
|
|
162
196
|
activeCliOperations.delete(program);
|
|
197
|
+
if (outcome === 'error' && !active.errorLogged) {
|
|
198
|
+
const context = { productSurface: 'cli', operation: active.operation, skipConsole: true };
|
|
199
|
+
if (error !== undefined)
|
|
200
|
+
logFailure(active.logger, 'cli.operation_failed', error, context);
|
|
201
|
+
else
|
|
202
|
+
logCodedFailure(active.logger, 'cli.operation_failed', errorCode ?? 'command_failed', 'CLI command failed', context);
|
|
203
|
+
}
|
|
163
204
|
captureOperation(active.telemetry, {
|
|
164
205
|
productSurface: 'cli',
|
|
165
206
|
operation: active.operation,
|
|
@@ -168,6 +209,17 @@ function finishCliOperation(program, outcome, errorCode) {
|
|
|
168
209
|
errorCode,
|
|
169
210
|
});
|
|
170
211
|
}
|
|
212
|
+
function logActiveCliError(program, error) {
|
|
213
|
+
const active = activeCliOperations.get(program);
|
|
214
|
+
if (!active || active.errorLogged)
|
|
215
|
+
return;
|
|
216
|
+
active.errorLogged = true;
|
|
217
|
+
logFailure(active.logger, 'cli.operation_failed', error, {
|
|
218
|
+
productSurface: 'cli',
|
|
219
|
+
operation: active.operation,
|
|
220
|
+
skipConsole: true,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
171
223
|
export function waitForServerListening(server) {
|
|
172
224
|
return new Promise((resolve, reject) => {
|
|
173
225
|
const onError = (error) => {
|
|
@@ -182,15 +234,39 @@ export function waitForServerListening(server) {
|
|
|
182
234
|
server.once('listening', onListening);
|
|
183
235
|
});
|
|
184
236
|
}
|
|
237
|
+
async function shutdownTelemetryThenLogging() {
|
|
238
|
+
try {
|
|
239
|
+
await shutdownTelemetry();
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
await shutdownLogging();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export async function shutdownTelemetryAndLogging() {
|
|
246
|
+
const initialLogFlush = flushLogging().catch(() => { });
|
|
247
|
+
try {
|
|
248
|
+
await shutdownTelemetry();
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
await initialLogFlush;
|
|
252
|
+
await shutdownLogging();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
export function installStdioShutdownHandler(stream) {
|
|
256
|
+
installTelemetryStreamEndHandler(stream, shutdownTelemetryThenLogging);
|
|
257
|
+
}
|
|
185
258
|
/** Build the command tree without parsing arguments or running command actions. */
|
|
186
259
|
export function createCliProgram(options = {}) {
|
|
187
260
|
const program = new Command();
|
|
188
261
|
program
|
|
189
262
|
.name('fluxmail')
|
|
190
|
-
.description('Fluxmail, a self-hosted MCP
|
|
263
|
+
.description('Fluxmail, a self-hosted email API with MCP, REST, and CLI access')
|
|
191
264
|
.option('--instance <name>', 'Use a named local or remote instance')
|
|
265
|
+
.option('-a, --mail-account <id-or-email>', 'Use an email account by ID or address')
|
|
266
|
+
.option('--no-update-notifier', 'Skip the automatic update check for this command')
|
|
192
267
|
.version(VERSION, '-v, --version');
|
|
193
268
|
const selectedInstance = () => program.opts().instance;
|
|
269
|
+
const selectedAccount = () => program.opts().mailAccount;
|
|
194
270
|
function commandPath(command) {
|
|
195
271
|
const names = [];
|
|
196
272
|
for (let current = command; current?.parent; current = current.parent)
|
|
@@ -199,7 +275,7 @@ export function createCliProgram(options = {}) {
|
|
|
199
275
|
}
|
|
200
276
|
let telemetrySignalHandlersInstalled = false;
|
|
201
277
|
const TELEMETRY_SIGNAL_SHUTDOWN_TIMEOUT_MS = 1_000;
|
|
202
|
-
/** Flush queued telemetry before restoring Node's default signal behavior. */
|
|
278
|
+
/** Flush queued telemetry and local logs before restoring Node's default signal behavior. */
|
|
203
279
|
function installTelemetrySignalHandlers() {
|
|
204
280
|
if (telemetrySignalHandlersInstalled)
|
|
205
281
|
return;
|
|
@@ -216,7 +292,7 @@ export function createCliProgram(options = {}) {
|
|
|
216
292
|
process.kill(process.pid, signal);
|
|
217
293
|
};
|
|
218
294
|
const timeout = setTimeout(resignal, TELEMETRY_SIGNAL_SHUTDOWN_TIMEOUT_MS);
|
|
219
|
-
void
|
|
295
|
+
void shutdownTelemetryAndLogging().finally(() => {
|
|
220
296
|
clearTimeout(timeout);
|
|
221
297
|
resignal();
|
|
222
298
|
});
|
|
@@ -230,14 +306,57 @@ export function createCliProgram(options = {}) {
|
|
|
230
306
|
}
|
|
231
307
|
program.hook('preAction', (_command, actionCommand) => {
|
|
232
308
|
const command = commandPath(actionCommand);
|
|
309
|
+
const updateNotifierEnabled = program.opts().updateNotifier;
|
|
310
|
+
if (updateNotifierEnabled && actionCommand.name() !== 'stdio') {
|
|
311
|
+
try {
|
|
312
|
+
const notifier = options.updateNotifierFactory?.();
|
|
313
|
+
if (notifier)
|
|
314
|
+
activeUpdateNotifiers.set(program, notifier);
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Update checks must not affect command execution.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
233
320
|
// Respect the opt-out before creating a telemetry client or recording this command.
|
|
234
321
|
if (command === 'telemetry disable')
|
|
235
322
|
return;
|
|
323
|
+
let telemetryEnvironment;
|
|
324
|
+
let telemetryDataDir;
|
|
325
|
+
if (command === 'config migrate') {
|
|
326
|
+
const migration = actionCommand.opts();
|
|
327
|
+
if (migration.from) {
|
|
328
|
+
try {
|
|
329
|
+
const values = parseEnvContent(readFileSync(path.resolve(migration.from), 'utf8'));
|
|
330
|
+
if (values.FLUXMAIL_DATA_DIR !== undefined) {
|
|
331
|
+
telemetryDataDir = resolveDataDir({
|
|
332
|
+
env: { ...process.env, FLUXMAIL_DATA_DIR: values.FLUXMAIL_DATA_DIR },
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (telemetryDisabledInAnyEnvironment(process.env, values)) {
|
|
336
|
+
telemetryEnvironment = { ...process.env, FLUXMAIL_TELEMETRY: '0' };
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
// The command action reports file and parsing errors.
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const dataDir = telemetryDataDir ?? resolveDataDir();
|
|
345
|
+
let logging;
|
|
346
|
+
try {
|
|
347
|
+
logging = readLoggingSettings(dataDir);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
// Keep recovery and remote commands available when logging configuration is invalid.
|
|
351
|
+
logging = { level: 'info', destination: 'both' };
|
|
352
|
+
}
|
|
353
|
+
const mode = actionCommand.name() === 'serve' ? 'serve' : actionCommand.name() === 'stdio' ? 'stdio' : 'cli';
|
|
236
354
|
activeCliOperations.set(program, {
|
|
237
|
-
telemetry: options.telemetry ?? getTelemetry(
|
|
355
|
+
telemetry: options.telemetry ?? getTelemetry(dataDir, telemetryEnvironment),
|
|
238
356
|
operation: command,
|
|
239
357
|
startedAt: performance.now(),
|
|
240
358
|
initialExitCode: process.exitCode,
|
|
359
|
+
logger: getLogger(dataDir, mode, logging),
|
|
241
360
|
});
|
|
242
361
|
});
|
|
243
362
|
program.hook('postAction', async (_command, actionCommand) => {
|
|
@@ -247,8 +366,20 @@ export function createCliProgram(options = {}) {
|
|
|
247
366
|
process.exitCode !== undefined &&
|
|
248
367
|
process.exitCode !== 0;
|
|
249
368
|
finishCliOperation(program, failed ? 'error' : 'success', failed ? 'command_failed' : undefined);
|
|
250
|
-
|
|
251
|
-
|
|
369
|
+
try {
|
|
370
|
+
if (actionCommand.name() !== 'serve' && actionCommand.name() !== 'stdio') {
|
|
371
|
+
await Promise.all([options.telemetry ? Promise.resolve() : shutdownTelemetry(), shutdownLogging()]);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
finally {
|
|
375
|
+
const notifier = activeUpdateNotifiers.get(program);
|
|
376
|
+
activeUpdateNotifiers.delete(program);
|
|
377
|
+
try {
|
|
378
|
+
notifier?.notify();
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// Update notices must not affect the command outcome.
|
|
382
|
+
}
|
|
252
383
|
}
|
|
253
384
|
});
|
|
254
385
|
program
|
|
@@ -261,8 +392,11 @@ export function createCliProgram(options = {}) {
|
|
|
261
392
|
try {
|
|
262
393
|
const name = opts.name ?? (opts.existingAdmin ? undefined : await textPrompt('Administrator name'));
|
|
263
394
|
const email = opts.email ?? (await textPrompt('Administrator email'));
|
|
264
|
-
const password = await loginPassword('New password');
|
|
265
395
|
const context = createContext();
|
|
396
|
+
const passwordMember = opts.existingAdmin
|
|
397
|
+
? { ...findMember(context.db, opts.existingAdmin), email }
|
|
398
|
+
: { name: name ?? '', email };
|
|
399
|
+
const password = await newPassword('New password', passwordMember, options.passwordPrompt ?? hiddenPrompt);
|
|
266
400
|
const result = await setupInitialAdmin(context.db, {
|
|
267
401
|
...(name ? { name } : {}),
|
|
268
402
|
email,
|
|
@@ -281,6 +415,7 @@ export function createCliProgram(options = {}) {
|
|
|
281
415
|
console.log(`Fluxmail is ready. Logged in to local as ${result.member.name}.`);
|
|
282
416
|
}
|
|
283
417
|
catch (err) {
|
|
418
|
+
logActiveCliError(program, err);
|
|
284
419
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
285
420
|
process.exitCode = 1;
|
|
286
421
|
}
|
|
@@ -307,16 +442,15 @@ export function createCliProgram(options = {}) {
|
|
|
307
442
|
const client = instanceClient(instanceName, false);
|
|
308
443
|
const deviceName = `CLI on ${hostname()}`;
|
|
309
444
|
const result = opts.enroll || opts.reset
|
|
310
|
-
? await
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
}, false)
|
|
445
|
+
? await (async () => {
|
|
446
|
+
const token = (opts.reset ? process.env.FLUXMAIL_RESET_CODE : process.env.FLUXMAIL_INVITE_CODE) ??
|
|
447
|
+
(await hiddenPrompt(opts.reset ? 'Password reset code' : 'Enrollment code'));
|
|
448
|
+
return withNewPassword('New password', undefined, options.passwordPrompt ?? hiddenPrompt, (password) => client.json(opts.reset ? '/api/v1/auth/password-reset' : '/api/v1/auth/enroll', {
|
|
449
|
+
method: 'POST',
|
|
450
|
+
headers: { 'content-type': 'application/json' },
|
|
451
|
+
body: JSON.stringify({ token, password, deviceName }),
|
|
452
|
+
}, false));
|
|
453
|
+
})()
|
|
320
454
|
: await client.json('/api/v1/auth/login', {
|
|
321
455
|
method: 'POST',
|
|
322
456
|
headers: { 'content-type': 'application/json' },
|
|
@@ -331,6 +465,7 @@ export function createCliProgram(options = {}) {
|
|
|
331
465
|
console.log(`Logged in to ${instanceName} as ${result.member.name}.`);
|
|
332
466
|
}
|
|
333
467
|
catch (err) {
|
|
468
|
+
logActiveCliError(program, err);
|
|
334
469
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
335
470
|
process.exitCode = 1;
|
|
336
471
|
}
|
|
@@ -349,6 +484,7 @@ export function createCliProgram(options = {}) {
|
|
|
349
484
|
console.log(`Logged out of ${selected.name}.`);
|
|
350
485
|
}
|
|
351
486
|
catch (err) {
|
|
487
|
+
logActiveCliError(program, err);
|
|
352
488
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
353
489
|
process.exitCode = 1;
|
|
354
490
|
}
|
|
@@ -373,6 +509,7 @@ export function createCliProgram(options = {}) {
|
|
|
373
509
|
console.log(`Using ${name}.`);
|
|
374
510
|
}
|
|
375
511
|
catch (err) {
|
|
512
|
+
logActiveCliError(program, err);
|
|
376
513
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
377
514
|
process.exitCode = 1;
|
|
378
515
|
}
|
|
@@ -387,6 +524,7 @@ export function createCliProgram(options = {}) {
|
|
|
387
524
|
console.log(`Removed CLI instance ${name}. Server data was not changed.`);
|
|
388
525
|
}
|
|
389
526
|
catch (err) {
|
|
527
|
+
logActiveCliError(program, err);
|
|
390
528
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
391
529
|
process.exitCode = 1;
|
|
392
530
|
}
|
|
@@ -402,8 +540,9 @@ export function createCliProgram(options = {}) {
|
|
|
402
540
|
if (selected.profile.kind !== 'local') {
|
|
403
541
|
throw new EmailError('invalid_request', 'Administrator recovery is only available on the local instance host.');
|
|
404
542
|
}
|
|
405
|
-
const password = await loginPassword('New password');
|
|
406
543
|
const context = createContext();
|
|
544
|
+
const target = findMember(context.db, memberRef);
|
|
545
|
+
const password = await newPassword('New password', target, options.passwordPrompt ?? hiddenPrompt);
|
|
407
546
|
const member = await recoverAdminPassword(context.db, memberRef, password);
|
|
408
547
|
recordAdminAuditEvent(context.db, {
|
|
409
548
|
operation: 'auth.recover_admin',
|
|
@@ -415,6 +554,7 @@ export function createCliProgram(options = {}) {
|
|
|
415
554
|
console.log(`Reset ${member.name}'s password and revoked existing sessions.`);
|
|
416
555
|
}
|
|
417
556
|
catch (err) {
|
|
557
|
+
logActiveCliError(program, err);
|
|
418
558
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
419
559
|
process.exitCode = 1;
|
|
420
560
|
}
|
|
@@ -430,6 +570,7 @@ export function createCliProgram(options = {}) {
|
|
|
430
570
|
}
|
|
431
571
|
}
|
|
432
572
|
catch (err) {
|
|
573
|
+
logActiveCliError(program, err);
|
|
433
574
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
434
575
|
process.exitCode = 1;
|
|
435
576
|
}
|
|
@@ -446,6 +587,7 @@ export function createCliProgram(options = {}) {
|
|
|
446
587
|
console.log(`Revoked ${sessionId}.`);
|
|
447
588
|
}
|
|
448
589
|
catch (err) {
|
|
590
|
+
logActiveCliError(program, err);
|
|
449
591
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
450
592
|
process.exitCode = 1;
|
|
451
593
|
}
|
|
@@ -459,6 +601,10 @@ export function createCliProgram(options = {}) {
|
|
|
459
601
|
const app = createApp(ctx);
|
|
460
602
|
ctx.scheduler.start();
|
|
461
603
|
const server = serve({ fetch: app.fetch, port: ctx.config.port }, () => {
|
|
604
|
+
ctx.logger.info('server.started', 'Fluxmail HTTP server started', {
|
|
605
|
+
details: { port: ctx.config.port },
|
|
606
|
+
skipConsole: true,
|
|
607
|
+
});
|
|
462
608
|
ctx.telemetry.capture('mcp server started', { product_surface: 'mcp', transport: 'http' });
|
|
463
609
|
console.log(`Fluxmail listening on ${ctx.config.publicUrl}`);
|
|
464
610
|
console.log(` MCP endpoint: ${ctx.config.publicUrl}/mcp`);
|
|
@@ -478,7 +624,7 @@ export function createCliProgram(options = {}) {
|
|
|
478
624
|
}
|
|
479
625
|
console.log(` Plan: ${planLine(getEntitlements(ctx.db))}`);
|
|
480
626
|
warnLicense(ctx.db, console.log);
|
|
481
|
-
ctx.licenseController.start(
|
|
627
|
+
ctx.licenseController.start();
|
|
482
628
|
});
|
|
483
629
|
await waitForServerListening(server);
|
|
484
630
|
});
|
|
@@ -490,7 +636,7 @@ export function createCliProgram(options = {}) {
|
|
|
490
636
|
.option('--allow <capability>', 'Allow one MCP capability; repeat as needed', collectOption, [])
|
|
491
637
|
.action(async (opts) => {
|
|
492
638
|
installTelemetrySignalHandlers();
|
|
493
|
-
|
|
639
|
+
installStdioShutdownHandler(process.stdin);
|
|
494
640
|
const ctx = createContext();
|
|
495
641
|
const permissions = permissionPolicyFromOptions(opts);
|
|
496
642
|
const selected = resolveInstance(selectedInstance());
|
|
@@ -509,12 +655,18 @@ export function createCliProgram(options = {}) {
|
|
|
509
655
|
maxAttachmentBytes: ctx.config.maxAttachmentBytes,
|
|
510
656
|
telemetry: ctx.telemetry,
|
|
511
657
|
transport: 'stdio',
|
|
658
|
+
logger: ctx.logger,
|
|
512
659
|
});
|
|
513
660
|
ctx.scheduler.start();
|
|
514
661
|
await server.connect(new StdioServerTransport());
|
|
515
662
|
ctx.telemetry.capture('mcp server started', { product_surface: 'mcp', transport: 'stdio' });
|
|
663
|
+
ctx.logger.info('server.started', 'Fluxmail stdio MCP server started', {
|
|
664
|
+
productSurface: 'mcp',
|
|
665
|
+
details: { transport: 'stdio' },
|
|
666
|
+
skipConsole: true,
|
|
667
|
+
});
|
|
516
668
|
// stdout belongs to the MCP protocol; log to stderr only.
|
|
517
|
-
ctx.licenseController.start(
|
|
669
|
+
ctx.licenseController.start();
|
|
518
670
|
console.error('Fluxmail MCP server running on stdio');
|
|
519
671
|
warnLicense(ctx.db);
|
|
520
672
|
const pending = scopedService.listScheduled().filter((send) => send.status === 'pending').length;
|
|
@@ -565,6 +717,7 @@ export function createCliProgram(options = {}) {
|
|
|
565
717
|
validateAccountConnectionFlags(provider, opts);
|
|
566
718
|
}
|
|
567
719
|
catch (err) {
|
|
720
|
+
logActiveCliError(program, err);
|
|
568
721
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
569
722
|
process.exitCode = 1;
|
|
570
723
|
return;
|
|
@@ -575,6 +728,7 @@ export function createCliProgram(options = {}) {
|
|
|
575
728
|
useControlPlane = selectGmailConnectionMode(createContext().config, opts) === 'hosted';
|
|
576
729
|
}
|
|
577
730
|
catch (err) {
|
|
731
|
+
logActiveCliError(program, err);
|
|
578
732
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
579
733
|
process.exitCode = 1;
|
|
580
734
|
return;
|
|
@@ -644,6 +798,7 @@ export function createCliProgram(options = {}) {
|
|
|
644
798
|
}
|
|
645
799
|
}
|
|
646
800
|
catch (err) {
|
|
801
|
+
logActiveCliError(program, err);
|
|
647
802
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
648
803
|
process.exitCode = 1;
|
|
649
804
|
}
|
|
@@ -745,7 +900,7 @@ export function createCliProgram(options = {}) {
|
|
|
745
900
|
throw new EmailError('invalid_request', `Google authorized ${result.email}, but account ${existing.id} belongs to ${existing.email}. ` +
|
|
746
901
|
'Try again and choose the matching Google account.');
|
|
747
902
|
}
|
|
748
|
-
return ctx.registry.addGmailAccount(result.email, result.tokens, result.displayName, owner.id, access);
|
|
903
|
+
return ctx.registry.addGmailAccount(result.email, result.tokens, result.displayName, owner.id, access, result.oauthClient);
|
|
749
904
|
});
|
|
750
905
|
auditConnection('account.connection.complete', account.id);
|
|
751
906
|
console.log(`\nConnected ${account.email} (account id: ${account.id})` +
|
|
@@ -757,7 +912,7 @@ export function createCliProgram(options = {}) {
|
|
|
757
912
|
}
|
|
758
913
|
}
|
|
759
914
|
catch (err) {
|
|
760
|
-
finishCliOperation(program, 'error', isEmailError(err) ? err.code : 'internal');
|
|
915
|
+
finishCliOperation(program, 'error', isEmailError(err) ? err.code : 'internal', err);
|
|
761
916
|
console.error(`\nError: ${err instanceof Error ? err.message : String(err)}`);
|
|
762
917
|
process.exitCode = 1;
|
|
763
918
|
}
|
|
@@ -798,6 +953,7 @@ export function createCliProgram(options = {}) {
|
|
|
798
953
|
console.log(`Warning: ${warning.message}.`);
|
|
799
954
|
}
|
|
800
955
|
catch (err) {
|
|
956
|
+
logActiveCliError(program, err);
|
|
801
957
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
802
958
|
process.exitCode = 1;
|
|
803
959
|
}
|
|
@@ -822,6 +978,7 @@ export function createCliProgram(options = {}) {
|
|
|
822
978
|
}
|
|
823
979
|
}
|
|
824
980
|
catch (err) {
|
|
981
|
+
logActiveCliError(program, err);
|
|
825
982
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
826
983
|
process.exitCode = 1;
|
|
827
984
|
}
|
|
@@ -838,6 +995,7 @@ export function createCliProgram(options = {}) {
|
|
|
838
995
|
console.log(`Removed ${accountId}`);
|
|
839
996
|
}
|
|
840
997
|
catch (err) {
|
|
998
|
+
logActiveCliError(program, err);
|
|
841
999
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
842
1000
|
process.exitCode = 1;
|
|
843
1001
|
}
|
|
@@ -857,6 +1015,7 @@ export function createCliProgram(options = {}) {
|
|
|
857
1015
|
console.log(`${account.email} is now owned by ${account.ownerMemberId}.`);
|
|
858
1016
|
}
|
|
859
1017
|
catch (err) {
|
|
1018
|
+
logActiveCliError(program, err);
|
|
860
1019
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
861
1020
|
process.exitCode = 1;
|
|
862
1021
|
}
|
|
@@ -882,6 +1041,7 @@ export function createCliProgram(options = {}) {
|
|
|
882
1041
|
console.log(`Updated access for ${account.email}: ${account.sharedWithAll ? 'all members' : `${account.grantedMemberIds.length} explicit grant(s)`}.`);
|
|
883
1042
|
}
|
|
884
1043
|
catch (err) {
|
|
1044
|
+
logActiveCliError(program, err);
|
|
885
1045
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
886
1046
|
process.exitCode = 1;
|
|
887
1047
|
}
|
|
@@ -908,6 +1068,7 @@ export function createCliProgram(options = {}) {
|
|
|
908
1068
|
console.log(`Expires: ${new Date(member.invitation.expiresAt).toISOString()}`);
|
|
909
1069
|
}
|
|
910
1070
|
catch (err) {
|
|
1071
|
+
logActiveCliError(program, err);
|
|
911
1072
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
912
1073
|
process.exitCode = 1;
|
|
913
1074
|
}
|
|
@@ -927,6 +1088,7 @@ export function createCliProgram(options = {}) {
|
|
|
927
1088
|
}
|
|
928
1089
|
}
|
|
929
1090
|
catch (err) {
|
|
1091
|
+
logActiveCliError(program, err);
|
|
930
1092
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
931
1093
|
process.exitCode = 1;
|
|
932
1094
|
}
|
|
@@ -943,6 +1105,7 @@ export function createCliProgram(options = {}) {
|
|
|
943
1105
|
console.log(`Removed ${memberId}.`);
|
|
944
1106
|
}
|
|
945
1107
|
catch (err) {
|
|
1108
|
+
logActiveCliError(program, err);
|
|
946
1109
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
947
1110
|
process.exitCode = 1;
|
|
948
1111
|
}
|
|
@@ -965,6 +1128,7 @@ export function createCliProgram(options = {}) {
|
|
|
965
1128
|
console.log(`${member.name} now has the ${member.role} role.`);
|
|
966
1129
|
}
|
|
967
1130
|
catch (err) {
|
|
1131
|
+
logActiveCliError(program, err);
|
|
968
1132
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
969
1133
|
process.exitCode = 1;
|
|
970
1134
|
}
|
|
@@ -987,6 +1151,7 @@ export function createCliProgram(options = {}) {
|
|
|
987
1151
|
console.log(`${member.name} is now ${member.status}.`);
|
|
988
1152
|
}
|
|
989
1153
|
catch (err) {
|
|
1154
|
+
logActiveCliError(program, err);
|
|
990
1155
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
991
1156
|
process.exitCode = 1;
|
|
992
1157
|
}
|
|
@@ -1006,6 +1171,7 @@ export function createCliProgram(options = {}) {
|
|
|
1006
1171
|
console.log(`Expires: ${new Date(token.expiresAt).toISOString()}`);
|
|
1007
1172
|
}
|
|
1008
1173
|
catch (err) {
|
|
1174
|
+
logActiveCliError(program, err);
|
|
1009
1175
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1010
1176
|
process.exitCode = 1;
|
|
1011
1177
|
}
|
|
@@ -1023,6 +1189,7 @@ export function createCliProgram(options = {}) {
|
|
|
1023
1189
|
}
|
|
1024
1190
|
}
|
|
1025
1191
|
catch (err) {
|
|
1192
|
+
logActiveCliError(program, err);
|
|
1026
1193
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1027
1194
|
process.exitCode = 1;
|
|
1028
1195
|
}
|
|
@@ -1038,6 +1205,7 @@ export function createCliProgram(options = {}) {
|
|
|
1038
1205
|
console.log(`Revoked ${sessionId}.`);
|
|
1039
1206
|
}
|
|
1040
1207
|
catch (err) {
|
|
1208
|
+
logActiveCliError(program, err);
|
|
1041
1209
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1042
1210
|
process.exitCode = 1;
|
|
1043
1211
|
}
|
|
@@ -1094,6 +1262,7 @@ export function createCliProgram(options = {}) {
|
|
|
1094
1262
|
console.log('Store it now; it cannot be shown again.');
|
|
1095
1263
|
}
|
|
1096
1264
|
catch (err) {
|
|
1265
|
+
logActiveCliError(program, err);
|
|
1097
1266
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1098
1267
|
process.exitCode = 1;
|
|
1099
1268
|
}
|
|
@@ -1120,6 +1289,7 @@ export function createCliProgram(options = {}) {
|
|
|
1120
1289
|
}
|
|
1121
1290
|
}
|
|
1122
1291
|
catch (err) {
|
|
1292
|
+
logActiveCliError(program, err);
|
|
1123
1293
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1124
1294
|
process.exitCode = 1;
|
|
1125
1295
|
}
|
|
@@ -1146,6 +1316,7 @@ export function createCliProgram(options = {}) {
|
|
|
1146
1316
|
: `Updated ${keyId} to ${accountIds.length} mailbox(es).`);
|
|
1147
1317
|
}
|
|
1148
1318
|
catch (err) {
|
|
1319
|
+
logActiveCliError(program, err);
|
|
1149
1320
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1150
1321
|
process.exitCode = 1;
|
|
1151
1322
|
}
|
|
@@ -1180,6 +1351,7 @@ export function createCliProgram(options = {}) {
|
|
|
1180
1351
|
console.log(`Updated ${keyId} to profile ${permissions.profile}`);
|
|
1181
1352
|
}
|
|
1182
1353
|
catch (err) {
|
|
1354
|
+
logActiveCliError(program, err);
|
|
1183
1355
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1184
1356
|
process.exitCode = 1;
|
|
1185
1357
|
}
|
|
@@ -1199,6 +1371,7 @@ export function createCliProgram(options = {}) {
|
|
|
1199
1371
|
console.log(`Revoked ${keyId}`);
|
|
1200
1372
|
}
|
|
1201
1373
|
catch (err) {
|
|
1374
|
+
logActiveCliError(program, err);
|
|
1202
1375
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1203
1376
|
process.exitCode = 1;
|
|
1204
1377
|
}
|
|
@@ -1206,9 +1379,19 @@ export function createCliProgram(options = {}) {
|
|
|
1206
1379
|
const license = program.command('license').description('Manage the paid-tier license');
|
|
1207
1380
|
license
|
|
1208
1381
|
.command('activate')
|
|
1209
|
-
.argument('
|
|
1382
|
+
.argument('[key]', 'Deprecated: license key shown once at purchase')
|
|
1383
|
+
.option('--key-file <path>', 'Read the license key from a file, or use - for stdin')
|
|
1210
1384
|
.description('Store a license key and validate it with the license server')
|
|
1211
|
-
.action(async (
|
|
1385
|
+
.action(async (deprecatedKey, options) => {
|
|
1386
|
+
if (deprecatedKey && options.keyFile) {
|
|
1387
|
+
console.error('Error: The deprecated positional key cannot be combined with --key-file.');
|
|
1388
|
+
process.exitCode = 1;
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (deprecatedKey) {
|
|
1392
|
+
console.error('Warning: Passing a license key as an argument is deprecated. Use --key-file or hidden input.');
|
|
1393
|
+
}
|
|
1394
|
+
const key = deprecatedKey ?? (await secretInput(options.keyFile, 'License key'));
|
|
1212
1395
|
if (!LICENSE_KEY_PATTERN.test(key)) {
|
|
1213
1396
|
console.error('That does not look like a Fluxmail license key (expected "fluxmail_lic_" followed by 40 hex characters).');
|
|
1214
1397
|
process.exitCode = 1;
|
|
@@ -1230,6 +1413,7 @@ export function createCliProgram(options = {}) {
|
|
|
1230
1413
|
}
|
|
1231
1414
|
}
|
|
1232
1415
|
catch (err) {
|
|
1416
|
+
logActiveCliError(program, err);
|
|
1233
1417
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1234
1418
|
process.exitCode = 1;
|
|
1235
1419
|
}
|
|
@@ -1242,7 +1426,7 @@ export function createCliProgram(options = {}) {
|
|
|
1242
1426
|
const status = await instanceClient(selectedInstance()).json('/api/v1/admin/license');
|
|
1243
1427
|
console.log(status.configured
|
|
1244
1428
|
? `License configured from ${status.source}.`
|
|
1245
|
-
: 'No license key configured. Run "fluxmail license activate
|
|
1429
|
+
: 'No license key configured. Run "fluxmail license activate" after purchasing one.');
|
|
1246
1430
|
console.log(`Current plan: ${planLine(status.entitlements)}`);
|
|
1247
1431
|
console.log(`Usage: ${status.usage.accounts} mailbox(es), ${status.usage.members} member(s)` +
|
|
1248
1432
|
(status.usage.overQuota ? ' (over plan limits)' : ''));
|
|
@@ -1252,6 +1436,7 @@ export function createCliProgram(options = {}) {
|
|
|
1252
1436
|
console.log(`Warning: ${status.warning}`);
|
|
1253
1437
|
}
|
|
1254
1438
|
catch (err) {
|
|
1439
|
+
logActiveCliError(program, err);
|
|
1255
1440
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1256
1441
|
process.exitCode = 1;
|
|
1257
1442
|
}
|
|
@@ -1268,51 +1453,174 @@ export function createCliProgram(options = {}) {
|
|
|
1268
1453
|
console.log('This instance is back to Personal plan limits.');
|
|
1269
1454
|
}
|
|
1270
1455
|
catch (err) {
|
|
1456
|
+
logActiveCliError(program, err);
|
|
1271
1457
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1272
1458
|
process.exitCode = 1;
|
|
1273
1459
|
}
|
|
1274
1460
|
});
|
|
1275
|
-
const configCmd = program
|
|
1276
|
-
.command('config')
|
|
1277
|
-
.description('Persistent settings stored in the data dir, usable from any directory');
|
|
1461
|
+
const configCmd = program.command('config').description('Inspect and initialize deployment configuration');
|
|
1278
1462
|
configCmd
|
|
1279
|
-
.command('
|
|
1280
|
-
.
|
|
1281
|
-
.
|
|
1282
|
-
.description('Store a setting (shell env vars and local .env files still take precedence)')
|
|
1283
|
-
.action((key, value) => {
|
|
1463
|
+
.command('init')
|
|
1464
|
+
.description('Create config.toml in the Fluxmail data directory')
|
|
1465
|
+
.action(() => {
|
|
1284
1466
|
try {
|
|
1285
1467
|
const dataDir = resolveDataDir();
|
|
1286
|
-
|
|
1287
|
-
|
|
1468
|
+
const file = configTomlPath(dataDir);
|
|
1469
|
+
writeDeploymentConfig(file, deploymentToml(), { exclusive: true });
|
|
1470
|
+
console.log(`Created ${file}`);
|
|
1288
1471
|
}
|
|
1289
1472
|
catch (err) {
|
|
1473
|
+
logActiveCliError(program, err);
|
|
1290
1474
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1291
1475
|
process.exitCode = 1;
|
|
1292
1476
|
}
|
|
1293
1477
|
});
|
|
1294
1478
|
configCmd
|
|
1295
|
-
.command('
|
|
1296
|
-
.
|
|
1297
|
-
.
|
|
1298
|
-
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1479
|
+
.command('show')
|
|
1480
|
+
.description('Show effective configuration, sources, paths, and restart requirements')
|
|
1481
|
+
.action(() => {
|
|
1482
|
+
const deployment = resolveStoreLocation().deployment;
|
|
1483
|
+
const compatibility = inspectStoreCompatibility(deployment.dbPath, deployment.dataDir);
|
|
1484
|
+
const canReadInstanceSettings = compatibility.storeFormat >= MIN_SUPPORTED_STORE_FORMAT &&
|
|
1485
|
+
compatibility.storeFormat <= compatibility.maximumSupportedFormat;
|
|
1486
|
+
const db = canReadInstanceSettings && deployment.dbPath !== ':memory:'
|
|
1487
|
+
? openReadonlyDb(deployment.dbPath)
|
|
1488
|
+
: openDb(':memory:', { backupBeforeMigration: false });
|
|
1489
|
+
try {
|
|
1490
|
+
const configuration = createConfigurationService(deployment, db);
|
|
1491
|
+
const config = configuration.config;
|
|
1492
|
+
console.log(`Data directory: ${deployment.dataDir} [${deployment.sources.dataDir}]`);
|
|
1493
|
+
console.log(`Configuration file: ${deployment.configFile}${existsSync(deployment.configFile) ? '' : ' (not created)'}`);
|
|
1494
|
+
console.log(`Database: ${deployment.dbPath} [${deployment.sources.dbPath}] restart required`);
|
|
1495
|
+
console.log(`Server port: ${deployment.port} [${deployment.sources.port}] restart required`);
|
|
1496
|
+
console.log(`Public URL: ${deployment.publicUrl} [${deployment.sources.publicUrl}] restart required`);
|
|
1497
|
+
console.log(`Trust proxy: ${deployment.trustProxy} [${deployment.sources.trustProxy}] restart required`);
|
|
1498
|
+
console.log(`Local OAuth host: ${deployment.oauthHost} [${deployment.sources.oauthHost}] restart required`);
|
|
1499
|
+
console.log(`Local OAuth port: ${deployment.oauthPort} [${deployment.sources.oauthPort}] restart required`);
|
|
1500
|
+
console.log(`Maximum attachment: ${deployment.maxAttachmentBytes / (1024 * 1024)} MB [${deployment.sources.maxAttachmentBytes}] restart required`);
|
|
1501
|
+
console.log(`Encryption key: ${deployment.encryptionKey.length === 32 ? 'configured' : 'not configured'} [${deployment.sources.encryptionKey}]`);
|
|
1502
|
+
const oauth = configuration.oauthStatus();
|
|
1503
|
+
console.log(`Google OAuth: configured [${oauth.google.source}]`);
|
|
1504
|
+
console.log(`Outlook OAuth: ${oauth.outlook.clientId ? 'configured' : 'not configured'} [${oauth.outlook.source ?? 'none'}]`);
|
|
1505
|
+
console.log(`License: ${config.licenseKey ? 'configured' : 'not configured'} [${configuration.licenseSource() ?? 'none'}]`);
|
|
1506
|
+
}
|
|
1507
|
+
finally {
|
|
1508
|
+
db.$client.close();
|
|
1509
|
+
}
|
|
1301
1510
|
});
|
|
1302
1511
|
configCmd
|
|
1303
|
-
.command('
|
|
1304
|
-
.
|
|
1305
|
-
.
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
const
|
|
1309
|
-
if (
|
|
1310
|
-
|
|
1512
|
+
.command('migrate')
|
|
1513
|
+
.requiredOption('--from <env-file>', 'Read recognized settings from this env file')
|
|
1514
|
+
.option('--dry-run', 'Show recognized setting names without changing Fluxmail')
|
|
1515
|
+
.description('Import settings from an env file without deleting it')
|
|
1516
|
+
.action((options) => {
|
|
1517
|
+
const source = path.resolve(options.from);
|
|
1518
|
+
if (options.dryRun) {
|
|
1519
|
+
const recognized = recognizedMigrationSettings(parseEnvContent(readFileSync(source, 'utf8')));
|
|
1520
|
+
console.log(recognized.length ? recognized.join('\n') : 'No recognized settings found.');
|
|
1311
1521
|
return;
|
|
1312
1522
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1523
|
+
const result = migrateConfigurationFile(source);
|
|
1524
|
+
console.log(`Imported ${result.recognized.length} recognized setting${result.recognized.length === 1 ? '' : 's'} from ${result.source}.`);
|
|
1525
|
+
console.log('Restart Fluxmail to apply deployment configuration changes.');
|
|
1526
|
+
});
|
|
1527
|
+
const oauth = program.command('oauth').description('Manage OAuth applications');
|
|
1528
|
+
oauth
|
|
1529
|
+
.command('status')
|
|
1530
|
+
.description('Show OAuth application status without secrets')
|
|
1531
|
+
.action(async () => {
|
|
1532
|
+
const status = await instanceClient(selectedInstance()).json('/api/v1/admin/oauth-apps');
|
|
1533
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1534
|
+
});
|
|
1535
|
+
const oauthConfigure = oauth.command('configure').description('Configure an OAuth application');
|
|
1536
|
+
oauthConfigure
|
|
1537
|
+
.command('google')
|
|
1538
|
+
.requiredOption('--client-id <id>', 'Google OAuth client ID')
|
|
1539
|
+
.option('--client-secret-file <path>', 'Read the client secret from a file, or use - for stdin')
|
|
1540
|
+
.description('Configure a custom Google OAuth application')
|
|
1541
|
+
.action(async (options) => {
|
|
1542
|
+
const clientSecret = await secretInput(options.clientSecretFile, 'Google client secret');
|
|
1543
|
+
await instanceClient(selectedInstance()).json('/api/v1/admin/oauth-apps/google', {
|
|
1544
|
+
method: 'PUT',
|
|
1545
|
+
headers: { 'content-type': 'application/json' },
|
|
1546
|
+
body: JSON.stringify({ clientId: options.clientId, clientSecret }),
|
|
1547
|
+
});
|
|
1548
|
+
console.log('Google OAuth application configured.');
|
|
1549
|
+
});
|
|
1550
|
+
oauthConfigure
|
|
1551
|
+
.command('outlook')
|
|
1552
|
+
.requiredOption('--client-id <id>', 'Microsoft OAuth client ID')
|
|
1553
|
+
.option('--tenant-id <tenant>', 'Microsoft tenant ID', 'common')
|
|
1554
|
+
.option('--public-client', 'Configure a public client without a secret')
|
|
1555
|
+
.option('--client-secret-file <path>', 'Read the client secret from a file, or use - for stdin')
|
|
1556
|
+
.description('Configure a custom Microsoft OAuth application')
|
|
1557
|
+
.action(async (options) => {
|
|
1558
|
+
if (options.publicClient && options.clientSecretFile) {
|
|
1559
|
+
throw new EmailError('invalid_request', '--public-client cannot be combined with --client-secret-file.');
|
|
1560
|
+
}
|
|
1561
|
+
const clientSecret = options.publicClient
|
|
1562
|
+
? undefined
|
|
1563
|
+
: await secretInput(options.clientSecretFile, 'Microsoft client secret');
|
|
1564
|
+
await instanceClient(selectedInstance()).json('/api/v1/admin/oauth-apps/outlook', {
|
|
1565
|
+
method: 'PUT',
|
|
1566
|
+
headers: { 'content-type': 'application/json' },
|
|
1567
|
+
body: JSON.stringify({
|
|
1568
|
+
clientId: options.clientId,
|
|
1569
|
+
tenantId: options.tenantId,
|
|
1570
|
+
publicClient: Boolean(options.publicClient),
|
|
1571
|
+
...(clientSecret ? { clientSecret } : {}),
|
|
1572
|
+
}),
|
|
1573
|
+
});
|
|
1574
|
+
console.log('Microsoft OAuth application configured.');
|
|
1575
|
+
});
|
|
1576
|
+
oauth
|
|
1577
|
+
.command('reset')
|
|
1578
|
+
.argument('<provider>', 'google or outlook')
|
|
1579
|
+
.description('Remove a stored OAuth application')
|
|
1580
|
+
.action(async (provider) => {
|
|
1581
|
+
if (provider !== 'google' && provider !== 'outlook') {
|
|
1582
|
+
throw new EmailError('invalid_request', 'Provider must be google or outlook.');
|
|
1583
|
+
}
|
|
1584
|
+
await instanceClient(selectedInstance()).json(`/api/v1/admin/oauth-apps/${provider}`, { method: 'DELETE' });
|
|
1585
|
+
console.log(`${provider === 'google' ? 'Google' : 'Microsoft'} OAuth application reset.`);
|
|
1586
|
+
});
|
|
1587
|
+
program
|
|
1588
|
+
.command('logs')
|
|
1589
|
+
.description('Show recent local log entries')
|
|
1590
|
+
.option('--tail <count>', 'Number of entries to show, from 1 through 1000', '100')
|
|
1591
|
+
.option('--level <level>', 'Minimum level: info, warn, or error', 'info')
|
|
1592
|
+
.option('--json', 'Print each entry as JSON')
|
|
1593
|
+
.action((opts) => {
|
|
1594
|
+
try {
|
|
1595
|
+
const tail = Number(opts.tail);
|
|
1596
|
+
if (!Number.isInteger(tail) || tail < 1 || tail > 1000) {
|
|
1597
|
+
throw new EmailError('invalid_request', '--tail must be an integer from 1 through 1000.');
|
|
1598
|
+
}
|
|
1599
|
+
if (!['info', 'warn', 'error'].includes(opts.level)) {
|
|
1600
|
+
throw new EmailError('invalid_request', '--level must be info, warn, or error.');
|
|
1601
|
+
}
|
|
1602
|
+
const result = readLocalLogs(resolveDataDir(), {
|
|
1603
|
+
tail,
|
|
1604
|
+
minimumLevel: opts.level,
|
|
1605
|
+
});
|
|
1606
|
+
for (const entry of result.entries) {
|
|
1607
|
+
if (opts.json) {
|
|
1608
|
+
console.log(entry.raw);
|
|
1609
|
+
continue;
|
|
1610
|
+
}
|
|
1611
|
+
const singleLine = escapeLogTextForConsole;
|
|
1612
|
+
const code = entry.record.error?.code ? ` [${singleLine(entry.record.error.code)}]` : '';
|
|
1613
|
+
console.log(`${singleLine(entry.record.timestamp)} ${entry.record.level.toUpperCase()} ${singleLine(entry.record.event)}${code}: ${singleLine(entry.record.message)}`);
|
|
1614
|
+
}
|
|
1615
|
+
if (result.malformedLines > 0) {
|
|
1616
|
+
console.error(`Warning: skipped ${result.malformedLines} malformed local log line${result.malformedLines === 1 ? '' : 's'}.`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
catch (error) {
|
|
1620
|
+
const code = isEmailError(error) ? error.code : 'internal';
|
|
1621
|
+
finishCliOperation(program, 'error', code, error);
|
|
1622
|
+
console.error(`Error [${code}]: ${error instanceof Error ? error.message : String(error)}`);
|
|
1623
|
+
process.exitCode = 1;
|
|
1316
1624
|
}
|
|
1317
1625
|
});
|
|
1318
1626
|
const telemetryCmd = program.command('telemetry').description('Manage anonymous usage telemetry');
|
|
@@ -1330,7 +1638,6 @@ export function createCliProgram(options = {}) {
|
|
|
1330
1638
|
.action(() => {
|
|
1331
1639
|
const dataDir = resolveDataDir();
|
|
1332
1640
|
setTelemetryEnabled(dataDir, true);
|
|
1333
|
-
unsetStoredConfig(dataDir, 'FLUXMAIL_TELEMETRY');
|
|
1334
1641
|
console.log('Telemetry enabled. FLUXMAIL_TELEMETRY=0 or DO_NOT_TRACK=1 can still turn it off.');
|
|
1335
1642
|
});
|
|
1336
1643
|
telemetryCmd
|
|
@@ -1340,6 +1647,15 @@ export function createCliProgram(options = {}) {
|
|
|
1340
1647
|
const dataDir = resolveDataDir();
|
|
1341
1648
|
console.log(`Telemetry is ${isTelemetryEnabled(dataDir) ? 'enabled' : 'disabled'}.`);
|
|
1342
1649
|
});
|
|
1650
|
+
registerMailCommands(program, {
|
|
1651
|
+
selectedInstance,
|
|
1652
|
+
selectedAccount,
|
|
1653
|
+
reportError: (error, code) => {
|
|
1654
|
+
finishCliOperation(program, 'error', code, error);
|
|
1655
|
+
console.error(`Error [${code}]: ${error instanceof Error ? error.message : String(error)}`);
|
|
1656
|
+
process.exitCode = 1;
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1343
1659
|
program
|
|
1344
1660
|
.command('status')
|
|
1345
1661
|
.description('Show mailbox and provider status for the selected instance')
|
|
@@ -1349,6 +1665,7 @@ export function createCliProgram(options = {}) {
|
|
|
1349
1665
|
console.log(JSON.stringify(status, null, 2));
|
|
1350
1666
|
}
|
|
1351
1667
|
catch (err) {
|
|
1668
|
+
logActiveCliError(program, err);
|
|
1352
1669
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1353
1670
|
process.exitCode = 1;
|
|
1354
1671
|
}
|
|
@@ -1356,15 +1673,15 @@ export function createCliProgram(options = {}) {
|
|
|
1356
1673
|
return program;
|
|
1357
1674
|
}
|
|
1358
1675
|
export async function runCli(argv = process.argv) {
|
|
1359
|
-
const program = createCliProgram();
|
|
1676
|
+
const program = createCliProgram({ updateNotifierFactory: createCliUpdateNotifier });
|
|
1360
1677
|
try {
|
|
1361
1678
|
await program.parseAsync([...argv]);
|
|
1362
1679
|
}
|
|
1363
1680
|
catch (err) {
|
|
1364
|
-
finishCliOperation(program, 'error', isEmailError(err) ? err.code : 'internal');
|
|
1681
|
+
finishCliOperation(program, 'error', isEmailError(err) ? err.code : 'internal', err);
|
|
1365
1682
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1366
1683
|
process.exitCode = 1;
|
|
1367
|
-
await shutdownTelemetry();
|
|
1684
|
+
await Promise.all([shutdownTelemetry(), shutdownLogging()]);
|
|
1368
1685
|
}
|
|
1369
1686
|
}
|
|
1370
1687
|
function isEntrypoint() {
|