@serve.zone/dcrouter 18.4.1 → 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 +5 -23
- package/dist_ts/email/classes.workapp-mail-manager.js +63 -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 +90 -68
- 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
- package/readme.plan.md +0 -22
|
@@ -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. 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;
|
|
@@ -32,6 +37,7 @@ type TMailAddressBindingSyncResponse = plugins.servezoneInterfaces.requests.mail
|
|
|
32
37
|
type TMailAddressBindingDeleteResponse = plugins.servezoneInterfaces.requests.mail.IReq_DeleteMailAddressBinding['response'];
|
|
33
38
|
type TMailCredentialRotateResponse = plugins.servezoneInterfaces.requests.mail.IReq_RotateMailCredential['response'];
|
|
34
39
|
type TMailEnqueueResponse = plugins.servezoneInterfaces.requests.mail.IReq_EnqueueMail['response'];
|
|
40
|
+
type TMailSubmissionErrorCode = plugins.servezoneInterfaces.data.TMailSubmissionErrorCode;
|
|
35
41
|
type TMailDeliveryStatusResponse = plugins.servezoneInterfaces.requests.mail.IReq_GetMailDeliveryStatus['response'];
|
|
36
42
|
type TMailEndpointRegistrationResponse = plugins.servezoneInterfaces.requests.mail.IReq_RegisterServiceMailEndpoint['response'];
|
|
37
43
|
type TWorkAppMailBinding = plugins.servezoneInterfaces.data.IWorkAppMailBinding;
|
|
@@ -66,31 +72,14 @@ type TCachedEmailRouteData = {
|
|
|
66
72
|
};
|
|
67
73
|
};
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
displayName?: string;
|
|
78
|
-
inbound?: interfaces.data.IWorkAppMailInboundRoute;
|
|
79
|
-
inboundTarget?: plugins.servezoneInterfaces.data.IMailInboundTarget;
|
|
80
|
-
smtp: {
|
|
81
|
-
enabled: boolean;
|
|
82
|
-
username: string;
|
|
83
|
-
};
|
|
84
|
-
createdAt: number;
|
|
85
|
-
updatedAt: number;
|
|
86
|
-
createdBy: string;
|
|
87
|
-
smtpPassword: string;
|
|
88
|
-
smtpLastRotatedAt?: number;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
interface IStoredWorkAppMailState {
|
|
92
|
-
version: 1;
|
|
93
|
-
identities: IStoredWorkAppMailIdentity[];
|
|
75
|
+
class MailSubmissionValidationError extends Error {
|
|
76
|
+
constructor(
|
|
77
|
+
public readonly code: TMailSubmissionErrorCode,
|
|
78
|
+
messageArg: string,
|
|
79
|
+
) {
|
|
80
|
+
super(messageArg);
|
|
81
|
+
this.name = 'MailSubmissionValidationError';
|
|
82
|
+
}
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
/**
|
|
@@ -114,10 +103,13 @@ export function isWorkAppManagedSmtpUsername(username: string): boolean {
|
|
|
114
103
|
}
|
|
115
104
|
|
|
116
105
|
export class WorkAppMailManager {
|
|
117
|
-
private readonly
|
|
106
|
+
private readonly emailValidator = new plugins.smartmta.Core.EmailValidator();
|
|
107
|
+
private readonly gatewayMailDomainStore: IGatewayMailDomainStore;
|
|
118
108
|
private identityMutationPromise: Promise<unknown> = Promise.resolve();
|
|
119
109
|
|
|
120
|
-
constructor(private dcRouterRef: DcRouter) {
|
|
110
|
+
constructor(private dcRouterRef: DcRouter) {
|
|
111
|
+
this.gatewayMailDomainStore = dcRouterRef.gatewayMailDomainStore || new GatewayMailDomainStore();
|
|
112
|
+
}
|
|
121
113
|
|
|
122
114
|
public async listMailIdentities(
|
|
123
115
|
ownership?: Partial<interfaces.data.IWorkAppMailOwnership>,
|
|
@@ -164,8 +156,8 @@ export class WorkAppMailManager {
|
|
|
164
156
|
return { success: true, action: 'unchanged' };
|
|
165
157
|
}
|
|
166
158
|
const [deletedIdentity] = identities.splice(existingIndex, 1);
|
|
167
|
-
await this.writeStoredIdentities(identities);
|
|
168
|
-
await this.applyStoredIdentitiesToRuntime(
|
|
159
|
+
const persistedIdentities = await this.writeStoredIdentities(identities);
|
|
160
|
+
await this.applyStoredIdentitiesToRuntime(persistedIdentities);
|
|
169
161
|
return {
|
|
170
162
|
success: true,
|
|
171
163
|
action: 'deleted',
|
|
@@ -215,8 +207,8 @@ export class WorkAppMailManager {
|
|
|
215
207
|
identities.push(identity);
|
|
216
208
|
}
|
|
217
209
|
|
|
218
|
-
await this.writeStoredIdentities(identities);
|
|
219
|
-
await this.applyStoredIdentitiesToRuntime(
|
|
210
|
+
const persistedIdentities = await this.writeStoredIdentities(identities);
|
|
211
|
+
await this.applyStoredIdentitiesToRuntime(persistedIdentities);
|
|
220
212
|
|
|
221
213
|
const response: interfaces.data.IWorkAppMailIdentitySyncResult = {
|
|
222
214
|
success: true,
|
|
@@ -267,6 +259,14 @@ export class WorkAppMailManager {
|
|
|
267
259
|
return Array.from(groups.values()).map((group) => this.toWorkAppMailBinding(group));
|
|
268
260
|
}
|
|
269
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
|
+
|
|
270
270
|
public async syncMailAddressBinding(
|
|
271
271
|
binding: TMailAddressBindingSync,
|
|
272
272
|
createdBy: string,
|
|
@@ -357,12 +357,14 @@ export class WorkAppMailManager {
|
|
|
357
357
|
updatedAt: now,
|
|
358
358
|
};
|
|
359
359
|
|
|
360
|
-
await this.writeStoredIdentities(identities);
|
|
361
|
-
await this.applyStoredIdentitiesToRuntime(
|
|
360
|
+
const persistedIdentities = await this.writeStoredIdentities(identities);
|
|
361
|
+
await this.applyStoredIdentitiesToRuntime(persistedIdentities);
|
|
362
362
|
|
|
363
363
|
return {
|
|
364
364
|
success: true,
|
|
365
|
-
credential: this.toMailCredentialSecret(
|
|
365
|
+
credential: this.toMailCredentialSecret(
|
|
366
|
+
persistedIdentities.find((identityArg) => identityArg.id === identity.id)!,
|
|
367
|
+
),
|
|
366
368
|
};
|
|
367
369
|
}
|
|
368
370
|
|
|
@@ -459,6 +461,7 @@ export class WorkAppMailManager {
|
|
|
459
461
|
} catch (error) {
|
|
460
462
|
return {
|
|
461
463
|
accepted: false,
|
|
464
|
+
...(error instanceof MailSubmissionValidationError ? { errorCode: error.code } : {}),
|
|
462
465
|
message: (error as Error).message,
|
|
463
466
|
};
|
|
464
467
|
}
|
|
@@ -722,36 +725,13 @@ export class WorkAppMailManager {
|
|
|
722
725
|
}
|
|
723
726
|
|
|
724
727
|
private async readStoredIdentities(): Promise<IStoredWorkAppMailIdentity[]> {
|
|
725
|
-
|
|
726
|
-
const storedData = await storageManager.get(this.storageKey);
|
|
727
|
-
if (!storedData) return [];
|
|
728
|
-
const parsed = JSON.parse(storedData) as IStoredWorkAppMailState | IStoredWorkAppMailIdentity[];
|
|
729
|
-
return Array.isArray(parsed) ? parsed : parsed.identities || [];
|
|
728
|
+
return await this.gatewayMailDomainStore.readAllIdentities();
|
|
730
729
|
}
|
|
731
730
|
|
|
732
|
-
private async writeStoredIdentities(
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
};
|
|
737
|
-
const storageManager = this.getRequiredStorageManager();
|
|
738
|
-
await storageManager.set(this.storageKey, JSON.stringify(state, null, 2));
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
private getRequiredStorageManager(): IStorageManagerLike & Required<
|
|
742
|
-
Pick<IStorageManagerLike, 'get' | 'set'>
|
|
743
|
-
> {
|
|
744
|
-
const storageManager = this.dcRouterRef.storageManager;
|
|
745
|
-
if (
|
|
746
|
-
!storageManager
|
|
747
|
-
|| typeof storageManager.get !== 'function'
|
|
748
|
-
|| typeof storageManager.set !== 'function'
|
|
749
|
-
) {
|
|
750
|
-
throw new Error('DcRouter storage manager does not support managed mail identity persistence');
|
|
751
|
-
}
|
|
752
|
-
return storageManager as IStorageManagerLike & Required<
|
|
753
|
-
Pick<IStorageManagerLike, 'get' | 'set'>
|
|
754
|
-
>;
|
|
731
|
+
private async writeStoredIdentities(
|
|
732
|
+
identities: IStoredWorkAppMailIdentity[],
|
|
733
|
+
): Promise<IStoredWorkAppMailIdentity[]> {
|
|
734
|
+
return await this.gatewayMailDomainStore.writeAllIdentities(identities);
|
|
755
735
|
}
|
|
756
736
|
|
|
757
737
|
private buildInboundRoute(identity: IStoredWorkAppMailIdentity): IEmailRoute {
|
|
@@ -898,11 +878,7 @@ export class WorkAppMailManager {
|
|
|
898
878
|
}
|
|
899
879
|
|
|
900
880
|
private normalizeDomain(domain: string): string {
|
|
901
|
-
|
|
902
|
-
if (!normalized || normalized.includes('@') || !normalized.includes('.')) {
|
|
903
|
-
throw new Error(`Invalid email domain: ${domain}`);
|
|
904
|
-
}
|
|
905
|
-
return normalized;
|
|
881
|
+
return normalizeGatewayMailDomain(domain);
|
|
906
882
|
}
|
|
907
883
|
|
|
908
884
|
private normalizeLocalPart(localPart: string): string {
|
|
@@ -1341,11 +1317,23 @@ export class WorkAppMailManager {
|
|
|
1341
1317
|
if (!messageArg.text && !messageArg.html) {
|
|
1342
1318
|
throw new Error('Either text or html is required');
|
|
1343
1319
|
}
|
|
1320
|
+
const customReplyToHeader = Object.keys(messageArg.headers || {})
|
|
1321
|
+
.find((keyArg) => keyArg.toLowerCase() === 'reply-to');
|
|
1322
|
+
if (messageArg.replyTo !== undefined && customReplyToHeader) {
|
|
1323
|
+
throw new MailSubmissionValidationError(
|
|
1324
|
+
'REPLY_TO_FIELD_HEADER_CONFLICT',
|
|
1325
|
+
'replyTo cannot be combined with a custom Reply-To header',
|
|
1326
|
+
);
|
|
1327
|
+
}
|
|
1328
|
+
const replyTo = messageArg.replyTo === undefined
|
|
1329
|
+
? undefined
|
|
1330
|
+
: this.validateReplyTo(messageArg.replyTo);
|
|
1344
1331
|
const messageId = `<${plugins.crypto.randomUUID()}@${identityArg.domain}>`;
|
|
1345
1332
|
const headers: Record<string, string> = {
|
|
1346
1333
|
From: identityArg.address,
|
|
1347
1334
|
To: messageArg.to.join(', '),
|
|
1348
1335
|
...(messageArg.cc?.length ? { Cc: messageArg.cc.join(', ') } : {}),
|
|
1336
|
+
...(replyTo ? { 'Reply-To': replyTo } : {}),
|
|
1349
1337
|
Subject: messageArg.subject,
|
|
1350
1338
|
Date: new Date().toUTCString(),
|
|
1351
1339
|
'Message-ID': messageId,
|
|
@@ -1521,6 +1509,40 @@ export class WorkAppMailManager {
|
|
|
1521
1509
|
]).has(valueArg.toLowerCase());
|
|
1522
1510
|
}
|
|
1523
1511
|
|
|
1512
|
+
private validateReplyTo(valueArg: unknown): string {
|
|
1513
|
+
const invalid = (): never => {
|
|
1514
|
+
throw new MailSubmissionValidationError(
|
|
1515
|
+
'INVALID_REPLY_TO',
|
|
1516
|
+
'replyTo must be one bare printable ASCII mailbox address',
|
|
1517
|
+
);
|
|
1518
|
+
};
|
|
1519
|
+
if (
|
|
1520
|
+
typeof valueArg !== 'string'
|
|
1521
|
+
|| valueArg.length === 0
|
|
1522
|
+
|| valueArg.length > 254
|
|
1523
|
+
|| !/^[\x21-\x7e]+$/.test(valueArg)
|
|
1524
|
+
|| /[(),:;<>"[\]\\]/.test(valueArg)
|
|
1525
|
+
) {
|
|
1526
|
+
return invalid();
|
|
1527
|
+
}
|
|
1528
|
+
const parts = valueArg.split('@');
|
|
1529
|
+
if (parts.length !== 2) return invalid();
|
|
1530
|
+
const [localPart, domain] = parts;
|
|
1531
|
+
if (!localPart || localPart.length > 64 || !domain) return invalid();
|
|
1532
|
+
const domainLabels = domain.split('.');
|
|
1533
|
+
if (
|
|
1534
|
+
domainLabels.some((labelArg) => (
|
|
1535
|
+
labelArg.length === 0
|
|
1536
|
+
|| labelArg.length > 63
|
|
1537
|
+
|| !/^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/.test(labelArg)
|
|
1538
|
+
))
|
|
1539
|
+
|| !this.emailValidator.isValidFormat(valueArg)
|
|
1540
|
+
) {
|
|
1541
|
+
return invalid();
|
|
1542
|
+
}
|
|
1543
|
+
return valueArg;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1524
1546
|
private sanitizeHeaderValue(valueArg: string): string {
|
|
1525
1547
|
return String(valueArg).replace(/[\r\n]+/g, ' ').trim();
|
|
1526
1548
|
}
|