@serve.zone/coremail 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.smartconfig.json +50 -0
  2. package/changelog.md +10 -0
  3. package/cli.js +4 -0
  4. package/dist_ts/00_commitinfo_data.d.ts +8 -0
  5. package/dist_ts/00_commitinfo_data.js +9 -0
  6. package/dist_ts/classes.auth.d.ts +45 -0
  7. package/dist_ts/classes.auth.js +204 -0
  8. package/dist_ts/classes.config.d.ts +2 -0
  9. package/dist_ts/classes.config.js +72 -0
  10. package/dist_ts/classes.coremail.d.ts +41 -0
  11. package/dist_ts/classes.coremail.js +155 -0
  12. package/dist_ts/classes.desiredstate.d.ts +40 -0
  13. package/dist_ts/classes.desiredstate.js +382 -0
  14. package/dist_ts/classes.gateway.d.ts +62 -0
  15. package/dist_ts/classes.gateway.js +669 -0
  16. package/dist_ts/classes.inbound.d.ts +49 -0
  17. package/dist_ts/classes.inbound.js +702 -0
  18. package/dist_ts/classes.maintenance.d.ts +16 -0
  19. package/dist_ts/classes.maintenance.js +339 -0
  20. package/dist_ts/classes.models.d.ts +161 -0
  21. package/dist_ts/classes.models.js +595 -0
  22. package/dist_ts/classes.server.d.ts +40 -0
  23. package/dist_ts/classes.server.js +309 -0
  24. package/dist_ts/classes.storage.d.ts +32 -0
  25. package/dist_ts/classes.storage.js +317 -0
  26. package/dist_ts/classes.submissions.d.ts +33 -0
  27. package/dist_ts/classes.submissions.js +385 -0
  28. package/dist_ts/classes.transfer.d.ts +41 -0
  29. package/dist_ts/classes.transfer.js +289 -0
  30. package/dist_ts/coremail.cursor.d.ts +20 -0
  31. package/dist_ts/coremail.cursor.js +191 -0
  32. package/dist_ts/coremail.log.d.ts +3 -0
  33. package/dist_ts/coremail.log.js +11 -0
  34. package/dist_ts/coremail.mime.d.ts +8 -0
  35. package/dist_ts/coremail.mime.js +63 -0
  36. package/dist_ts/coremail.persistence.d.ts +148 -0
  37. package/dist_ts/coremail.persistence.js +695 -0
  38. package/dist_ts/coremail.quota.d.ts +29 -0
  39. package/dist_ts/coremail.quota.js +149 -0
  40. package/dist_ts/coremail.selectors.d.ts +3 -0
  41. package/dist_ts/coremail.selectors.js +21 -0
  42. package/dist_ts/coremail.validation.d.ts +9 -0
  43. package/dist_ts/coremail.validation.js +325 -0
  44. package/dist_ts/index.d.ts +4 -0
  45. package/dist_ts/index.js +39 -0
  46. package/dist_ts/interfaces.d.ts +92 -0
  47. package/dist_ts/interfaces.js +2 -0
  48. package/dist_ts/plugins.d.ts +11 -0
  49. package/dist_ts/plugins.js +12 -0
  50. package/license.md +19 -0
  51. package/package.json +48 -0
  52. package/readme.md +199 -0
  53. package/ts/00_commitinfo_data.ts +8 -0
  54. package/ts/classes.auth.ts +321 -0
  55. package/ts/classes.config.ts +104 -0
  56. package/ts/classes.coremail.ts +249 -0
  57. package/ts/classes.desiredstate.ts +508 -0
  58. package/ts/classes.gateway.ts +811 -0
  59. package/ts/classes.inbound.ts +898 -0
  60. package/ts/classes.maintenance.ts +335 -0
  61. package/ts/classes.models.ts +530 -0
  62. package/ts/classes.server.ts +444 -0
  63. package/ts/classes.storage.ts +448 -0
  64. package/ts/classes.submissions.ts +554 -0
  65. package/ts/classes.transfer.ts +379 -0
  66. package/ts/coremail.cursor.ts +328 -0
  67. package/ts/coremail.log.ts +21 -0
  68. package/ts/coremail.mime.ts +94 -0
  69. package/ts/coremail.persistence.ts +1156 -0
  70. package/ts/coremail.quota.ts +214 -0
  71. package/ts/coremail.selectors.ts +35 -0
  72. package/ts/coremail.validation.ts +446 -0
  73. package/ts/index.ts +38 -0
  74. package/ts/interfaces.ts +109 -0
  75. package/ts/plugins.ts +11 -0
@@ -0,0 +1,321 @@
1
+ import * as plugins from './plugins.js';
2
+ import type {
3
+ ICoreMailAuthLimiterSnapshot,
4
+ ICoreMailControlSession,
5
+ ICoreMailPeer,
6
+ ICoreMailWorkloadSession,
7
+ TCoreMailSession,
8
+ } from './interfaces.js';
9
+ import {
10
+ requireCoreMailIdentifier,
11
+ requireCoreMailSafeInteger,
12
+ } from './coremail.selectors.js';
13
+
14
+ type TCoreMailDesiredState = plugins.serveZoneInterfaces.data.ICoreMailDesiredState;
15
+ type TCoreMailBinding = plugins.serveZoneInterfaces.data.ICoreMailBindingDesiredState;
16
+ type TCoreMailVerifier =
17
+ plugins.serveZoneInterfaces.data.ICoreMailBindingCredentialVerifier;
18
+
19
+ export const coreMailSessionKey = 'coremail.authenticated-session';
20
+ export const coreMailPreAuthDeadlineKey = 'coremail.pre-auth-deadline';
21
+
22
+ export class CoreMailAuthenticationError extends Error {
23
+ public constructor() {
24
+ super('CoreMail authentication failed.');
25
+ this.name = 'CoreMailAuthenticationError';
26
+ }
27
+ }
28
+
29
+ export class CoreMailAuthorizationError extends Error {
30
+ public constructor() {
31
+ super('CoreMail authority is unavailable.');
32
+ this.name = 'CoreMailAuthorizationError';
33
+ }
34
+ }
35
+
36
+ export class CoreMailCapabilityDeniedError extends Error {
37
+ public constructor() {
38
+ super('CoreMail operation is not permitted by this authority.');
39
+ this.name = 'CoreMailCapabilityDeniedError';
40
+ }
41
+ }
42
+
43
+ export class CoreMailAuthLimiter {
44
+ private inFlight = 0;
45
+ private tokens: number;
46
+ private lastRefillAt: number;
47
+
48
+ public constructor(
49
+ private readonly maximumInFlight = 4,
50
+ private readonly capacity = 8,
51
+ private readonly refillPerSecond = 2,
52
+ private readonly now: () => number = Date.now,
53
+ ) {
54
+ this.tokens = capacity;
55
+ this.lastRefillAt = now();
56
+ }
57
+
58
+ private refill(): void {
59
+ const now = this.now();
60
+ const elapsedMs = Math.max(0, now - this.lastRefillAt);
61
+ if (elapsedMs < 1_000) {
62
+ return;
63
+ }
64
+ const intervals = Math.floor(elapsedMs / 1_000);
65
+ this.tokens = Math.min(
66
+ this.capacity,
67
+ this.tokens + (intervals * this.refillPerSecond),
68
+ );
69
+ this.lastRefillAt += intervals * 1_000;
70
+ }
71
+
72
+ public snapshot(): ICoreMailAuthLimiterSnapshot {
73
+ this.refill();
74
+ return { inFlight: this.inFlight, tokens: this.tokens };
75
+ }
76
+
77
+ public async run<T>(workArg: () => Promise<T>): Promise<T> {
78
+ this.refill();
79
+ if (this.inFlight >= this.maximumInFlight || this.tokens < 1) {
80
+ throw new CoreMailAuthenticationError();
81
+ }
82
+ this.tokens--;
83
+ this.inFlight++;
84
+ try {
85
+ return await workArg();
86
+ } finally {
87
+ this.inFlight--;
88
+ }
89
+ }
90
+ }
91
+
92
+ const findAcceptedVerifier = (
93
+ credentialsArg: TCoreMailVerifier[],
94
+ credentialIdArg: string,
95
+ credentialVersionArg: number,
96
+ nowArg: number,
97
+ ): TCoreMailVerifier | undefined => credentialsArg.find((credentialArg) =>
98
+ credentialArg.credentialId === credentialIdArg
99
+ && credentialArg.version === credentialVersionArg
100
+ && (
101
+ credentialArg.state === 'current'
102
+ || (
103
+ credentialArg.state === 'retiring'
104
+ && credentialArg.acceptUntil !== undefined
105
+ && credentialArg.acceptUntil >= nowArg
106
+ )
107
+ )
108
+ );
109
+
110
+ export class CoreMailAuth {
111
+ public readonly limiter = new CoreMailAuthLimiter();
112
+
113
+ public constructor(
114
+ private readonly controlBootstrap:
115
+ plugins.serveZoneInterfaces.data.ICoreMailControlBootstrap,
116
+ private readonly getActiveDesiredState: () =>
117
+ Promise<TCoreMailDesiredState | null>,
118
+ private readonly now: () => number = Date.now,
119
+ ) {}
120
+
121
+ private requireFreshPeer(peerArg: ICoreMailPeer): void {
122
+ const deadline = peerArg.context.state[coreMailPreAuthDeadlineKey];
123
+ if (
124
+ peerArg.data.has(coreMailSessionKey)
125
+ || !Number.isSafeInteger(deadline)
126
+ || Number(deadline) < this.now()
127
+ ) {
128
+ peerArg.close(4401, 'authentication required');
129
+ throw new CoreMailAuthenticationError();
130
+ }
131
+ }
132
+
133
+ private async verify(
134
+ verifierArg: TCoreMailVerifier | undefined,
135
+ secretArg: string,
136
+ ): Promise<void> {
137
+ if (
138
+ !verifierArg
139
+ || typeof secretArg !== 'string'
140
+ || secretArg.length < 32
141
+ || secretArg.length > 512
142
+ ) {
143
+ throw new CoreMailAuthenticationError();
144
+ }
145
+ const verified = await this.limiter.run(async () =>
146
+ plugins.argon2.verify(verifierArg.verificationHash, secretArg)
147
+ );
148
+ if (!verified) {
149
+ throw new CoreMailAuthenticationError();
150
+ }
151
+ }
152
+
153
+ public async authenticateControl(
154
+ peerArg: ICoreMailPeer,
155
+ requestArg:
156
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateControl['request'],
157
+ ): Promise<ICoreMailControlSession> {
158
+ this.requireFreshPeer(peerArg);
159
+ try {
160
+ const credentialId = requireCoreMailIdentifier(
161
+ requestArg.credentialId,
162
+ 'credentialId',
163
+ );
164
+ const credentialVersion = requireCoreMailSafeInteger(
165
+ requestArg.credentialVersion,
166
+ 'credentialVersion',
167
+ 1,
168
+ );
169
+ const verifier = findAcceptedVerifier(
170
+ this.controlBootstrap.credentials,
171
+ credentialId,
172
+ credentialVersion,
173
+ this.now(),
174
+ );
175
+ await this.verify(verifier, requestArg.credentialSecret);
176
+ const session = Object.freeze<ICoreMailControlSession>({
177
+ kind: 'control',
178
+ peerId: peerArg.id,
179
+ credentialId,
180
+ credentialVersion,
181
+ authenticatedAt: this.now(),
182
+ });
183
+ peerArg.data.set(coreMailSessionKey, session);
184
+ return session;
185
+ } catch {
186
+ peerArg.close(4401, 'authentication failed');
187
+ throw new CoreMailAuthenticationError();
188
+ }
189
+ }
190
+
191
+ public async authenticateWorkload(
192
+ peerArg: ICoreMailPeer,
193
+ requestArg:
194
+ plugins.serveZoneInterfaces.requests.coremail.IReq_CoreMailAuthenticateWorkload['request'],
195
+ ): Promise<ICoreMailWorkloadSession> {
196
+ this.requireFreshPeer(peerArg);
197
+ try {
198
+ const desiredState = await this.getActiveDesiredState();
199
+ const bindingId = requireCoreMailIdentifier(
200
+ requestArg.bindingId,
201
+ 'bindingId',
202
+ );
203
+ const credentialId = requireCoreMailIdentifier(
204
+ requestArg.credentialId,
205
+ 'credentialId',
206
+ );
207
+ const credentialVersion = requireCoreMailSafeInteger(
208
+ requestArg.credentialVersion,
209
+ 'credentialVersion',
210
+ 1,
211
+ );
212
+ const binding = desiredState?.bindings.find((bindingArg) =>
213
+ bindingArg.bindingId === bindingId
214
+ && bindingArg.state !== 'disabled'
215
+ );
216
+ const verifier = binding && findAcceptedVerifier(
217
+ binding.credentials,
218
+ credentialId,
219
+ credentialVersion,
220
+ this.now(),
221
+ );
222
+ await this.verify(verifier, requestArg.credentialSecret);
223
+ const allowedOperations =
224
+ plugins.serveZoneInterfaces.data.resolveCoreMailWorkloadOperations(
225
+ binding!.state,
226
+ binding!.capabilities,
227
+ );
228
+ const session = Object.freeze<ICoreMailWorkloadSession>({
229
+ kind: 'workload',
230
+ peerId: peerArg.id,
231
+ tenantId: binding!.tenantId,
232
+ serviceId: binding!.serviceId,
233
+ bindingId: binding!.bindingId,
234
+ bindingRevision: binding!.revision,
235
+ configEpoch: desiredState!.configEpoch,
236
+ credentialId,
237
+ credentialVersion,
238
+ bindingState: binding!.state as 'active' | 'draining',
239
+ capabilities: Object.freeze([...binding!.capabilities]) as
240
+ plugins.serveZoneInterfaces.data.TCoreMailCapability[],
241
+ allowedOperations: Object.freeze([...allowedOperations]) as
242
+ plugins.serveZoneInterfaces.data.TCoreMailWorkloadOperation[],
243
+ coreMailTransferOrigin: desiredState!.gateway.coreMailTransferOrigin,
244
+ authenticatedAt: this.now(),
245
+ });
246
+ peerArg.data.set(coreMailSessionKey, session);
247
+ return session;
248
+ } catch {
249
+ peerArg.close(4401, 'authentication failed');
250
+ throw new CoreMailAuthenticationError();
251
+ }
252
+ }
253
+
254
+ public requireControlSession(peerArg: ICoreMailPeer): ICoreMailControlSession {
255
+ const session = peerArg.data.get(coreMailSessionKey) as TCoreMailSession | undefined;
256
+ if (!session || session.kind !== 'control' || session.peerId !== peerArg.id) {
257
+ peerArg.close(4403, 'authority unavailable');
258
+ throw new CoreMailAuthorizationError();
259
+ }
260
+ const verifier = findAcceptedVerifier(
261
+ this.controlBootstrap.credentials,
262
+ session.credentialId,
263
+ session.credentialVersion,
264
+ this.now(),
265
+ );
266
+ if (!verifier) {
267
+ peerArg.close(4403, 'authority expired');
268
+ throw new CoreMailAuthorizationError();
269
+ }
270
+ return session;
271
+ }
272
+
273
+ public async requireWorkloadSession(
274
+ peerArg: ICoreMailPeer,
275
+ operationArg: plugins.serveZoneInterfaces.data.TCoreMailWorkloadOperation,
276
+ ): Promise<{
277
+ session: ICoreMailWorkloadSession;
278
+ binding: TCoreMailBinding;
279
+ }> {
280
+ const session = peerArg.data.get(coreMailSessionKey) as TCoreMailSession | undefined;
281
+ if (!session || session.kind !== 'workload' || session.peerId !== peerArg.id) {
282
+ peerArg.close(4403, 'authority unavailable');
283
+ throw new CoreMailAuthorizationError();
284
+ }
285
+ const desiredState = await this.getActiveDesiredState();
286
+ const binding = desiredState?.bindings.find((bindingArg) =>
287
+ bindingArg.bindingId === session.bindingId
288
+ && bindingArg.serviceId === session.serviceId
289
+ && bindingArg.tenantId === session.tenantId
290
+ && bindingArg.revision === session.bindingRevision
291
+ && bindingArg.state === session.bindingState
292
+ );
293
+ const verifier = binding && findAcceptedVerifier(
294
+ binding.credentials,
295
+ session.credentialId,
296
+ session.credentialVersion,
297
+ this.now(),
298
+ );
299
+ if (
300
+ !desiredState
301
+ || desiredState.configEpoch !== session.configEpoch
302
+ || !binding
303
+ || !verifier
304
+ ) {
305
+ peerArg.close(4403, 'authority expired');
306
+ throw new CoreMailAuthorizationError();
307
+ }
308
+ const allowedOperations =
309
+ plugins.serveZoneInterfaces.data.resolveCoreMailWorkloadOperations(
310
+ binding.state,
311
+ binding.capabilities,
312
+ );
313
+ if (
314
+ !session.allowedOperations.includes(operationArg)
315
+ || !allowedOperations.includes(operationArg)
316
+ ) {
317
+ throw new CoreMailCapabilityDeniedError();
318
+ }
319
+ return { session, binding };
320
+ }
321
+ }
@@ -0,0 +1,104 @@
1
+ import * as plugins from './plugins.js';
2
+ import type { ICoreMailConfig } from './interfaces.js';
3
+
4
+ const requireEnv = (keyArg: string): string => {
5
+ const value = process.env[keyArg];
6
+ if (!value || value.trim() !== value) {
7
+ throw new Error(`CoreMail requires a non-empty canonical ${keyArg}.`);
8
+ }
9
+ return value;
10
+ };
11
+
12
+ const parsePositiveInteger = (valueArg: string, keyArg: string, maximumArg: number): number => {
13
+ if (!/^[1-9][0-9]*$/.test(valueArg)) {
14
+ throw new Error(`${keyArg} must be a positive integer.`);
15
+ }
16
+ const value = Number(valueArg);
17
+ if (!Number.isSafeInteger(value) || value > maximumArg) {
18
+ throw new Error(`${keyArg} is outside the supported range.`);
19
+ }
20
+ return value;
21
+ };
22
+
23
+ const parsePlainJson = (valueArg: string, keyArg: string): Record<string, unknown> => {
24
+ const parsed: unknown = JSON.parse(valueArg);
25
+ if (
26
+ !parsed
27
+ || typeof parsed !== 'object'
28
+ || Array.isArray(parsed)
29
+ || Object.getPrototypeOf(parsed) !== Object.prototype
30
+ ) {
31
+ throw new Error(`${keyArg} must contain one plain JSON object.`);
32
+ }
33
+ return parsed as Record<string, unknown>;
34
+ };
35
+
36
+ const parseHostnames = (valueArg: string): string[] => {
37
+ const hostnames = valueArg.split(',');
38
+ if (
39
+ hostnames.length === 0
40
+ || hostnames.length > 16
41
+ || hostnames.some((entryArg) =>
42
+ entryArg.trim() !== entryArg
43
+ || !/^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/.test(entryArg)
44
+ || entryArg.includes('..')
45
+ )
46
+ || new Set(hostnames).size !== hostnames.length
47
+ ) {
48
+ throw new Error('COREMAIL_HOSTNAMES must be a unique canonical hostname list.');
49
+ }
50
+ return hostnames;
51
+ };
52
+
53
+ export const readCoreMailConfig = (): ICoreMailConfig => {
54
+ const controlBootstrap =
55
+ plugins.serveZoneInterfaces.data.normalizeCoreMailControlBootstrap(
56
+ parsePlainJson(
57
+ requireEnv(plugins.serveZoneInterfaces.data.coreMailRuntimeKeys.controlBootstrap),
58
+ plugins.serveZoneInterfaces.data.coreMailRuntimeKeys.controlBootstrap,
59
+ ),
60
+ );
61
+ const serviceId = requireEnv('COREMAIL_SERVICE_ID');
62
+ if (controlBootstrap.coreMailServiceId !== serviceId) {
63
+ throw new Error('CoreMail control bootstrap is bound to another service.');
64
+ }
65
+ const storageDescriptor = parsePlainJson(
66
+ requireEnv('COREMAIL_STORAGE_DESCRIPTOR'),
67
+ 'COREMAIL_STORAGE_DESCRIPTOR',
68
+ );
69
+ return Object.freeze({
70
+ port: parsePositiveInteger(process.env.COREMAIL_PORT ?? '3000', 'COREMAIL_PORT', 65_535),
71
+ hostnames: Object.freeze(
72
+ parseHostnames(process.env.COREMAIL_HOSTNAMES ?? 'coremail.serve.zone'),
73
+ ) as unknown as string[],
74
+ mongoDescriptor: Object.freeze({
75
+ mongoDbUrl: requireEnv('COREMAIL_MONGODB_URL'),
76
+ mongoDbName: requireEnv('COREMAIL_MONGODB_NAME'),
77
+ maxPoolSize: parsePositiveInteger(
78
+ process.env.COREMAIL_MONGODB_MAX_POOL_SIZE ?? '50',
79
+ 'COREMAIL_MONGODB_MAX_POOL_SIZE',
80
+ 500,
81
+ ),
82
+ maxIdleTimeMS: 300_000,
83
+ serverSelectionTimeoutMS: 10_000,
84
+ socketTimeoutMS: 30_000,
85
+ }),
86
+ storageDescriptor: Object.freeze(storageDescriptor),
87
+ bucketName: requireEnv('COREMAIL_BUCKET_NAME'),
88
+ controlBootstrap,
89
+ resolveRuntimeSecret: requireEnv,
90
+ replica: Object.freeze({
91
+ taskId: requireEnv('COREMAIL_TASK_ID'),
92
+ serviceId,
93
+ rolloutId: requireEnv('COREMAIL_ROLLOUT_ID'),
94
+ rolloutGeneration: parsePositiveInteger(
95
+ requireEnv('COREMAIL_ROLLOUT_GENERATION'),
96
+ 'COREMAIL_ROLLOUT_GENERATION',
97
+ Number.MAX_SAFE_INTEGER,
98
+ ),
99
+ imageDigest: plugins.serveZoneInterfaces.data.normalizeCoreMailSha256(
100
+ requireEnv('COREMAIL_IMAGE_DIGEST'),
101
+ ),
102
+ }),
103
+ });
104
+ };
@@ -0,0 +1,249 @@
1
+ import * as plugins from './plugins.js';
2
+ import { CoreMailAuth } from './classes.auth.js';
3
+ import { readCoreMailConfig } from './classes.config.js';
4
+ import { CoreMailDesiredStateService } from './classes.desiredstate.js';
5
+ import {
6
+ CoreMailGateway,
7
+ CoreMailOutboundWorker,
8
+ } from './classes.gateway.js';
9
+ import { CoreMailInboundService } from './classes.inbound.js';
10
+ import { CoreMailMaintenanceService } from './classes.maintenance.js';
11
+ import {
12
+ createCoreMailModels,
13
+ prepareCoreMailModelsForTransactions,
14
+ type ICoreMailModels,
15
+ } from './classes.models.js';
16
+ import { CoreMailServer } from './classes.server.js';
17
+ import { CoreMailStorage } from './classes.storage.js';
18
+ import { CoreMailSubmissionService } from './classes.submissions.js';
19
+ import { CoreMailTransferService } from './classes.transfer.js';
20
+ import type { ICoreMailConfig } from './interfaces.js';
21
+
22
+ type TLifecycleState =
23
+ | 'new'
24
+ | 'starting'
25
+ | 'running'
26
+ | 'stopping'
27
+ | 'failed'
28
+ | 'stopped';
29
+
30
+ export class CoreMail {
31
+ public readonly config: ICoreMailConfig;
32
+ public readonly database: plugins.smartdata.SmartdataDb;
33
+ public readonly models: ICoreMailModels;
34
+ public readonly storage: CoreMailStorage;
35
+ public readonly transfers: CoreMailTransferService;
36
+ public readonly desiredState: CoreMailDesiredStateService;
37
+ public readonly auth: CoreMailAuth;
38
+ public readonly submissions: CoreMailSubmissionService;
39
+ public readonly inbound: CoreMailInboundService;
40
+ public readonly maintenance: CoreMailMaintenanceService;
41
+ public readonly gateway: CoreMailGateway;
42
+ public readonly server: CoreMailServer;
43
+ public readonly outboundWorker: CoreMailOutboundWorker;
44
+
45
+ private lifecycleState: TLifecycleState = 'new';
46
+ private startPromise: Promise<void> | null = null;
47
+ private stopPromise: Promise<void> | null = null;
48
+ private databaseStarted = false;
49
+ private storageStarted = false;
50
+ private gatewayStarted = false;
51
+ private serverStarted = false;
52
+ private workerStarted = false;
53
+ private maintenanceStarted = false;
54
+
55
+ public constructor(configArg: ICoreMailConfig = readCoreMailConfig()) {
56
+ this.config = configArg;
57
+ this.database = new plugins.smartdata.SmartdataDb(configArg.mongoDescriptor);
58
+ this.models = createCoreMailModels(this.database);
59
+ this.storage = new CoreMailStorage(
60
+ configArg.storageDescriptor,
61
+ configArg.bucketName,
62
+ );
63
+ this.transfers = new CoreMailTransferService(this.models, this.storage);
64
+ let server: CoreMailServer | undefined;
65
+ this.desiredState = new CoreMailDesiredStateService(
66
+ this.database,
67
+ this.models,
68
+ this.storage,
69
+ this.transfers,
70
+ configArg.replica,
71
+ (bindingArg, configEpochArg) =>
72
+ server?.activeSessionCounts(bindingArg, configEpochArg) ?? [],
73
+ );
74
+ this.auth = new CoreMailAuth(
75
+ configArg.controlBootstrap,
76
+ this.desiredState.getActiveDesiredState,
77
+ );
78
+ this.submissions = new CoreMailSubmissionService(
79
+ this.database,
80
+ this.models,
81
+ this.storage,
82
+ this.transfers,
83
+ );
84
+ this.inbound = new CoreMailInboundService(
85
+ this.database,
86
+ this.models,
87
+ this.transfers,
88
+ configArg.replica.serviceId,
89
+ this.desiredState.getActiveDesiredState,
90
+ configArg.resolveRuntimeSecret,
91
+ );
92
+ this.gateway = new CoreMailGateway(
93
+ configArg,
94
+ this.models,
95
+ this.transfers,
96
+ this.inbound,
97
+ this.desiredState.getActiveDesiredState,
98
+ );
99
+ this.maintenance = new CoreMailMaintenanceService(this.models, this.storage);
100
+ this.server = server = new CoreMailServer(
101
+ configArg,
102
+ this.database,
103
+ this.storage,
104
+ this.transfers,
105
+ this.desiredState,
106
+ this.auth,
107
+ this.submissions,
108
+ this.inbound,
109
+ this.gateway,
110
+ );
111
+ this.outboundWorker = new CoreMailOutboundWorker(
112
+ this.models,
113
+ this.transfers,
114
+ this.gateway,
115
+ configArg.replica.taskId,
116
+ configArg.replica.serviceId,
117
+ );
118
+ }
119
+
120
+ public async start(): Promise<void> {
121
+ if (this.lifecycleState === 'running') return;
122
+ if (this.lifecycleState === 'starting') {
123
+ await this.startPromise;
124
+ return;
125
+ }
126
+ if (this.lifecycleState !== 'new') {
127
+ throw new Error(`CoreMail cannot start from lifecycle state "${this.lifecycleState}".`);
128
+ }
129
+ this.lifecycleState = 'starting';
130
+ const startPromise = this.startInternal();
131
+ this.startPromise = startPromise;
132
+ try {
133
+ await startPromise;
134
+ this.lifecycleState = 'running';
135
+ } catch (error) {
136
+ try {
137
+ await this.stopStartedResources();
138
+ this.lifecycleState = 'stopped';
139
+ } catch (cleanupError) {
140
+ this.lifecycleState = 'failed';
141
+ throw new AggregateError(
142
+ [error, cleanupError],
143
+ 'CoreMail startup and cleanup both failed.',
144
+ );
145
+ }
146
+ throw error;
147
+ } finally {
148
+ if (this.startPromise === startPromise) {
149
+ this.startPromise = null;
150
+ }
151
+ }
152
+ }
153
+
154
+ private async startInternal(): Promise<void> {
155
+ await this.database.init();
156
+ this.databaseStarted = true;
157
+ await Promise.all(
158
+ Object.values(this.models).map((modelArg) => modelArg.init()),
159
+ );
160
+ await prepareCoreMailModelsForTransactions(this.models);
161
+ await this.storage.start();
162
+ this.storageStarted = true;
163
+ await this.desiredState.start();
164
+ this.gateway.start();
165
+ this.gatewayStarted = true;
166
+ await this.server.start();
167
+ this.serverStarted = true;
168
+ this.maintenance.start();
169
+ this.maintenanceStarted = true;
170
+ this.outboundWorker.start();
171
+ this.workerStarted = true;
172
+ }
173
+
174
+ public async stop(): Promise<void> {
175
+ if (this.lifecycleState === 'stopped') return;
176
+ if (this.lifecycleState === 'stopping') {
177
+ await this.stopPromise;
178
+ return;
179
+ }
180
+ if (this.lifecycleState === 'starting') {
181
+ await this.startPromise?.catch(() => undefined);
182
+ }
183
+ this.lifecycleState = 'stopping';
184
+ const stopPromise = this.stopStartedResources();
185
+ this.stopPromise = stopPromise;
186
+ try {
187
+ await stopPromise;
188
+ this.lifecycleState = 'stopped';
189
+ } catch (error) {
190
+ this.lifecycleState = 'failed';
191
+ throw error;
192
+ } finally {
193
+ if (this.stopPromise === stopPromise) {
194
+ this.stopPromise = null;
195
+ }
196
+ }
197
+ }
198
+
199
+ private async stopStartedResources(): Promise<void> {
200
+ const errors: unknown[] = [];
201
+ const stopOne = async (
202
+ startedArg: boolean,
203
+ stopArg: () => Promise<void>,
204
+ markStoppedArg: () => void,
205
+ ) => {
206
+ if (!startedArg) return;
207
+ try {
208
+ await stopArg();
209
+ markStoppedArg();
210
+ } catch (error) {
211
+ errors.push(error);
212
+ }
213
+ };
214
+ await stopOne(
215
+ this.workerStarted,
216
+ () => this.outboundWorker.stop(),
217
+ () => { this.workerStarted = false; },
218
+ );
219
+ await stopOne(
220
+ this.serverStarted,
221
+ () => this.server.stop(),
222
+ () => { this.serverStarted = false; },
223
+ );
224
+ await stopOne(
225
+ this.maintenanceStarted,
226
+ () => this.maintenance.stop(),
227
+ () => { this.maintenanceStarted = false; },
228
+ );
229
+ await stopOne(
230
+ this.gatewayStarted,
231
+ () => this.gateway.stop(),
232
+ () => { this.gatewayStarted = false; },
233
+ );
234
+ await this.transfers.stop().catch((error) => errors.push(error));
235
+ await stopOne(
236
+ this.storageStarted,
237
+ () => this.storage.stop(),
238
+ () => { this.storageStarted = false; },
239
+ );
240
+ await stopOne(
241
+ this.databaseStarted,
242
+ () => this.database.close(),
243
+ () => { this.databaseStarted = false; },
244
+ );
245
+ if (errors.length > 0) {
246
+ throw new AggregateError(errors, 'CoreMail lifecycle shutdown failed.');
247
+ }
248
+ }
249
+ }