fluxmail 0.5.0 → 0.6.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/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 +2 -1
- package/dist/accounts/registry.d.ts.map +1 -1
- package/dist/accounts/registry.js +7 -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 +71 -4
- 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/config.js
CHANGED
|
@@ -1,427 +1,402 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
5
2
|
import { DEFAULT_GOOGLE_CLIENT_ID, DEFAULT_GOOGLE_CLIENT_SECRET } from './accounts/defaultGoogleOAuth.js';
|
|
6
3
|
import { DEFAULT_LICENSE_SERVER_URL } from './licensing/client.js';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
* keys so adding a new setting cannot bypass the reference list by accident.
|
|
17
|
-
*/
|
|
18
|
-
export const CONFIG_REFERENCE = {
|
|
19
|
-
GOOGLE_CLIENT_ID: {
|
|
4
|
+
import { DEFAULT_MAX_ATTACHMENT_BYTES, DEPLOYMENT_REFERENCE, HARD_MAX_ATTACHMENT_BYTES, configTomlPath, deploymentToml, ensureDeploymentEncryptionKey, expandHome, readSecretEnvironment, resolveDataDirectory, resolveDeploymentConfig, writeDeploymentConfig, } from './deploymentConfig.js';
|
|
5
|
+
import { InstanceConfigStore } from './instanceConfig.js';
|
|
6
|
+
export { DEFAULT_MAX_ATTACHMENT_BYTES, HARD_MAX_ATTACHMENT_BYTES, configTomlPath, deploymentToml, ensureDeploymentEncryptionKey, expandHome, resolveDeploymentConfig, writeDeploymentConfig, };
|
|
7
|
+
const INSTANCE_SETTING_REGISTRY = {
|
|
8
|
+
'oauth.google.client_id': {
|
|
9
|
+
canonicalName: 'oauth.google.client_id',
|
|
10
|
+
env: 'GOOGLE_CLIENT_ID',
|
|
11
|
+
type: 'string',
|
|
12
|
+
validation: 'Non-empty and paired with a Google client secret',
|
|
20
13
|
defaultValue: 'Fluxmail Desktop OAuth app',
|
|
14
|
+
sensitive: false,
|
|
15
|
+
mutability: 'immediate',
|
|
16
|
+
environmentRequiresRestart: true,
|
|
21
17
|
description: 'Override the built-in Google OAuth client ID.',
|
|
18
|
+
category: 'instance',
|
|
22
19
|
},
|
|
23
|
-
|
|
20
|
+
'oauth.google.client_secret': {
|
|
21
|
+
canonicalName: 'oauth.google.client_secret',
|
|
22
|
+
env: 'GOOGLE_CLIENT_SECRET',
|
|
23
|
+
type: 'secret',
|
|
24
|
+
validation: 'Non-empty and paired with a Google client ID',
|
|
24
25
|
defaultValue: 'Fluxmail Desktop OAuth app',
|
|
26
|
+
sensitive: true,
|
|
27
|
+
mutability: 'immediate',
|
|
28
|
+
environmentRequiresRestart: true,
|
|
25
29
|
description: 'Override the built-in Google OAuth client secret. Required with GOOGLE_CLIENT_ID.',
|
|
26
|
-
|
|
30
|
+
category: 'instance',
|
|
31
|
+
envFile: true,
|
|
27
32
|
},
|
|
28
|
-
|
|
33
|
+
'oauth.microsoft.client_id': {
|
|
34
|
+
canonicalName: 'oauth.microsoft.client_id',
|
|
35
|
+
env: 'MICROSOFT_CLIENT_ID',
|
|
36
|
+
type: 'string',
|
|
37
|
+
validation: 'Non-empty when any Microsoft override is present',
|
|
29
38
|
defaultValue: 'required for Outlook',
|
|
39
|
+
sensitive: false,
|
|
40
|
+
mutability: 'immediate',
|
|
41
|
+
environmentRequiresRestart: true,
|
|
30
42
|
description: 'Microsoft Entra application client ID.',
|
|
43
|
+
category: 'instance',
|
|
31
44
|
},
|
|
32
|
-
|
|
45
|
+
'oauth.microsoft.client_secret': {
|
|
46
|
+
canonicalName: 'oauth.microsoft.client_secret',
|
|
47
|
+
env: 'MICROSOFT_CLIENT_SECRET',
|
|
48
|
+
type: 'secret',
|
|
49
|
+
validation: 'Non-empty when configured',
|
|
33
50
|
defaultValue: 'required for hosted Outlook connections',
|
|
51
|
+
sensitive: true,
|
|
52
|
+
mutability: 'immediate',
|
|
53
|
+
environmentRequiresRestart: true,
|
|
34
54
|
description: 'Microsoft Entra application client secret.',
|
|
35
|
-
|
|
55
|
+
category: 'instance',
|
|
56
|
+
envFile: true,
|
|
36
57
|
},
|
|
37
|
-
|
|
58
|
+
'oauth.microsoft.tenant_id': {
|
|
59
|
+
canonicalName: 'oauth.microsoft.tenant_id',
|
|
60
|
+
env: 'MICROSOFT_TENANT_ID',
|
|
61
|
+
type: 'string',
|
|
62
|
+
validation: 'Non-empty tenant ID or verified domain',
|
|
38
63
|
defaultValue: 'common',
|
|
64
|
+
sensitive: false,
|
|
65
|
+
mutability: 'immediate',
|
|
66
|
+
environmentRequiresRestart: true,
|
|
39
67
|
description: 'Microsoft Entra tenant ID or verified domain.',
|
|
68
|
+
category: 'instance',
|
|
40
69
|
},
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
defaultValue: '<data dir>/fluxmail.db',
|
|
47
|
-
description: 'Override the SQLite database path.',
|
|
48
|
-
},
|
|
49
|
-
FLUXMAIL_ENCRYPTION_KEY: {
|
|
50
|
-
defaultValue: 'generated automatically',
|
|
51
|
-
description: 'A 64-character hexadecimal key used to encrypt provider credentials.',
|
|
52
|
-
secret: true,
|
|
53
|
-
},
|
|
54
|
-
FLUXMAIL_PORT: {
|
|
55
|
-
defaultValue: '8977',
|
|
56
|
-
description: 'HTTP server port.',
|
|
57
|
-
},
|
|
58
|
-
FLUXMAIL_PUBLIC_URL: {
|
|
59
|
-
defaultValue: 'http://localhost:<FLUXMAIL_PORT>',
|
|
60
|
-
description: 'Public base URL used for HTTP APIs and hosted OAuth callbacks.',
|
|
61
|
-
},
|
|
62
|
-
FLUXMAIL_TRUST_PROXY: {
|
|
63
|
-
defaultValue: '0',
|
|
64
|
-
description: 'Trust Forwarded, X-Forwarded-Proto, and X-Forwarded-For headers from a reverse proxy.',
|
|
65
|
-
},
|
|
66
|
-
FLUXMAIL_OAUTH_PORT: {
|
|
67
|
-
defaultValue: '8976',
|
|
68
|
-
description: 'Port for the local OAuth callback listener.',
|
|
69
|
-
},
|
|
70
|
-
FLUXMAIL_OAUTH_HOST: {
|
|
71
|
-
defaultValue: '127.0.0.1',
|
|
72
|
-
description: 'Bind address for the local OAuth callback listener.',
|
|
73
|
-
},
|
|
74
|
-
FLUXMAIL_MAX_ATTACHMENT_MB: {
|
|
75
|
-
defaultValue: '10',
|
|
76
|
-
description: 'Largest decoded attachment returned through MCP or REST, from 1 through 25 MB.',
|
|
77
|
-
},
|
|
78
|
-
FLUXMAIL_LICENSE_KEY: {
|
|
70
|
+
'license.key': {
|
|
71
|
+
canonicalName: 'license.key',
|
|
72
|
+
env: 'FLUXMAIL_LICENSE_KEY',
|
|
73
|
+
type: 'secret',
|
|
74
|
+
validation: 'Fluxmail license key',
|
|
79
75
|
defaultValue: 'none',
|
|
76
|
+
sensitive: true,
|
|
77
|
+
mutability: 'immediate',
|
|
78
|
+
environmentRequiresRestart: true,
|
|
80
79
|
description: 'Paid-plan license key, normally stored with fluxmail license activate.',
|
|
81
|
-
|
|
80
|
+
category: 'instance',
|
|
81
|
+
envFile: true,
|
|
82
82
|
},
|
|
83
|
-
|
|
83
|
+
'telemetry.enabled': {
|
|
84
|
+
canonicalName: 'telemetry.enabled',
|
|
85
|
+
env: 'FLUXMAIL_TELEMETRY',
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
validation: '0, 1, false, true, no, off, or on',
|
|
84
88
|
defaultValue: '1',
|
|
89
|
+
sensitive: false,
|
|
90
|
+
mutability: 'pre-database',
|
|
85
91
|
description: 'Set to 0 to turn off anonymous CLI, MCP, and REST usage telemetry.',
|
|
92
|
+
category: 'instance',
|
|
93
|
+
primaryStorage: 'Data directory marker',
|
|
86
94
|
},
|
|
87
|
-
|
|
95
|
+
'telemetry.do_not_track': {
|
|
96
|
+
canonicalName: 'telemetry.do_not_track',
|
|
97
|
+
env: 'DO_NOT_TRACK',
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
validation: 'Truthy value disables telemetry',
|
|
88
100
|
defaultValue: 'unset',
|
|
101
|
+
sensitive: false,
|
|
102
|
+
mutability: 'pre-database',
|
|
89
103
|
description: 'Set to 1 to turn off anonymous usage telemetry.',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
defaultValue: DEFAULT_LICENSE_SERVER_URL,
|
|
93
|
-
description: 'License validation service override used for development and testing.',
|
|
94
|
-
documented: false,
|
|
104
|
+
category: 'instance',
|
|
105
|
+
primaryStorage: 'Data directory marker',
|
|
95
106
|
},
|
|
96
107
|
};
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
export const SETTING_REGISTRY = {
|
|
109
|
+
...Object.fromEntries(Object.values(DEPLOYMENT_REFERENCE).map((entry) => [
|
|
110
|
+
entry.canonicalName,
|
|
111
|
+
{
|
|
112
|
+
canonicalName: entry.canonicalName,
|
|
113
|
+
env: entry.env,
|
|
114
|
+
...('toml' in entry && entry.toml ? { toml: entry.toml } : {}),
|
|
115
|
+
type: entry.type,
|
|
116
|
+
validation: entry.validation,
|
|
117
|
+
defaultValue: entry.defaultValue,
|
|
118
|
+
sensitive: 'secret' in entry && entry.secret === true,
|
|
119
|
+
mutability: 'restart',
|
|
120
|
+
description: entry.description,
|
|
121
|
+
category: 'deployment',
|
|
122
|
+
...('secret' in entry && entry.secret ? { envFile: true } : {}),
|
|
123
|
+
...('documented' in entry && entry.documented === false ? { documented: false } : {}),
|
|
124
|
+
},
|
|
125
|
+
])),
|
|
126
|
+
...INSTANCE_SETTING_REGISTRY,
|
|
127
|
+
};
|
|
128
|
+
export const CONFIG_REFERENCE = Object.fromEntries(Object.values(SETTING_REGISTRY).map((entry) => [
|
|
129
|
+
entry.env,
|
|
130
|
+
{
|
|
131
|
+
defaultValue: entry.defaultValue,
|
|
132
|
+
description: entry.description,
|
|
133
|
+
category: entry.category,
|
|
134
|
+
...(entry.sensitive ? { secret: true } : {}),
|
|
135
|
+
...(entry.envFile ? { envFile: true } : {}),
|
|
136
|
+
...(entry.toml ? { toml: entry.toml } : {}),
|
|
137
|
+
...(entry.documented === false ? { documented: false } : {}),
|
|
138
|
+
},
|
|
139
|
+
]));
|
|
140
|
+
export class EnvironmentControlledSettingError extends Error {
|
|
141
|
+
code = 'environment_controlled';
|
|
99
142
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const parsed = JSON.parse(value);
|
|
104
|
-
return typeof parsed === 'string' ? parsed : value.slice(1, -1);
|
|
105
|
-
}
|
|
106
|
-
catch {
|
|
107
|
-
return value.slice(1, -1);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (value.startsWith("'") && value.endsWith("'"))
|
|
111
|
-
return value.slice(1, -1);
|
|
112
|
-
return value;
|
|
143
|
+
function readOptionalSecretEnvironment(env, name) {
|
|
144
|
+
return readSecretEnvironment(env, name, { blank: 'unset' });
|
|
113
145
|
}
|
|
114
|
-
function
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
146
|
+
function environmentGoogle(env) {
|
|
147
|
+
const clientId = env.GOOGLE_CLIENT_ID?.trim();
|
|
148
|
+
const secret = readOptionalSecretEnvironment(env, 'GOOGLE_CLIENT_SECRET');
|
|
149
|
+
const controlled = Boolean(clientId) || secret.value !== undefined;
|
|
150
|
+
if (!controlled)
|
|
151
|
+
return { controlled: false };
|
|
152
|
+
if (!clientId)
|
|
153
|
+
throw new Error('GOOGLE_CLIENT_ID is required when Google OAuth environment overrides are used.');
|
|
154
|
+
if (!secret.value) {
|
|
155
|
+
throw new Error('GOOGLE_CLIENT_SECRET or GOOGLE_CLIENT_SECRET_FILE is required with GOOGLE_CLIENT_ID.');
|
|
118
156
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
return comment === -1 ? raw : raw.slice(0, comment).trimEnd();
|
|
157
|
+
return {
|
|
158
|
+
value: { clientId, clientSecret: secret.value },
|
|
159
|
+
source: secret.source === 'environment-file' ? 'environment-file' : 'environment',
|
|
160
|
+
controlled: true,
|
|
161
|
+
};
|
|
125
162
|
}
|
|
126
|
-
function
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const key = trimmed
|
|
136
|
-
.slice(0, eq)
|
|
137
|
-
.trim()
|
|
138
|
-
.replace(/^export\s+/, '');
|
|
139
|
-
if (key)
|
|
140
|
-
out[key] = parseEnvValue(trimmed.slice(eq + 1).trim());
|
|
163
|
+
function environmentMicrosoft(env) {
|
|
164
|
+
const clientId = env.MICROSOFT_CLIENT_ID?.trim();
|
|
165
|
+
const clientSecret = readOptionalSecretEnvironment(env, 'MICROSOFT_CLIENT_SECRET');
|
|
166
|
+
const tenantId = env.MICROSOFT_TENANT_ID?.trim();
|
|
167
|
+
const controlled = Boolean(clientId) || Boolean(tenantId) || clientSecret.value !== undefined;
|
|
168
|
+
if (!controlled)
|
|
169
|
+
return { controlled: false };
|
|
170
|
+
if (!clientId) {
|
|
171
|
+
throw new Error('MICROSOFT_CLIENT_ID is required when Microsoft OAuth environment overrides are used.');
|
|
141
172
|
}
|
|
142
|
-
return
|
|
173
|
+
return {
|
|
174
|
+
value: {
|
|
175
|
+
clientId,
|
|
176
|
+
tenantId: tenantId || 'common',
|
|
177
|
+
...(clientSecret.value ? { clientSecret: clientSecret.value } : {}),
|
|
178
|
+
},
|
|
179
|
+
source: clientSecret.source === 'environment-file' ? 'environment-file' : 'environment',
|
|
180
|
+
controlled: true,
|
|
181
|
+
};
|
|
143
182
|
}
|
|
144
|
-
function
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for (const [key, value] of Object.entries(parseEnvContent(readFileSync(file, 'utf8')))) {
|
|
148
|
-
if (process.env[key] === undefined)
|
|
149
|
-
process.env[key] = value;
|
|
150
|
-
}
|
|
183
|
+
function environmentLicense(env) {
|
|
184
|
+
const configured = readOptionalSecretEnvironment(env, 'FLUXMAIL_LICENSE_KEY');
|
|
185
|
+
return { value: configured.value?.trim(), source: configured.source, controlled: configured.value !== undefined };
|
|
151
186
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
187
|
+
function baseConfigFromDeployment(deployment) {
|
|
188
|
+
return {
|
|
189
|
+
dataDir: deployment.dataDir,
|
|
190
|
+
dbPath: deployment.dbPath,
|
|
191
|
+
encryptionKey: deployment.encryptionKey,
|
|
192
|
+
port: deployment.port,
|
|
193
|
+
publicUrl: deployment.publicUrl,
|
|
194
|
+
publicUrlConfigured: deployment.publicUrlConfigured,
|
|
195
|
+
trustProxy: deployment.trustProxy,
|
|
196
|
+
oauthPort: deployment.oauthPort,
|
|
197
|
+
oauthHost: deployment.oauthHost,
|
|
198
|
+
maxAttachmentBytes: deployment.maxAttachmentBytes,
|
|
199
|
+
logLevel: deployment.logLevel,
|
|
200
|
+
logDestination: deployment.logDestination,
|
|
201
|
+
licenseServerUrl: deployment.licenseServerUrl,
|
|
202
|
+
};
|
|
159
203
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
204
|
+
function configFromDeployment(deployment) {
|
|
205
|
+
const google = environmentGoogle(deployment.environment);
|
|
206
|
+
const microsoft = environmentMicrosoft(deployment.environment);
|
|
207
|
+
const license = environmentLicense(deployment.environment);
|
|
208
|
+
return {
|
|
209
|
+
...baseConfigFromDeployment(deployment),
|
|
210
|
+
google: google.value ?? { clientId: DEFAULT_GOOGLE_CLIENT_ID, clientSecret: DEFAULT_GOOGLE_CLIENT_SECRET },
|
|
211
|
+
...(microsoft.value ? { microsoft: microsoft.value } : {}),
|
|
212
|
+
...(license.value ? { licenseKey: license.value, licenseKeyFromEnvironment: true } : {}),
|
|
213
|
+
};
|
|
167
214
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
loadDotEnv();
|
|
171
|
-
const fromEnv = readEnvironment('FLUXMAIL_DATA_DIR');
|
|
172
|
-
const dataDir = fromEnv ? expandHome(fromEnv) : path.join(homedir(), '.fluxmail');
|
|
215
|
+
export function resolveDataDir(options = {}) {
|
|
216
|
+
const dataDir = resolveDataDirectory(options.env);
|
|
173
217
|
mkdirSync(dataDir, { recursive: true });
|
|
174
218
|
return dataDir;
|
|
175
219
|
}
|
|
176
|
-
export function
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
applyDotEnvFile(configFilePath(dataDir));
|
|
182
|
-
}
|
|
183
|
-
/** Best-effort permission tightening: chmod fails on files owned by another user, but the read may still work. */
|
|
184
|
-
function tryRestrictPermissions(file) {
|
|
185
|
-
try {
|
|
186
|
-
chmodSync(file, 0o600);
|
|
187
|
-
}
|
|
188
|
-
catch {
|
|
189
|
-
// Reading (or a later write) will surface a real permission problem.
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
/** Settings persisted by `fluxmail config set`, e.g. GOOGLE_CLIENT_ID. */
|
|
193
|
-
export function readStoredConfig(dataDir) {
|
|
194
|
-
const file = configFilePath(dataDir);
|
|
195
|
-
if (!existsSync(file))
|
|
196
|
-
return {};
|
|
197
|
-
tryRestrictPermissions(file);
|
|
198
|
-
return parseEnvContent(readFileSync(file, 'utf8'));
|
|
199
|
-
}
|
|
200
|
-
const CONFIG_KEY_PATTERN = /^[A-Z][A-Z0-9_]*$/;
|
|
201
|
-
export function setStoredConfig(dataDir, key, value) {
|
|
202
|
-
if (!CONFIG_KEY_PATTERN.test(key)) {
|
|
203
|
-
throw new Error(`Invalid key "${key}": use UPPER_SNAKE_CASE, e.g. GOOGLE_CLIENT_ID`);
|
|
204
|
-
}
|
|
205
|
-
if (key === 'FLUXMAIL_DATA_DIR') {
|
|
206
|
-
throw new Error('FLUXMAIL_DATA_DIR cannot be stored in the data dir itself; set it in your shell or a .env file');
|
|
207
|
-
}
|
|
208
|
-
withConfigLock(dataDir, () => {
|
|
209
|
-
const stored = readStoredConfig(dataDir);
|
|
210
|
-
stored[key] = value;
|
|
211
|
-
writeStoredConfig(dataDir, stored);
|
|
220
|
+
export function readLoggingSettings(dataDir = resolveDataDir()) {
|
|
221
|
+
const deployment = resolveDeploymentConfig({
|
|
222
|
+
env: { ...process.env, FLUXMAIL_DATA_DIR: dataDir },
|
|
223
|
+
defaultLicenseServerUrl: DEFAULT_LICENSE_SERVER_URL,
|
|
224
|
+
generateEncryptionKey: false,
|
|
212
225
|
});
|
|
226
|
+
return { level: deployment.logLevel, destination: deployment.logDestination };
|
|
213
227
|
}
|
|
214
|
-
export function
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
delete stored[key];
|
|
220
|
-
writeStoredConfig(dataDir, stored);
|
|
221
|
-
return true;
|
|
228
|
+
export function resolveStoreLocation(options = {}) {
|
|
229
|
+
const deployment = resolveDeploymentConfig({
|
|
230
|
+
...options,
|
|
231
|
+
defaultLicenseServerUrl: DEFAULT_LICENSE_SERVER_URL,
|
|
232
|
+
generateEncryptionKey: false,
|
|
222
233
|
});
|
|
234
|
+
return { dataDir: deployment.dataDir, dbPath: deployment.dbPath, deployment };
|
|
223
235
|
}
|
|
224
|
-
export function
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
236
|
+
export function loadConfig(storeLocation) {
|
|
237
|
+
const initial = storeLocation?.deployment;
|
|
238
|
+
const deployment = initial
|
|
239
|
+
? ensureDeploymentEncryptionKey(initial)
|
|
240
|
+
: resolveDeploymentConfig({
|
|
241
|
+
env: {
|
|
242
|
+
...process.env,
|
|
243
|
+
...(storeLocation?.dataDir ? { FLUXMAIL_DATA_DIR: storeLocation.dataDir } : {}),
|
|
244
|
+
...(storeLocation?.dbPath ? { FLUXMAIL_DB_PATH: storeLocation.dbPath } : {}),
|
|
245
|
+
},
|
|
246
|
+
defaultLicenseServerUrl: DEFAULT_LICENSE_SERVER_URL,
|
|
247
|
+
generateEncryptionKey: true,
|
|
248
|
+
});
|
|
249
|
+
return configFromDeployment(deployment);
|
|
228
250
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
export class ConfigurationService {
|
|
252
|
+
deployment;
|
|
253
|
+
store;
|
|
254
|
+
config;
|
|
255
|
+
googleEnvironment;
|
|
256
|
+
microsoftEnvironment;
|
|
257
|
+
licenseEnvironment;
|
|
258
|
+
currentGoogle;
|
|
259
|
+
currentMicrosoft;
|
|
260
|
+
currentLicenseKey;
|
|
261
|
+
observedDataVersion = -1;
|
|
262
|
+
googleSource = 'built-in';
|
|
263
|
+
microsoftSource = null;
|
|
264
|
+
currentLicenseSource = null;
|
|
265
|
+
constructor(deployment, db) {
|
|
266
|
+
this.deployment = deployment;
|
|
267
|
+
this.store = new InstanceConfigStore(db, deployment.encryptionKey);
|
|
268
|
+
this.googleEnvironment = environmentGoogle(deployment.environment);
|
|
269
|
+
this.microsoftEnvironment = environmentMicrosoft(deployment.environment);
|
|
270
|
+
this.licenseEnvironment = environmentLicense(deployment.environment);
|
|
271
|
+
const config = baseConfigFromDeployment(deployment);
|
|
272
|
+
Object.defineProperties(config, {
|
|
273
|
+
google: {
|
|
274
|
+
enumerable: true,
|
|
275
|
+
get: () => {
|
|
276
|
+
this.refreshIfChanged();
|
|
277
|
+
return this.currentGoogle;
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
microsoft: {
|
|
281
|
+
enumerable: true,
|
|
282
|
+
get: () => {
|
|
283
|
+
this.refreshIfChanged();
|
|
284
|
+
return this.currentMicrosoft;
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
licenseKey: {
|
|
288
|
+
enumerable: true,
|
|
289
|
+
get: () => {
|
|
290
|
+
this.refreshIfChanged();
|
|
291
|
+
return this.currentLicenseKey;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
licenseKeyFromEnvironment: {
|
|
295
|
+
enumerable: true,
|
|
296
|
+
value: this.licenseEnvironment.controlled,
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
this.config = config;
|
|
300
|
+
this.reload();
|
|
243
301
|
}
|
|
244
|
-
|
|
245
|
-
if (
|
|
246
|
-
|
|
247
|
-
rmSync(temporary, { force: true });
|
|
302
|
+
refreshIfChanged() {
|
|
303
|
+
if (this.store.dataVersion() !== this.observedDataVersion)
|
|
304
|
+
this.reload();
|
|
248
305
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
staleMs: CONFIG_LOCK_STALE_MS,
|
|
268
|
-
description: `the stored configuration at ${configFilePath(dataDir)}`,
|
|
269
|
-
}, callback);
|
|
270
|
-
}
|
|
271
|
-
function decodeEncryptionKey(value, errorMessage) {
|
|
272
|
-
if (!/^[0-9a-fA-F]{64}$/.test(value))
|
|
273
|
-
throw new Error(errorMessage);
|
|
274
|
-
return Buffer.from(value, 'hex');
|
|
275
|
-
}
|
|
276
|
-
function readPort(name, fallback) {
|
|
277
|
-
const raw = readEnvironment(name);
|
|
278
|
-
if (raw === undefined)
|
|
279
|
-
return fallback;
|
|
280
|
-
const value = Number(raw);
|
|
281
|
-
if (!Number.isInteger(value) || value < 1 || value > 65_535) {
|
|
282
|
-
throw new Error(`${name} must be an integer between 1 and 65535, got "${raw}"`);
|
|
283
|
-
}
|
|
284
|
-
return value;
|
|
285
|
-
}
|
|
286
|
-
function readMaxAttachmentBytes() {
|
|
287
|
-
const raw = readEnvironment('FLUXMAIL_MAX_ATTACHMENT_MB');
|
|
288
|
-
if (raw === undefined)
|
|
289
|
-
return DEFAULT_MAX_ATTACHMENT_BYTES;
|
|
290
|
-
const value = Number(raw);
|
|
291
|
-
const hardMaxMegabytes = HARD_MAX_ATTACHMENT_BYTES / BYTES_PER_MEGABYTE;
|
|
292
|
-
if (!Number.isInteger(value) || value < 1 || value > hardMaxMegabytes) {
|
|
293
|
-
throw new Error(`FLUXMAIL_MAX_ATTACHMENT_MB must be an integer between 1 and ${hardMaxMegabytes}, got "${raw}"`);
|
|
294
|
-
}
|
|
295
|
-
return value * BYTES_PER_MEGABYTE;
|
|
296
|
-
}
|
|
297
|
-
function readTrustProxy() {
|
|
298
|
-
const raw = readEnvironment('FLUXMAIL_TRUST_PROXY');
|
|
299
|
-
if (raw === undefined)
|
|
300
|
-
return false;
|
|
301
|
-
const normalized = raw.trim().toLowerCase();
|
|
302
|
-
if (normalized === '1' || normalized === 'true')
|
|
303
|
-
return true;
|
|
304
|
-
if (normalized === '0' || normalized === 'false')
|
|
305
|
-
return false;
|
|
306
|
-
throw new Error(`FLUXMAIL_TRUST_PROXY must be 0, 1, false, or true, got "${raw}"`);
|
|
307
|
-
}
|
|
308
|
-
function readPublicUrl(port) {
|
|
309
|
-
const value = (readEnvironment('FLUXMAIL_PUBLIC_URL') ?? `http://localhost:${port}`).replace(/\/+$/, '');
|
|
310
|
-
let parsed;
|
|
311
|
-
try {
|
|
312
|
-
parsed = new URL(value);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
throw new Error(`FLUXMAIL_PUBLIC_URL must be a valid HTTP or HTTPS URL, got "${value}"`);
|
|
316
|
-
}
|
|
317
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
318
|
-
throw new Error(`FLUXMAIL_PUBLIC_URL must use HTTP or HTTPS, got "${parsed.protocol}"`);
|
|
319
|
-
}
|
|
320
|
-
if (parsed.username || parsed.password) {
|
|
321
|
-
throw new Error('FLUXMAIL_PUBLIC_URL cannot contain embedded credentials');
|
|
322
|
-
}
|
|
323
|
-
if (parsed.search || parsed.hash) {
|
|
324
|
-
throw new Error('FLUXMAIL_PUBLIC_URL cannot contain a query string or fragment');
|
|
306
|
+
reload() {
|
|
307
|
+
while (true) {
|
|
308
|
+
const dataVersion = this.store.dataVersion();
|
|
309
|
+
const storedGoogle = this.googleEnvironment.controlled ? undefined : this.store.google();
|
|
310
|
+
const storedMicrosoft = this.microsoftEnvironment.controlled ? undefined : this.store.microsoft();
|
|
311
|
+
const storedLicense = this.licenseEnvironment.controlled ? undefined : this.store.licenseKey();
|
|
312
|
+
if (this.store.dataVersion() !== dataVersion)
|
|
313
|
+
continue;
|
|
314
|
+
this.currentGoogle = this.googleEnvironment.value ??
|
|
315
|
+
storedGoogle ?? { clientId: DEFAULT_GOOGLE_CLIENT_ID, clientSecret: DEFAULT_GOOGLE_CLIENT_SECRET };
|
|
316
|
+
this.googleSource = this.googleEnvironment.source ?? (storedGoogle ? 'stored' : 'built-in');
|
|
317
|
+
this.currentMicrosoft = this.microsoftEnvironment.value ?? storedMicrosoft;
|
|
318
|
+
this.microsoftSource = this.microsoftEnvironment.source ?? (storedMicrosoft ? 'stored' : null);
|
|
319
|
+
this.currentLicenseKey = this.licenseEnvironment.value ?? storedLicense;
|
|
320
|
+
this.currentLicenseSource = this.licenseEnvironment.source ?? (storedLicense ? 'stored' : null);
|
|
321
|
+
this.observedDataVersion = dataVersion;
|
|
322
|
+
return this.config;
|
|
323
|
+
}
|
|
325
324
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
325
|
+
oauthStatus() {
|
|
326
|
+
this.refreshIfChanged();
|
|
327
|
+
return {
|
|
328
|
+
google: {
|
|
329
|
+
clientId: this.currentGoogle.clientId,
|
|
330
|
+
clientSecretConfigured: true,
|
|
331
|
+
source: this.googleSource,
|
|
332
|
+
mutable: !this.googleEnvironment.controlled,
|
|
333
|
+
},
|
|
334
|
+
outlook: {
|
|
335
|
+
clientId: this.currentMicrosoft?.clientId ?? null,
|
|
336
|
+
tenantId: this.currentMicrosoft?.tenantId ?? null,
|
|
337
|
+
clientSecretConfigured: Boolean(this.currentMicrosoft?.clientSecret),
|
|
338
|
+
source: this.microsoftSource,
|
|
339
|
+
mutable: !this.microsoftEnvironment.controlled,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
332
342
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
tryRestrictPermissions(keyPath);
|
|
337
|
-
const key = decodeEncryptionKey(readFileSync(keyPath, 'utf8').trim(), `Corrupt encryption key at ${keyPath}`);
|
|
338
|
-
return key;
|
|
343
|
+
licenseSource() {
|
|
344
|
+
this.refreshIfChanged();
|
|
345
|
+
return this.currentLicenseSource;
|
|
339
346
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
description: `the encryption key at ${keyPath}`,
|
|
344
|
-
}, () => {
|
|
345
|
-
if (existsSync(keyPath)) {
|
|
346
|
-
tryRestrictPermissions(keyPath);
|
|
347
|
-
return decodeEncryptionKey(readFileSync(keyPath, 'utf8').trim(), `Corrupt encryption key at ${keyPath}`);
|
|
347
|
+
setGoogle(value) {
|
|
348
|
+
if (this.googleEnvironment.controlled) {
|
|
349
|
+
throw new EnvironmentControlledSettingError('Google OAuth is controlled by environment variables and cannot be changed through the API.');
|
|
348
350
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
fsyncSync(descriptor);
|
|
356
|
-
closeSync(descriptor);
|
|
357
|
-
descriptor = undefined;
|
|
358
|
-
renameSync(temporary, keyPath);
|
|
359
|
-
trySyncDirectory(dataDir);
|
|
360
|
-
return key;
|
|
351
|
+
this.store.setGoogle(value);
|
|
352
|
+
this.reload();
|
|
353
|
+
}
|
|
354
|
+
resetGoogle() {
|
|
355
|
+
if (this.googleEnvironment.controlled) {
|
|
356
|
+
throw new EnvironmentControlledSettingError('Google OAuth is controlled by environment variables and cannot be changed through the API.');
|
|
361
357
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
358
|
+
const removed = this.store.removeGoogle();
|
|
359
|
+
this.reload();
|
|
360
|
+
return removed;
|
|
361
|
+
}
|
|
362
|
+
setMicrosoft(value) {
|
|
363
|
+
if (this.microsoftEnvironment.controlled) {
|
|
364
|
+
throw new EnvironmentControlledSettingError('Microsoft OAuth is controlled by environment variables and cannot be changed through the API.');
|
|
366
365
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return {
|
|
373
|
-
dataDir,
|
|
374
|
-
dbPath: databasePath ? expandHome(databasePath) : path.join(dataDir, 'fluxmail.db'),
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
export function loadConfig(storeLocation = resolveStoreLocation()) {
|
|
378
|
-
// Precedence: shell env > cwd .env.local > cwd .env > data-dir config.env.
|
|
379
|
-
const { dataDir, dbPath } = storeLocation;
|
|
380
|
-
const licenseKeyFromEnvironment = readEnvironment('FLUXMAIL_LICENSE_KEY') !== undefined;
|
|
381
|
-
applyStoredConfig(dataDir);
|
|
382
|
-
const port = readPort('FLUXMAIL_PORT', 8977);
|
|
383
|
-
const oauthPort = readPort('FLUXMAIL_OAUTH_PORT', 8976);
|
|
384
|
-
const publicUrlConfigured = readEnvironment('FLUXMAIL_PUBLIC_URL') !== undefined;
|
|
385
|
-
const publicUrl = readPublicUrl(port);
|
|
386
|
-
const config = {
|
|
387
|
-
dataDir,
|
|
388
|
-
dbPath,
|
|
389
|
-
encryptionKey: loadEncryptionKey(dataDir),
|
|
390
|
-
port,
|
|
391
|
-
publicUrl,
|
|
392
|
-
publicUrlConfigured,
|
|
393
|
-
trustProxy: readTrustProxy(),
|
|
394
|
-
oauthPort,
|
|
395
|
-
oauthHost: readEnvironment('FLUXMAIL_OAUTH_HOST') ?? '127.0.0.1',
|
|
396
|
-
maxAttachmentBytes: readMaxAttachmentBytes(),
|
|
397
|
-
licenseServerUrl: (readEnvironment('FLUXMAIL_LICENSE_SERVER_URL') ?? DEFAULT_LICENSE_SERVER_URL).replace(/\/+$/, ''),
|
|
398
|
-
};
|
|
399
|
-
const licenseKey = readEnvironment('FLUXMAIL_LICENSE_KEY')?.trim();
|
|
400
|
-
if (licenseKey) {
|
|
401
|
-
config.licenseKey = licenseKey;
|
|
402
|
-
config.licenseKeyFromEnvironment = licenseKeyFromEnvironment;
|
|
366
|
+
this.refreshIfChanged();
|
|
367
|
+
if (this.currentMicrosoft)
|
|
368
|
+
this.store.pinMicrosoftOAuthAppForAccounts(this.currentMicrosoft);
|
|
369
|
+
this.store.setMicrosoft(value);
|
|
370
|
+
this.reload();
|
|
403
371
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
372
|
+
resetMicrosoft() {
|
|
373
|
+
if (this.microsoftEnvironment.controlled) {
|
|
374
|
+
throw new EnvironmentControlledSettingError('Microsoft OAuth is controlled by environment variables and cannot be changed through the API.');
|
|
375
|
+
}
|
|
376
|
+
this.refreshIfChanged();
|
|
377
|
+
if (this.currentMicrosoft)
|
|
378
|
+
this.store.pinMicrosoftOAuthAppForAccounts(this.currentMicrosoft);
|
|
379
|
+
const removed = this.store.removeMicrosoft();
|
|
380
|
+
this.reload();
|
|
381
|
+
return removed;
|
|
408
382
|
}
|
|
409
|
-
|
|
410
|
-
|
|
383
|
+
setLicenseKey(value) {
|
|
384
|
+
if (this.licenseEnvironment.controlled) {
|
|
385
|
+
throw new EnvironmentControlledSettingError('The license is controlled by the environment and cannot be changed through the API.');
|
|
386
|
+
}
|
|
387
|
+
this.store.setLicenseKey(value);
|
|
388
|
+
this.reload();
|
|
411
389
|
}
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
config.microsoft = {
|
|
420
|
-
clientId: microsoftClientId,
|
|
421
|
-
tenantId: readEnvironment('MICROSOFT_TENANT_ID')?.trim() || 'common',
|
|
422
|
-
...(microsoftClientSecret ? { clientSecret: microsoftClientSecret } : {}),
|
|
423
|
-
};
|
|
390
|
+
removeLicenseKey() {
|
|
391
|
+
if (this.licenseEnvironment.controlled) {
|
|
392
|
+
throw new EnvironmentControlledSettingError('The license is controlled by the environment and cannot be changed through the API.');
|
|
393
|
+
}
|
|
394
|
+
const removed = this.store.removeLicenseKey();
|
|
395
|
+
this.reload();
|
|
396
|
+
return removed;
|
|
424
397
|
}
|
|
425
|
-
|
|
398
|
+
}
|
|
399
|
+
export function createConfigurationService(deployment, db) {
|
|
400
|
+
return new ConfigurationService(deployment, db);
|
|
426
401
|
}
|
|
427
402
|
//# sourceMappingURL=config.js.map
|