@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.
- package/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +2 -1
- package/dist_ts/classes.dcrouter.js +4 -2
- package/dist_ts/db/documents/classes.gateway-mail-domain.doc.d.ts +16 -0
- package/dist_ts/db/documents/classes.gateway-mail-domain.doc.js +107 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/email/classes.gateway-mail-domain.store.d.ts +11 -0
- package/dist_ts/email/classes.gateway-mail-domain.store.js +70 -0
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +3 -23
- package/dist_ts/email/classes.workapp-mail-manager.js +18 -35
- package/dist_ts/email/gateway-mail-domain-authority.d.ts +44 -0
- package/dist_ts/email/gateway-mail-domain-authority.js +160 -0
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +33 -1
- package/dist_ts/opsserver/helpers/gateway-client-auth.d.ts +2 -0
- package/dist_ts/opsserver/helpers/gateway-client-auth.js +5 -3
- package/dist_ts_migrations/index.js +196 -11
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +2 -2
- package/readme.md +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +3 -1
- package/ts/db/documents/classes.gateway-mail-domain.doc.ts +42 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/email/classes.gateway-mail-domain.store.ts +89 -0
- package/ts/email/classes.workapp-mail-manager.ts +33 -70
- package/ts/email/gateway-mail-domain-authority.ts +222 -0
- package/ts/email/index.ts +2 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +43 -1
- package/ts/opsserver/helpers/gateway-client-auth.ts +6 -2
- 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.
|
|
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.
|
|
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.
|
|
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
|
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/classes.dcrouter.ts
CHANGED
|
@@ -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
|
+
|
package/ts/db/documents/index.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
|
755
|
-
|
|
756
|
-
> {
|
|
757
|
-
|
|
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
|
-
|
|
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';
|