fluxmail 0.4.1 → 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/README.md +6 -4
- package/dist/accounts/defaultGoogleOAuth.d.ts +4 -0
- package/dist/accounts/defaultGoogleOAuth.d.ts.map +1 -0
- package/dist/accounts/defaultGoogleOAuth.js +7 -0
- package/dist/accounts/defaultGoogleOAuth.js.map +1 -0
- package/dist/accounts/gmailConnection.js +2 -2
- package/dist/accounts/gmailConnection.js.map +1 -1
- package/dist/accounts/googleAuth.d.ts +16 -9
- package/dist/accounts/googleAuth.d.ts.map +1 -1
- package/dist/accounts/googleAuth.js +52 -20
- 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 +25 -22
- package/dist/accounts/microsoftAuth.js.map +1 -1
- package/dist/accounts/registry.d.ts +11 -9
- package/dist/accounts/registry.d.ts.map +1 -1
- package/dist/accounts/registry.js +169 -98
- package/dist/accounts/registry.js.map +1 -1
- package/dist/auth.d.ts +106 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +455 -0
- package/dist/auth.js.map +1 -0
- package/dist/authorization.d.ts +9 -0
- package/dist/authorization.d.ts.map +1 -0
- package/dist/authorization.js +23 -0
- package/dist/authorization.js.map +1 -0
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +1096 -403
- package/dist/cli.js.map +1 -1
- package/dist/cliInstances.d.ts +51 -0
- package/dist/cliInstances.d.ts.map +1 -0
- package/dist/cliInstances.js +237 -0
- package/dist/cliInstances.js.map +1 -0
- 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 +95 -118
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +351 -286
- 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 +29 -8
- 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 +75 -3
- package/dist/http/admin.d.ts.map +1 -1
- package/dist/http/admin.js +347 -79
- 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 +119 -63
- package/dist/http/app.js.map +1 -1
- package/dist/http/identity.d.ts +114 -0
- package/dist/http/identity.d.ts.map +1 -0
- package/dist/http/identity.js +936 -0
- package/dist/http/identity.js.map +1 -0
- package/dist/http/operationRoutes.d.ts +8 -0
- package/dist/http/operationRoutes.d.ts.map +1 -0
- package/dist/http/operationRoutes.js +13 -0
- package/dist/http/operationRoutes.js.map +1 -0
- package/dist/http/rest.d.ts +16 -4
- package/dist/http/rest.d.ts.map +1 -1
- package/dist/http/rest.js +213 -71
- package/dist/http/rest.js.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- 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 +3 -1
- package/dist/mcp/buildServer.d.ts.map +1 -1
- package/dist/mcp/buildServer.js +27 -24
- package/dist/mcp/buildServer.js.map +1 -1
- package/dist/permissions.d.ts +1 -1
- package/dist/permissions.d.ts.map +1 -1
- package/dist/permissions.js +10 -4
- package/dist/permissions.js.map +1 -1
- package/dist/scheduler/sendScheduler.d.ts +4 -3
- package/dist/scheduler/sendScheduler.d.ts.map +1 -1
- package/dist/scheduler/sendScheduler.js +15 -3
- package/dist/scheduler/sendScheduler.js.map +1 -1
- package/dist/service/emailService.d.ts +10 -17
- package/dist/service/emailService.d.ts.map +1 -1
- package/dist/service/emailService.js +28 -38
- package/dist/service/emailService.js.map +1 -1
- package/dist/storage/adminAudit.d.ts +15 -2
- package/dist/storage/adminAudit.d.ts.map +1 -1
- package/dist/storage/adminAudit.js +22 -27
- package/dist/storage/adminAudit.js.map +1 -1
- package/dist/storage/apiKeys.d.ts +8 -8
- package/dist/storage/apiKeys.d.ts.map +1 -1
- package/dist/storage/apiKeys.js +16 -11
- package/dist/storage/apiKeys.js.map +1 -1
- package/dist/storage/db.d.ts +596 -31
- package/dist/storage/db.d.ts.map +1 -1
- package/dist/storage/db.js +383 -45
- package/dist/storage/db.js.map +1 -1
- package/dist/storage/fileLock.d.ts +7 -0
- package/dist/storage/fileLock.d.ts.map +1 -0
- package/dist/storage/fileLock.js +183 -0
- package/dist/storage/fileLock.js.map +1 -0
- package/dist/storage/gmailConnectionGrants.d.ts +3 -4
- package/dist/storage/gmailConnectionGrants.d.ts.map +1 -1
- package/dist/storage/gmailConnectionGrants.js +6 -6
- package/dist/storage/gmailConnectionGrants.js.map +1 -1
- package/dist/storage/members.d.ts +11 -0
- package/dist/storage/members.d.ts.map +1 -1
- package/dist/storage/members.js +70 -6
- package/dist/storage/members.js.map +1 -1
- package/dist/telemetry.d.ts +17 -2
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +34 -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 +38 -5
package/dist/config.js
CHANGED
|
@@ -1,337 +1,402 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import { DEFAULT_GOOGLE_CLIENT_ID, DEFAULT_GOOGLE_CLIENT_SECRET } from './accounts/defaultGoogleOAuth.js';
|
|
5
3
|
import { DEFAULT_LICENSE_SERVER_URL } from './licensing/client.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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',
|
|
13
|
+
defaultValue: 'Fluxmail Desktop OAuth app',
|
|
14
|
+
sensitive: false,
|
|
15
|
+
mutability: 'immediate',
|
|
16
|
+
environmentRequiresRestart: true,
|
|
17
|
+
description: 'Override the built-in Google OAuth client ID.',
|
|
18
|
+
category: 'instance',
|
|
18
19
|
},
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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',
|
|
25
|
+
defaultValue: 'Fluxmail Desktop OAuth app',
|
|
26
|
+
sensitive: true,
|
|
27
|
+
mutability: 'immediate',
|
|
28
|
+
environmentRequiresRestart: true,
|
|
29
|
+
description: 'Override the built-in Google OAuth client secret. Required with GOOGLE_CLIENT_ID.',
|
|
30
|
+
category: 'instance',
|
|
31
|
+
envFile: true,
|
|
23
32
|
},
|
|
24
|
-
|
|
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',
|
|
25
38
|
defaultValue: 'required for Outlook',
|
|
39
|
+
sensitive: false,
|
|
40
|
+
mutability: 'immediate',
|
|
41
|
+
environmentRequiresRestart: true,
|
|
26
42
|
description: 'Microsoft Entra application client ID.',
|
|
43
|
+
category: 'instance',
|
|
27
44
|
},
|
|
28
|
-
|
|
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',
|
|
29
50
|
defaultValue: 'required for hosted Outlook connections',
|
|
51
|
+
sensitive: true,
|
|
52
|
+
mutability: 'immediate',
|
|
53
|
+
environmentRequiresRestart: true,
|
|
30
54
|
description: 'Microsoft Entra application client secret.',
|
|
31
|
-
|
|
55
|
+
category: 'instance',
|
|
56
|
+
envFile: true,
|
|
32
57
|
},
|
|
33
|
-
|
|
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',
|
|
34
63
|
defaultValue: 'common',
|
|
64
|
+
sensitive: false,
|
|
65
|
+
mutability: 'immediate',
|
|
66
|
+
environmentRequiresRestart: true,
|
|
35
67
|
description: 'Microsoft Entra tenant ID or verified domain.',
|
|
68
|
+
category: 'instance',
|
|
36
69
|
},
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
defaultValue: '<data dir>/fluxmail.db',
|
|
43
|
-
description: 'Override the SQLite database path.',
|
|
44
|
-
},
|
|
45
|
-
FLUXMAIL_ENCRYPTION_KEY: {
|
|
46
|
-
defaultValue: 'generated automatically',
|
|
47
|
-
description: 'A 64-character hexadecimal key used to encrypt provider credentials.',
|
|
48
|
-
secret: true,
|
|
49
|
-
},
|
|
50
|
-
FLUXMAIL_PORT: {
|
|
51
|
-
defaultValue: '8977',
|
|
52
|
-
description: 'HTTP server port.',
|
|
53
|
-
},
|
|
54
|
-
FLUXMAIL_PUBLIC_URL: {
|
|
55
|
-
defaultValue: 'http://localhost:<FLUXMAIL_PORT>',
|
|
56
|
-
description: 'Public base URL used for HTTP APIs and hosted OAuth callbacks.',
|
|
57
|
-
},
|
|
58
|
-
FLUXMAIL_AUTH: {
|
|
59
|
-
defaultValue: 'apikey',
|
|
60
|
-
description: 'HTTP authentication mode. Use none only behind a trusted network boundary.',
|
|
61
|
-
},
|
|
62
|
-
FLUXMAIL_OAUTH_PORT: {
|
|
63
|
-
defaultValue: '8976',
|
|
64
|
-
description: 'Port for the local OAuth callback listener.',
|
|
65
|
-
},
|
|
66
|
-
FLUXMAIL_OAUTH_HOST: {
|
|
67
|
-
defaultValue: '127.0.0.1',
|
|
68
|
-
description: 'Bind address for the local OAuth callback listener.',
|
|
69
|
-
},
|
|
70
|
-
FLUXMAIL_MAX_ATTACHMENT_MB: {
|
|
71
|
-
defaultValue: '10',
|
|
72
|
-
description: 'Largest decoded attachment returned through MCP or REST, from 1 through 25 MB.',
|
|
73
|
-
},
|
|
74
|
-
FLUXMAIL_LICENSE_KEY: {
|
|
70
|
+
'license.key': {
|
|
71
|
+
canonicalName: 'license.key',
|
|
72
|
+
env: 'FLUXMAIL_LICENSE_KEY',
|
|
73
|
+
type: 'secret',
|
|
74
|
+
validation: 'Fluxmail license key',
|
|
75
75
|
defaultValue: 'none',
|
|
76
|
+
sensitive: true,
|
|
77
|
+
mutability: 'immediate',
|
|
78
|
+
environmentRequiresRestart: true,
|
|
76
79
|
description: 'Paid-plan license key, normally stored with fluxmail license activate.',
|
|
77
|
-
|
|
80
|
+
category: 'instance',
|
|
81
|
+
envFile: true,
|
|
78
82
|
},
|
|
79
|
-
|
|
83
|
+
'telemetry.enabled': {
|
|
84
|
+
canonicalName: 'telemetry.enabled',
|
|
85
|
+
env: 'FLUXMAIL_TELEMETRY',
|
|
86
|
+
type: 'boolean',
|
|
87
|
+
validation: '0, 1, false, true, no, off, or on',
|
|
80
88
|
defaultValue: '1',
|
|
89
|
+
sensitive: false,
|
|
90
|
+
mutability: 'pre-database',
|
|
81
91
|
description: 'Set to 0 to turn off anonymous CLI, MCP, and REST usage telemetry.',
|
|
92
|
+
category: 'instance',
|
|
93
|
+
primaryStorage: 'Data directory marker',
|
|
82
94
|
},
|
|
83
|
-
|
|
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',
|
|
84
100
|
defaultValue: 'unset',
|
|
101
|
+
sensitive: false,
|
|
102
|
+
mutability: 'pre-database',
|
|
85
103
|
description: 'Set to 1 to turn off anonymous usage telemetry.',
|
|
104
|
+
category: 'instance',
|
|
105
|
+
primaryStorage: 'Data directory marker',
|
|
86
106
|
},
|
|
87
|
-
FLUXMAIL_LICENSE_SERVER_URL: {
|
|
88
|
-
defaultValue: DEFAULT_LICENSE_SERVER_URL,
|
|
89
|
-
description: 'License validation service override used for development and testing.',
|
|
90
|
-
documented: false,
|
|
91
|
-
},
|
|
92
107
|
};
|
|
93
|
-
|
|
94
|
-
|
|
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';
|
|
95
142
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
try {
|
|
99
|
-
const parsed = JSON.parse(value);
|
|
100
|
-
return typeof parsed === 'string' ? parsed : value.slice(1, -1);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return value.slice(1, -1);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
if (value.startsWith("'") && value.endsWith("'"))
|
|
107
|
-
return value.slice(1, -1);
|
|
108
|
-
return value;
|
|
143
|
+
function readOptionalSecretEnvironment(env, name) {
|
|
144
|
+
return readSecretEnvironment(env, name, { blank: 'unset' });
|
|
109
145
|
}
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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.');
|
|
114
156
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
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
|
+
};
|
|
121
162
|
}
|
|
122
|
-
function
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const key = trimmed
|
|
132
|
-
.slice(0, eq)
|
|
133
|
-
.trim()
|
|
134
|
-
.replace(/^export\s+/, '');
|
|
135
|
-
if (key)
|
|
136
|
-
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.');
|
|
137
172
|
}
|
|
138
|
-
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
|
+
};
|
|
139
182
|
}
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
for (const [key, value] of Object.entries(parseEnvContent(readFileSync(file, 'utf8')))) {
|
|
144
|
-
if (process.env[key] === undefined)
|
|
145
|
-
process.env[key] = value;
|
|
146
|
-
}
|
|
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 };
|
|
147
186
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
};
|
|
155
203
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
};
|
|
163
214
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
loadDotEnv();
|
|
167
|
-
const fromEnv = readEnvironment('FLUXMAIL_DATA_DIR');
|
|
168
|
-
const dataDir = fromEnv ? expandHome(fromEnv) : path.join(homedir(), '.fluxmail');
|
|
215
|
+
export function resolveDataDir(options = {}) {
|
|
216
|
+
const dataDir = resolveDataDirectory(options.env);
|
|
169
217
|
mkdirSync(dataDir, { recursive: true });
|
|
170
218
|
return dataDir;
|
|
171
219
|
}
|
|
172
|
-
export function
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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,
|
|
225
|
+
});
|
|
226
|
+
return { level: deployment.logLevel, destination: deployment.logDestination };
|
|
178
227
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
228
|
+
export function resolveStoreLocation(options = {}) {
|
|
229
|
+
const deployment = resolveDeploymentConfig({
|
|
230
|
+
...options,
|
|
231
|
+
defaultLicenseServerUrl: DEFAULT_LICENSE_SERVER_URL,
|
|
232
|
+
generateEncryptionKey: false,
|
|
233
|
+
});
|
|
234
|
+
return { dataDir: deployment.dataDir, dbPath: deployment.dbPath, deployment };
|
|
187
235
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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);
|
|
195
250
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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();
|
|
200
301
|
}
|
|
201
|
-
|
|
202
|
-
|
|
302
|
+
refreshIfChanged() {
|
|
303
|
+
if (this.store.dataVersion() !== this.observedDataVersion)
|
|
304
|
+
this.reload();
|
|
203
305
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const file = configFilePath(dataDir);
|
|
223
|
-
const lines = Object.entries(values).map(([k, v]) => `${k}=${JSON.stringify(v)}`);
|
|
224
|
-
writeFileSync(file, lines.join('\n') + (lines.length ? '\n' : ''), { mode: 0o600 });
|
|
225
|
-
tryRestrictPermissions(file);
|
|
226
|
-
}
|
|
227
|
-
function decodeEncryptionKey(value, errorMessage) {
|
|
228
|
-
if (!/^[0-9a-fA-F]{64}$/.test(value))
|
|
229
|
-
throw new Error(errorMessage);
|
|
230
|
-
return Buffer.from(value, 'hex');
|
|
231
|
-
}
|
|
232
|
-
function readPort(name, fallback) {
|
|
233
|
-
const raw = readEnvironment(name);
|
|
234
|
-
if (raw === undefined)
|
|
235
|
-
return fallback;
|
|
236
|
-
const value = Number(raw);
|
|
237
|
-
if (!Number.isInteger(value) || value < 1 || value > 65_535) {
|
|
238
|
-
throw new Error(`${name} must be an integer between 1 and 65535, got "${raw}"`);
|
|
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
|
+
}
|
|
239
324
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
};
|
|
250
342
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
const value = (readEnvironment('FLUXMAIL_PUBLIC_URL') ?? `http://localhost:${port}`).replace(/\/+$/, '');
|
|
255
|
-
let parsed;
|
|
256
|
-
try {
|
|
257
|
-
parsed = new URL(value);
|
|
343
|
+
licenseSource() {
|
|
344
|
+
this.refreshIfChanged();
|
|
345
|
+
return this.currentLicenseSource;
|
|
258
346
|
}
|
|
259
|
-
|
|
260
|
-
|
|
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.');
|
|
350
|
+
}
|
|
351
|
+
this.store.setGoogle(value);
|
|
352
|
+
this.reload();
|
|
261
353
|
}
|
|
262
|
-
|
|
263
|
-
|
|
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.');
|
|
357
|
+
}
|
|
358
|
+
const removed = this.store.removeGoogle();
|
|
359
|
+
this.reload();
|
|
360
|
+
return removed;
|
|
264
361
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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.');
|
|
365
|
+
}
|
|
366
|
+
this.refreshIfChanged();
|
|
367
|
+
if (this.currentMicrosoft)
|
|
368
|
+
this.store.pinMicrosoftOAuthAppForAccounts(this.currentMicrosoft);
|
|
369
|
+
this.store.setMicrosoft(value);
|
|
370
|
+
this.reload();
|
|
267
371
|
}
|
|
268
|
-
|
|
269
|
-
|
|
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;
|
|
270
382
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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();
|
|
277
389
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
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;
|
|
284
397
|
}
|
|
285
|
-
const key = randomBytes(32);
|
|
286
|
-
writeFileSync(keyPath, key.toString('hex') + '\n', { mode: 0o600 });
|
|
287
|
-
return key;
|
|
288
398
|
}
|
|
289
|
-
export function
|
|
290
|
-
|
|
291
|
-
const dataDir = resolveDataDir();
|
|
292
|
-
const licenseKeyFromEnvironment = readEnvironment('FLUXMAIL_LICENSE_KEY') !== undefined;
|
|
293
|
-
applyStoredConfig(dataDir);
|
|
294
|
-
const port = readPort('FLUXMAIL_PORT', 8977);
|
|
295
|
-
const oauthPort = readPort('FLUXMAIL_OAUTH_PORT', 8976);
|
|
296
|
-
const publicUrlConfigured = readEnvironment('FLUXMAIL_PUBLIC_URL') !== undefined;
|
|
297
|
-
const publicUrl = readPublicUrl(port);
|
|
298
|
-
const databasePath = readEnvironment('FLUXMAIL_DB_PATH');
|
|
299
|
-
const authModeEnv = readEnvironment('FLUXMAIL_AUTH') ?? 'apikey';
|
|
300
|
-
if (authModeEnv !== 'apikey' && authModeEnv !== 'none') {
|
|
301
|
-
throw new Error(`FLUXMAIL_AUTH must be "apikey" or "none", got "${authModeEnv}"`);
|
|
302
|
-
}
|
|
303
|
-
const config = {
|
|
304
|
-
dataDir,
|
|
305
|
-
dbPath: databasePath ? expandHome(databasePath) : path.join(dataDir, 'fluxmail.db'),
|
|
306
|
-
encryptionKey: loadEncryptionKey(dataDir),
|
|
307
|
-
port,
|
|
308
|
-
publicUrl,
|
|
309
|
-
publicUrlConfigured,
|
|
310
|
-
oauthPort,
|
|
311
|
-
oauthHost: readEnvironment('FLUXMAIL_OAUTH_HOST') ?? '127.0.0.1',
|
|
312
|
-
authMode: authModeEnv,
|
|
313
|
-
maxAttachmentBytes: readMaxAttachmentBytes(),
|
|
314
|
-
licenseServerUrl: (readEnvironment('FLUXMAIL_LICENSE_SERVER_URL') ?? DEFAULT_LICENSE_SERVER_URL).replace(/\/+$/, ''),
|
|
315
|
-
};
|
|
316
|
-
const licenseKey = readEnvironment('FLUXMAIL_LICENSE_KEY')?.trim();
|
|
317
|
-
if (licenseKey) {
|
|
318
|
-
config.licenseKey = licenseKey;
|
|
319
|
-
config.licenseKeyFromEnvironment = licenseKeyFromEnvironment;
|
|
320
|
-
}
|
|
321
|
-
const clientId = readEnvironment('GOOGLE_CLIENT_ID');
|
|
322
|
-
const clientSecret = readEnvironment('GOOGLE_CLIENT_SECRET');
|
|
323
|
-
if (clientId && clientSecret) {
|
|
324
|
-
config.google = { clientId, clientSecret };
|
|
325
|
-
}
|
|
326
|
-
const microsoftClientId = readEnvironment('MICROSOFT_CLIENT_ID')?.trim();
|
|
327
|
-
if (microsoftClientId) {
|
|
328
|
-
const microsoftClientSecret = readEnvironment('MICROSOFT_CLIENT_SECRET')?.trim();
|
|
329
|
-
config.microsoft = {
|
|
330
|
-
clientId: microsoftClientId,
|
|
331
|
-
tenantId: readEnvironment('MICROSOFT_TENANT_ID')?.trim() || 'common',
|
|
332
|
-
...(microsoftClientSecret ? { clientSecret: microsoftClientSecret } : {}),
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
return config;
|
|
399
|
+
export function createConfigurationService(deployment, db) {
|
|
400
|
+
return new ConfigurationService(deployment, db);
|
|
336
401
|
}
|
|
337
402
|
//# sourceMappingURL=config.js.map
|