@serve.zone/dcrouter 18.5.0 → 18.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.
Files changed (34) hide show
  1. package/deno.json +1 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.d.ts +2 -1
  4. package/dist_ts/classes.dcrouter.js +4 -2
  5. package/dist_ts/db/documents/classes.gateway-mail-domain.doc.d.ts +16 -0
  6. package/dist_ts/db/documents/classes.gateway-mail-domain.doc.js +107 -0
  7. package/dist_ts/db/documents/index.d.ts +1 -0
  8. package/dist_ts/db/documents/index.js +2 -1
  9. package/dist_ts/email/classes.gateway-mail-domain.store.d.ts +11 -0
  10. package/dist_ts/email/classes.gateway-mail-domain.store.js +70 -0
  11. package/dist_ts/email/classes.workapp-mail-manager.d.ts +3 -23
  12. package/dist_ts/email/classes.workapp-mail-manager.js +18 -35
  13. package/dist_ts/email/gateway-mail-domain-authority.d.ts +44 -0
  14. package/dist_ts/email/gateway-mail-domain-authority.js +160 -0
  15. package/dist_ts/email/index.d.ts +2 -0
  16. package/dist_ts/email/index.js +3 -1
  17. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +33 -1
  18. package/dist_ts/opsserver/helpers/gateway-client-auth.d.ts +2 -0
  19. package/dist_ts/opsserver/helpers/gateway-client-auth.js +5 -3
  20. package/dist_ts_migrations/index.js +196 -11
  21. package/dist_ts_web/00_commitinfo_data.js +1 -1
  22. package/package.json +2 -2
  23. package/readme.md +1 -1
  24. package/ts/00_commitinfo_data.ts +1 -1
  25. package/ts/classes.dcrouter.ts +3 -1
  26. package/ts/db/documents/classes.gateway-mail-domain.doc.ts +42 -0
  27. package/ts/db/documents/index.ts +1 -0
  28. package/ts/email/classes.gateway-mail-domain.store.ts +89 -0
  29. package/ts/email/classes.workapp-mail-manager.ts +33 -70
  30. package/ts/email/gateway-mail-domain-authority.ts +222 -0
  31. package/ts/email/index.ts +2 -0
  32. package/ts/opsserver/handlers/gatewayclient.handler.ts +43 -1
  33. package/ts/opsserver/helpers/gateway-client-auth.ts +6 -2
  34. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.5.0',
6
+ version: '18.6.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHNfd2ViLzAwX2NvbW1pdGluZm9fZGF0YS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQTs7R0FFRztBQUNILE1BQU0sQ0FBQyxNQUFNLFVBQVUsR0FBRztJQUN4QixJQUFJLEVBQUUsc0JBQXNCO0lBQzVCLE9BQU8sRUFBRSxRQUFRO0lBQ2pCLFdBQVcsRUFBRSwwRUFBMEU7Q0FDeEYsQ0FBQSJ9
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "18.5.0",
4
+ "version": "18.6.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -64,7 +64,7 @@
64
64
  "@push.rocks/smartvpn": "1.22.0",
65
65
  "@push.rocks/taskbuffer": "^8.0.2",
66
66
  "@serve.zone/catalog": "^2.21.1",
67
- "@serve.zone/interfaces": "^19.7.0",
67
+ "@serve.zone/interfaces": "^19.8.0",
68
68
  "@serve.zone/remoteingress": "^5.1.0",
69
69
  "@tsclass/tsclass": "^9.5.1",
70
70
  "@types/qrcode": "^1.5.6",
package/readme.md CHANGED
@@ -472,7 +472,7 @@ await route.toggle(true);
472
472
 
473
473
  Use `@serve.zone/dcrouter/interfaces` or `@serve.zone/dcrouter-interfaces` when you want dcrouter-local raw TypedRequest contracts instead of resource managers. Use `@serve.zone/interfaces` for the canonical machine-facing gateway client route, DNS, domain, and mail contracts shared with Onebox and Cloudly.
474
474
 
475
- Gateway-client mail contracts let Cloudly or Onebox claim exact app addresses, attach inbound `smtpForward` targets, enable managed outbound SMTP credentials, rotate those credentials, enqueue service mail, and query delivery status through TypedRequest. Managed SMTP users can only send as their exact claimed address; mismatched envelope or header `From` values are rejected before relay. Typed submissions may set `replyTo` to one bare printable-ASCII mailbox address. Dcrouter validates it authoritatively and renders one `Reply-To` header; custom headers remain unable to override `Reply-To` or any other protected MIME field. Delivery status queries return the dcrouter spool item state, including deferred SmartMTA errors and next retry timestamps when outbound delivery is temporarily delayed.
475
+ Gateway-client mail contracts let Cloudly or Onebox claim exact app addresses, attach inbound `smtpForward` targets, enable managed outbound SMTP credentials, rotate those credentials, enqueue service mail, and query delivery status through TypedRequest. `getGatewayClientMailDomainCount` requires an authenticated, enabled gateway-client credential with the `gateway-clients:read` scope and `readMail` capability. It returns only that credential owner's distinct domain count; it does not accept a caller-selected owner and does not load bindings, DNS details, or recent mail. Managed SMTP users can only send as their exact claimed address; mismatched envelope or header `From` values are rejected before relay. Typed submissions may set `replyTo` to one bare printable-ASCII mailbox address. Dcrouter validates it authoritatively and renders one `Reply-To` header; custom headers remain unable to override `Reply-To` or any other protected MIME field. Delivery status queries return the dcrouter spool item state, including deferred SmartMTA errors and next retry timestamps when outbound delivery is temporarily delayed.
476
476
 
477
477
  ### Email Operations Views
478
478
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.5.0',
6
+ version: '18.6.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -42,7 +42,7 @@ import { DomainOwnershipError } from './dns/domain-ownership.js';
42
42
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
43
43
  import { SmartAcmeLifecycle } from './acme/classes.smartacme-lifecycle.js';
44
44
  import { AcmePermanentFailureError, classifyAcmeFailure } from './acme/acme-failure-classification.js';
45
- import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, MailDnsSync, MailEdgeEligibility, MailEgressCoordinator, ReleasedRemoteIngressEgressIdentitySource, SmartMtaBlobStorageManager, SmartMtaStorageManager, SmtpAccountManager, WorkAppMailManager, type ISmartMtaBlobStorageConfig, type TSmartMtaQueueItemLike } from './email/index.js';
45
+ import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, GatewayMailDomainStore, MailDnsSync, MailEdgeEligibility, MailEgressCoordinator, ReleasedRemoteIngressEgressIdentitySource, SmartMtaBlobStorageManager, SmartMtaStorageManager, SmtpAccountManager, WorkAppMailManager, type ISmartMtaBlobStorageConfig, type TSmartMtaQueueItemLike } from './email/index.js';
46
46
  import { WebPushManager } from './webpush/index.js';
47
47
  import type { IRoute } from '../ts_interfaces/data/route-management.js';
48
48
  import type { IEmailOutboundEgressStatus, IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailOutboundMode, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
@@ -355,6 +355,7 @@ export class DcRouter {
355
355
  public acmeConfigManager?: AcmeConfigManager;
356
356
  public emailSettingsManager?: EmailSettingsManager;
357
357
  public emailDomainManager?: EmailDomainManager;
358
+ public gatewayMailDomainStore: GatewayMailDomainStore;
358
359
  public workAppMailManager: WorkAppMailManager;
359
360
  /** Single composition owner of the email runtime auth block and route list. */
360
361
  public smtpAccountManager: SmtpAccountManager;
@@ -427,6 +428,7 @@ export class DcRouter {
427
428
 
428
429
  // Resolve all data paths from baseDir
429
430
  this.resolvedPaths = paths.resolvePaths(this.options.baseDir);
431
+ this.gatewayMailDomainStore = new GatewayMailDomainStore();
430
432
  this.workAppMailManager = new WorkAppMailManager(this);
431
433
  this.smtpAccountManager = new SmtpAccountManager(this);
432
434
  this.acceptedEmailSpool = new AcceptedEmailSpool(this);
@@ -0,0 +1,42 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { DcRouterDb } from '../classes.dcrouter-db.js';
3
+ import type { TGatewayClientType } from '../../../ts_interfaces/data/route-management.js';
4
+ import type { IStoredWorkAppMailIdentity } from '../../email/gateway-mail-domain-authority.js';
5
+
6
+ const getDb = () => DcRouterDb.getInstance().getDb();
7
+
8
+ /**
9
+ * Authoritative WorkApp mail identity storage, grouped so one document equals
10
+ * one distinct gateway-client-owned mail domain.
11
+ */
12
+ @plugins.smartdata.Collection(() => getDb())
13
+ export class GatewayMailDomainDoc extends plugins.smartdata.SmartDataDbDoc<
14
+ GatewayMailDomainDoc,
15
+ GatewayMailDomainDoc
16
+ > {
17
+ @plugins.smartdata.unI()
18
+ @plugins.smartdata.svDb()
19
+ public id!: string;
20
+
21
+ @plugins.smartdata.index()
22
+ @plugins.smartdata.svDb()
23
+ public ownerGatewayClientType!: TGatewayClientType;
24
+
25
+ @plugins.smartdata.index()
26
+ @plugins.smartdata.svDb()
27
+ public ownerGatewayClientId!: string;
28
+
29
+ @plugins.smartdata.index()
30
+ @plugins.smartdata.svDb()
31
+ public domain!: string;
32
+
33
+ @plugins.smartdata.svDb()
34
+ public identities: IStoredWorkAppMailIdentity[] = [];
35
+
36
+ @plugins.smartdata.svDb()
37
+ public createdAt!: number;
38
+
39
+ @plugins.smartdata.svDb()
40
+ public updatedAt!: number;
41
+ }
42
+
@@ -43,6 +43,7 @@ export * from './classes.acme-config.doc.js';
43
43
 
44
44
  // Email domain management
45
45
  export * from './classes.email-domain.doc.js';
46
+ export * from './classes.gateway-mail-domain.doc.js';
46
47
  export * from './classes.email-server-settings.doc.js';
47
48
  export * from './classes.smtp-account.doc.js';
48
49
 
@@ -0,0 +1,89 @@
1
+ import { DcRouterDb } from '../db/classes.dcrouter-db.js';
2
+ import { GatewayMailDomainDoc } from '../db/documents/classes.gateway-mail-domain.doc.js';
3
+ import {
4
+ createGatewayMailDomainId,
5
+ groupGatewayMailIdentities,
6
+ normalizeGatewayMailDomain,
7
+ normalizeGatewayMailDomainOwner,
8
+ type IGatewayMailDomainOwner,
9
+ type IStoredWorkAppMailIdentity,
10
+ } from './gateway-mail-domain-authority.js';
11
+
12
+ export interface IGatewayMailDomainStore {
13
+ readAllIdentities(): Promise<IStoredWorkAppMailIdentity[]>;
14
+ writeAllIdentities(identitiesArg: IStoredWorkAppMailIdentity[]): Promise<IStoredWorkAppMailIdentity[]>;
15
+ countDomains(ownerArg: IGatewayMailDomainOwner): Promise<number>;
16
+ }
17
+
18
+ const validateDocument = (documentArg: GatewayMailDomainDoc): IStoredWorkAppMailIdentity[] => {
19
+ const owner = normalizeGatewayMailDomainOwner({
20
+ gatewayClientType: documentArg.ownerGatewayClientType,
21
+ gatewayClientId: documentArg.ownerGatewayClientId,
22
+ });
23
+ const domain = normalizeGatewayMailDomain(documentArg.domain);
24
+ const expectedId = createGatewayMailDomainId(owner, domain);
25
+ if (documentArg.id !== expectedId) {
26
+ throw new Error(`GatewayMailDomainDoc has a non-canonical id: ${documentArg.id}`);
27
+ }
28
+ const groups = groupGatewayMailIdentities(documentArg.identities || []);
29
+ if (groups.length !== 1 || groups[0].id !== expectedId) {
30
+ throw new Error(`GatewayMailDomainDoc identities do not match owner/domain: ${documentArg.id}`);
31
+ }
32
+ return groups[0].identities;
33
+ };
34
+
35
+ export class GatewayMailDomainStore implements IGatewayMailDomainStore {
36
+ public async readAllIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
37
+ const documents = await GatewayMailDomainDoc.getInstances({});
38
+ const identities = documents.flatMap((documentArg) => validateDocument(documentArg));
39
+ return groupGatewayMailIdentities(identities).flatMap((groupArg) => groupArg.identities);
40
+ }
41
+
42
+ public async writeAllIdentities(
43
+ identitiesArg: IStoredWorkAppMailIdentity[],
44
+ ): Promise<IStoredWorkAppMailIdentity[]> {
45
+ const desiredGroups = groupGatewayMailIdentities(identitiesArg);
46
+ const desiredById = new Map(desiredGroups.map((groupArg) => [groupArg.id, groupArg]));
47
+ const db = DcRouterDb.getInstance().getDb();
48
+ const session = db.startSession();
49
+ try {
50
+ await session.withTransaction(async () => {
51
+ const existingDocuments = await GatewayMailDomainDoc.getInstances({}, { session });
52
+ const existingById = new Map(existingDocuments.map((documentArg) => {
53
+ validateDocument(documentArg);
54
+ return [documentArg.id, documentArg];
55
+ }));
56
+ const now = Date.now();
57
+
58
+ for (const group of desiredGroups) {
59
+ const document = existingById.get(group.id) || new GatewayMailDomainDoc();
60
+ document.id = group.id;
61
+ document.ownerGatewayClientType = group.gatewayClientType;
62
+ document.ownerGatewayClientId = group.gatewayClientId;
63
+ document.domain = group.domain;
64
+ document.identities = group.identities;
65
+ document.createdAt = document.createdAt || now;
66
+ document.updatedAt = now;
67
+ await document.save({ session });
68
+ }
69
+ for (const existingDocument of existingDocuments) {
70
+ if (!desiredById.has(existingDocument.id)) {
71
+ await existingDocument.delete({ session });
72
+ }
73
+ }
74
+ });
75
+ } finally {
76
+ await session.endSession();
77
+ }
78
+
79
+ return await this.readAllIdentities();
80
+ }
81
+
82
+ public async countDomains(ownerArg: IGatewayMailDomainOwner): Promise<number> {
83
+ const owner = normalizeGatewayMailDomainOwner(ownerArg);
84
+ return await GatewayMailDomainDoc.getCount({
85
+ ownerGatewayClientType: owner.gatewayClientType,
86
+ ownerGatewayClientId: owner.gatewayClientId,
87
+ });
88
+ }
89
+ }
@@ -4,7 +4,6 @@ import type {
4
4
  IMessageAcceptanceContext,
5
5
  IMessageAcceptanceDecision,
6
6
  IUnifiedEmailServerOptions,
7
- IStorageManagerLike,
8
7
  } from '@push.rocks/smartmta';
9
8
  import type { Buffer } from 'node:buffer';
10
9
  import * as plugins from '../plugins.js';
@@ -12,6 +11,12 @@ import { CachedEmail } from '../db/index.js';
12
11
  import type { ICachedEmailSmtpTransaction } from '../db/documents/classes.cached.email.js';
13
12
  import type { DcRouter } from '../classes.dcrouter.js';
14
13
  import type * as interfaces from '../../ts_interfaces/index.js';
14
+ import { GatewayMailDomainStore, type IGatewayMailDomainStore } from './classes.gateway-mail-domain.store.js';
15
+ import {
16
+ normalizeGatewayMailDomain,
17
+ normalizeGatewayMailDomainOwner,
18
+ type IStoredWorkAppMailIdentity,
19
+ } from './gateway-mail-domain-authority.js';
15
20
 
16
21
  type TSyncRequest = {
17
22
  ownership: interfaces.data.IWorkAppMailOwnership;
@@ -77,33 +82,6 @@ class MailSubmissionValidationError extends Error {
77
82
  }
78
83
  }
79
84
 
80
- interface IStoredWorkAppMailIdentity {
81
- id: string;
82
- externalKey: string;
83
- ownership: interfaces.data.IWorkAppMailOwnership;
84
- address: string;
85
- localPart: string;
86
- domain: string;
87
- enabled: boolean;
88
- displayName?: string;
89
- inbound?: interfaces.data.IWorkAppMailInboundRoute;
90
- inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget;
91
- smtp: {
92
- enabled: boolean;
93
- username: string;
94
- };
95
- createdAt: number;
96
- updatedAt: number;
97
- createdBy: string;
98
- smtpPassword: string;
99
- smtpLastRotatedAt?: number;
100
- }
101
-
102
- interface IStoredWorkAppMailState {
103
- version: 1;
104
- identities: IStoredWorkAppMailIdentity[];
105
- }
106
-
107
85
  /**
108
86
  * WorkApp-generated routes and SMTP users contributed to the composed email
109
87
  * runtime config. Consumed by SmtpAccountManager, the single owner of the
@@ -125,11 +103,13 @@ export function isWorkAppManagedSmtpUsername(username: string): boolean {
125
103
  }
126
104
 
127
105
  export class WorkAppMailManager {
128
- private readonly storageKey = '/workhosters/mail-identities.json';
129
106
  private readonly emailValidator = new plugins.smartmta.Core.EmailValidator();
107
+ private readonly gatewayMailDomainStore: IGatewayMailDomainStore;
130
108
  private identityMutationPromise: Promise<unknown> = Promise.resolve();
131
109
 
132
- constructor(private dcRouterRef: DcRouter) {}
110
+ constructor(private dcRouterRef: DcRouter) {
111
+ this.gatewayMailDomainStore = dcRouterRef.gatewayMailDomainStore || new GatewayMailDomainStore();
112
+ }
133
113
 
134
114
  public async listMailIdentities(
135
115
  ownership?: Partial<interfaces.data.IWorkAppMailOwnership>,
@@ -176,8 +156,8 @@ export class WorkAppMailManager {
176
156
  return { success: true, action: 'unchanged' };
177
157
  }
178
158
  const [deletedIdentity] = identities.splice(existingIndex, 1);
179
- await this.writeStoredIdentities(identities);
180
- await this.applyStoredIdentitiesToRuntime(identities);
159
+ const persistedIdentities = await this.writeStoredIdentities(identities);
160
+ await this.applyStoredIdentitiesToRuntime(persistedIdentities);
181
161
  return {
182
162
  success: true,
183
163
  action: 'deleted',
@@ -227,8 +207,8 @@ export class WorkAppMailManager {
227
207
  identities.push(identity);
228
208
  }
229
209
 
230
- await this.writeStoredIdentities(identities);
231
- await this.applyStoredIdentitiesToRuntime(identities);
210
+ const persistedIdentities = await this.writeStoredIdentities(identities);
211
+ await this.applyStoredIdentitiesToRuntime(persistedIdentities);
232
212
 
233
213
  const response: interfaces.data.IWorkAppMailIdentitySyncResult = {
234
214
  success: true,
@@ -279,6 +259,14 @@ export class WorkAppMailManager {
279
259
  return Array.from(groups.values()).map((group) => this.toWorkAppMailBinding(group));
280
260
  }
281
261
 
262
+ public async countMailDomains(
263
+ ownerArg: Pick<TMailResourceOwner, 'gatewayClientType' | 'gatewayClientId'>,
264
+ ): Promise<number> {
265
+ return await this.gatewayMailDomainStore.countDomains(
266
+ normalizeGatewayMailDomainOwner(ownerArg),
267
+ );
268
+ }
269
+
282
270
  public async syncMailAddressBinding(
283
271
  binding: TMailAddressBindingSync,
284
272
  createdBy: string,
@@ -369,12 +357,14 @@ export class WorkAppMailManager {
369
357
  updatedAt: now,
370
358
  };
371
359
 
372
- await this.writeStoredIdentities(identities);
373
- await this.applyStoredIdentitiesToRuntime(identities);
360
+ const persistedIdentities = await this.writeStoredIdentities(identities);
361
+ await this.applyStoredIdentitiesToRuntime(persistedIdentities);
374
362
 
375
363
  return {
376
364
  success: true,
377
- credential: this.toMailCredentialSecret(identities[identityIndex]),
365
+ credential: this.toMailCredentialSecret(
366
+ persistedIdentities.find((identityArg) => identityArg.id === identity.id)!,
367
+ ),
378
368
  };
379
369
  }
380
370
 
@@ -735,36 +725,13 @@ export class WorkAppMailManager {
735
725
  }
736
726
 
737
727
  private async readStoredIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
738
- const storageManager = this.getRequiredStorageManager();
739
- const storedData = await storageManager.get(this.storageKey);
740
- if (!storedData) return [];
741
- const parsed = JSON.parse(storedData) as IStoredWorkAppMailState | IStoredWorkAppMailIdentity[];
742
- return Array.isArray(parsed) ? parsed : parsed.identities || [];
743
- }
744
-
745
- private async writeStoredIdentities(identities: IStoredWorkAppMailIdentity[]): Promise<void> {
746
- const state: IStoredWorkAppMailState = {
747
- version: 1,
748
- identities,
749
- };
750
- const storageManager = this.getRequiredStorageManager();
751
- await storageManager.set(this.storageKey, JSON.stringify(state, null, 2));
728
+ return await this.gatewayMailDomainStore.readAllIdentities();
752
729
  }
753
730
 
754
- private getRequiredStorageManager(): IStorageManagerLike & Required<
755
- Pick<IStorageManagerLike, 'get' | 'set'>
756
- > {
757
- const storageManager = this.dcRouterRef.storageManager;
758
- if (
759
- !storageManager
760
- || typeof storageManager.get !== 'function'
761
- || typeof storageManager.set !== 'function'
762
- ) {
763
- throw new Error('DcRouter storage manager does not support managed mail identity persistence');
764
- }
765
- return storageManager as IStorageManagerLike & Required<
766
- Pick<IStorageManagerLike, 'get' | 'set'>
767
- >;
731
+ private async writeStoredIdentities(
732
+ identities: IStoredWorkAppMailIdentity[],
733
+ ): Promise<IStoredWorkAppMailIdentity[]> {
734
+ return await this.gatewayMailDomainStore.writeAllIdentities(identities);
768
735
  }
769
736
 
770
737
  private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute {
@@ -911,11 +878,7 @@ export class WorkAppMailManager {
911
878
  }
912
879
 
913
880
  private normalizeDomain(domain: string): string {
914
- const normalized = domain?.trim().toLowerCase();
915
- if (!normalized || normalized.includes('@') || !normalized.includes('.')) {
916
- throw new Error(`Invalid email domain: ${domain}`);
917
- }
918
- return normalized;
881
+ return normalizeGatewayMailDomain(domain);
919
882
  }
920
883
 
921
884
  private normalizeLocalPart(localPart: string): string {
@@ -0,0 +1,222 @@
1
+ import * as plugins from '../plugins.js';
2
+ import type {
3
+ IWorkAppMailInboundRoute,
4
+ IWorkAppMailOwnership,
5
+ } from '../../ts_interfaces/data/workhoster.js';
6
+ import type { TGatewayClientType } from '../../ts_interfaces/data/route-management.js';
7
+
8
+ export interface IStoredWorkAppMailIdentity {
9
+ id: string;
10
+ externalKey: string;
11
+ ownership: IWorkAppMailOwnership;
12
+ address: string;
13
+ localPart: string;
14
+ domain: string;
15
+ enabled: boolean;
16
+ displayName?: string;
17
+ inbound?: IWorkAppMailInboundRoute;
18
+ inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget;
19
+ smtp: {
20
+ enabled: boolean;
21
+ username: string;
22
+ };
23
+ createdAt: number;
24
+ updatedAt: number;
25
+ createdBy: string;
26
+ smtpPassword: string;
27
+ smtpLastRotatedAt?: number;
28
+ }
29
+
30
+ export interface IGatewayMailDomainOwner {
31
+ gatewayClientType: TGatewayClientType;
32
+ gatewayClientId: string;
33
+ }
34
+
35
+ export interface IGatewayMailDomainGroup extends IGatewayMailDomainOwner {
36
+ id: string;
37
+ domain: string;
38
+ identities: IStoredWorkAppMailIdentity[];
39
+ }
40
+
41
+ const gatewayClientTypes = new Set<TGatewayClientType>(['onebox', 'cloudly', 'custom']);
42
+
43
+ const requireTrimmedString = (valueArg: unknown, fieldNameArg: string): string => {
44
+ const value = typeof valueArg === 'string' ? valueArg.trim() : '';
45
+ if (!value) throw new Error(`${fieldNameArg} is required`);
46
+ return value;
47
+ };
48
+
49
+ const requireTimestamp = (valueArg: unknown, fieldNameArg: string): number => {
50
+ if (!Number.isSafeInteger(valueArg) || Number(valueArg) < 0) {
51
+ throw new Error(`${fieldNameArg} must be a non-negative safe integer`);
52
+ }
53
+ return Number(valueArg);
54
+ };
55
+
56
+ export function normalizeGatewayMailDomain(domainArg: unknown): string {
57
+ const candidate = typeof domainArg === 'string'
58
+ ? domainArg.trim().toLowerCase().replace(/\.+$/, '')
59
+ : '';
60
+ if (!candidate || candidate.includes('@') || !candidate.includes('.')) {
61
+ throw new Error(`Invalid email domain: ${String(domainArg || '')}`);
62
+ }
63
+ const ascii = plugins.url.domainToASCII(candidate);
64
+ if (!ascii || ascii.includes('@') || !ascii.includes('.')) {
65
+ throw new Error(`Invalid email domain: ${String(domainArg || '')}`);
66
+ }
67
+ return ascii;
68
+ }
69
+
70
+ export function normalizeGatewayMailDomainOwner(
71
+ ownerArg: {
72
+ gatewayClientType?: unknown;
73
+ gatewayClientId?: unknown;
74
+ },
75
+ ): IGatewayMailDomainOwner {
76
+ const gatewayClientType = ownerArg?.gatewayClientType as TGatewayClientType;
77
+ if (!gatewayClientTypes.has(gatewayClientType)) {
78
+ throw new Error(`Invalid gateway client type: ${String(ownerArg?.gatewayClientType || '')}`);
79
+ }
80
+ return {
81
+ gatewayClientType,
82
+ gatewayClientId: requireTrimmedString(ownerArg.gatewayClientId, 'gatewayClientId'),
83
+ };
84
+ }
85
+
86
+ export function gatewayMailOwnerFromLegacyOwnership(
87
+ ownershipArg: IWorkAppMailOwnership,
88
+ ): IGatewayMailDomainOwner & { appInstanceId: string } {
89
+ const owner = normalizeGatewayMailDomainOwner({
90
+ gatewayClientType: ownershipArg?.workHosterType,
91
+ gatewayClientId: ownershipArg?.workHosterId,
92
+ });
93
+ return {
94
+ ...owner,
95
+ appInstanceId: requireTrimmedString(ownershipArg?.workAppId, 'appInstanceId'),
96
+ };
97
+ }
98
+
99
+ export function createGatewayMailDomainId(
100
+ ownerArg: IGatewayMailDomainOwner,
101
+ domainArg: string,
102
+ ): string {
103
+ const owner = normalizeGatewayMailDomainOwner(ownerArg);
104
+ const domain = normalizeGatewayMailDomain(domainArg);
105
+ const digest = plugins.crypto.createHash('sha256')
106
+ .update(JSON.stringify([owner.gatewayClientType, owner.gatewayClientId, domain]))
107
+ .digest('hex');
108
+ return `gateway-mail-domain-${digest}`;
109
+ }
110
+
111
+ export function normalizeStoredWorkAppMailIdentity(
112
+ identityArg: unknown,
113
+ ): IStoredWorkAppMailIdentity {
114
+ if (!identityArg || typeof identityArg !== 'object' || Array.isArray(identityArg)) {
115
+ throw new Error('Stored WorkApp mail identity must be an object');
116
+ }
117
+ const identity = identityArg as Record<string, any>;
118
+ const owner = gatewayMailOwnerFromLegacyOwnership(identity.ownership);
119
+ const domain = normalizeGatewayMailDomain(identity.domain);
120
+ const localPart = requireTrimmedString(identity.localPart, 'localPart').toLowerCase();
121
+ if (localPart.includes('@') || /\s/.test(localPart)) {
122
+ throw new Error(`Invalid email local part: ${identity.localPart}`);
123
+ }
124
+ const rawAddress = requireTrimmedString(identity.address, 'address').toLowerCase();
125
+ const [addressLocalPart, addressDomain, addressExtra] = rawAddress.split('@');
126
+ if (!addressLocalPart || !addressDomain || addressExtra) {
127
+ throw new Error(`Invalid stored mail identity address: ${identity.address}`);
128
+ }
129
+ const address = `${addressLocalPart}@${normalizeGatewayMailDomain(addressDomain)}`;
130
+ if (address !== `${localPart}@${domain}`) {
131
+ throw new Error('Stored mail identity address, localPart, and domain do not match');
132
+ }
133
+ if (!identity.smtp || typeof identity.smtp !== 'object' || Array.isArray(identity.smtp)) {
134
+ throw new Error('Stored mail identity smtp configuration is required');
135
+ }
136
+ if (typeof identity.enabled !== 'boolean' || typeof identity.smtp.enabled !== 'boolean') {
137
+ throw new Error('Stored mail identity enabled flags must be boolean');
138
+ }
139
+ const createdAt = requireTimestamp(identity.createdAt, 'createdAt');
140
+ const updatedAt = requireTimestamp(identity.updatedAt, 'updatedAt');
141
+ const smtpLastRotatedAt = identity.smtpLastRotatedAt === undefined
142
+ ? undefined
143
+ : requireTimestamp(identity.smtpLastRotatedAt, 'smtpLastRotatedAt');
144
+ const ownership: IWorkAppMailOwnership = {
145
+ workHosterType: owner.gatewayClientType,
146
+ workHosterId: owner.gatewayClientId,
147
+ workAppId: owner.appInstanceId,
148
+ };
149
+ const externalKey = [
150
+ owner.gatewayClientType,
151
+ owner.gatewayClientId,
152
+ owner.appInstanceId,
153
+ address,
154
+ ].join(':');
155
+
156
+ return {
157
+ id: requireTrimmedString(identity.id, 'id'),
158
+ externalKey,
159
+ ownership,
160
+ address,
161
+ localPart,
162
+ domain,
163
+ enabled: identity.enabled,
164
+ ...(identity.displayName === undefined
165
+ ? {}
166
+ : { displayName: String(identity.displayName) }),
167
+ ...(identity.inbound === undefined
168
+ ? {}
169
+ : { inbound: structuredClone(identity.inbound) }),
170
+ ...(identity.inboundTarget === undefined
171
+ ? {}
172
+ : { inboundTarget: structuredClone(identity.inboundTarget) }),
173
+ smtp: {
174
+ enabled: identity.smtp.enabled,
175
+ username: requireTrimmedString(identity.smtp.username, 'smtp.username'),
176
+ },
177
+ createdAt,
178
+ updatedAt,
179
+ createdBy: requireTrimmedString(identity.createdBy, 'createdBy'),
180
+ smtpPassword: requireTrimmedString(identity.smtpPassword, 'smtpPassword'),
181
+ ...(smtpLastRotatedAt === undefined ? {} : { smtpLastRotatedAt }),
182
+ };
183
+ }
184
+
185
+ export function groupGatewayMailIdentities(
186
+ identitiesArg: unknown[],
187
+ ): IGatewayMailDomainGroup[] {
188
+ const groups = new Map<string, IGatewayMailDomainGroup>();
189
+ const identityIds = new Set<string>();
190
+ const externalKeys = new Set<string>();
191
+
192
+ for (const identityArg of identitiesArg) {
193
+ const identity = normalizeStoredWorkAppMailIdentity(identityArg);
194
+ if (identityIds.has(identity.id)) {
195
+ throw new Error(`Duplicate stored mail identity id: ${identity.id}`);
196
+ }
197
+ if (externalKeys.has(identity.externalKey)) {
198
+ throw new Error(`Duplicate stored mail identity externalKey: ${identity.externalKey}`);
199
+ }
200
+ identityIds.add(identity.id);
201
+ externalKeys.add(identity.externalKey);
202
+ const owner = gatewayMailOwnerFromLegacyOwnership(identity.ownership);
203
+ const id = createGatewayMailDomainId(owner, identity.domain);
204
+ const group = groups.get(id) || {
205
+ id,
206
+ gatewayClientType: owner.gatewayClientType,
207
+ gatewayClientId: owner.gatewayClientId,
208
+ domain: identity.domain,
209
+ identities: [],
210
+ };
211
+ group.identities.push(identity);
212
+ groups.set(id, group);
213
+ }
214
+
215
+ return Array.from(groups.values())
216
+ .map((groupArg) => ({
217
+ ...groupArg,
218
+ identities: groupArg.identities
219
+ .sort((leftArg, rightArg) => leftArg.externalKey.localeCompare(rightArg.externalKey)),
220
+ }))
221
+ .sort((leftArg, rightArg) => leftArg.id.localeCompare(rightArg.id));
222
+ }
package/ts/email/index.ts CHANGED
@@ -8,12 +8,14 @@ export * from './reconcile-mail-dns-singleton.js';
8
8
  export * from './classes.mail-egress-coordinator.js';
9
9
  export * from './classes.mail-edge-eligibility.js';
10
10
  export * from './classes.email-settings.manager.js';
11
+ export * from './classes.gateway-mail-domain.store.js';
11
12
  export * from './classes.smartmta-blob-storage-manager.js';
12
13
  export * from './classes.smartmta-storage-manager.js';
13
14
  export * from './smartmta-storage-policy.js';
14
15
  export * from './classes.smtp-account.manager.js';
15
16
  export * from './smtp-scram.js';
16
17
  export * from './classes.workapp-mail-manager.js';
18
+ export * from './gateway-mail-domain-authority.js';
17
19
  export * from './email-dns-records.js';
18
20
  export * from './helpers.email-log-traffic.js';
19
21
  export * from './helpers.inbound-security.js';