@serve.zone/dcrouter 14.0.1 → 14.2.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_serve/bundle.js +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +26 -0
- package/dist_ts/classes.dcrouter.js +491 -58
- package/dist_ts/config/classes.reference-resolver.js +3 -1
- package/dist_ts/db/documents/classes.cached.email.d.ts +4 -2
- package/dist_ts/db/documents/classes.cached.email.js +25 -5
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +25 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +185 -1
- package/dist_ts/http3/http3-route-augmentation.js +2 -1
- package/dist_ts/opsserver/handlers/workhoster.handler.d.ts +3 -0
- package/dist_ts/opsserver/handlers/workhoster.handler.js +102 -2
- package/dist_ts/plugins.d.ts +4 -2
- package/dist_ts/plugins.js +5 -3
- package/dist_ts_migrations/index.js +2 -2
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/network/ops-view-sourceprofiles.js +2 -1
- package/package.json +7 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +592 -60
- package/ts/config/classes.reference-resolver.ts +1 -0
- package/ts/db/documents/classes.cached.email.ts +31 -5
- package/ts/email/classes.workapp-mail-manager.ts +234 -0
- package/ts/http3/http3-route-augmentation.ts +1 -0
- package/ts/opsserver/handlers/workhoster.handler.ts +131 -1
- package/ts/plugins.ts +4 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/network/ops-view-sourceprofiles.ts +1 -0
|
@@ -479,6 +479,7 @@ export class ReferenceResolver {
|
|
|
479
479
|
if (override.basicAuth !== undefined) merged.basicAuth = override.basicAuth;
|
|
480
480
|
if (override.jwtAuth !== undefined) merged.jwtAuth = override.jwtAuth;
|
|
481
481
|
if (override.vpn !== undefined) merged.vpn = override.vpn;
|
|
482
|
+
if (override.challenge !== undefined) merged.challenge = override.challenge;
|
|
482
483
|
|
|
483
484
|
return merged;
|
|
484
485
|
}
|
|
@@ -6,7 +6,7 @@ const TTL = plugins.smartdata.smartdataTtlValues;
|
|
|
6
6
|
/**
|
|
7
7
|
* Email status in the cache
|
|
8
8
|
*/
|
|
9
|
-
export type TCachedEmailStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
|
|
9
|
+
export type TCachedEmailStatus = 'pending' | 'processing' | 'queued' | 'delivered' | 'failed' | 'deferred';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Helper to get the smartdata database instance
|
|
@@ -169,12 +169,38 @@ export class CachedEmail extends plugins.smartdata.SmartdataCachedDocument<Cache
|
|
|
169
169
|
/**
|
|
170
170
|
* Find all emails pending delivery (status = pending and nextAttempt <= now)
|
|
171
171
|
*/
|
|
172
|
-
public static async findPendingForDelivery(): Promise<CachedEmail[]> {
|
|
172
|
+
public static async findPendingForDelivery(limit = 25): Promise<CachedEmail[]> {
|
|
173
173
|
const now = new Date();
|
|
174
|
-
return await CachedEmail.
|
|
175
|
-
status: 'pending',
|
|
174
|
+
return await CachedEmail.findLimited({
|
|
175
|
+
status: { $in: ['pending', 'deferred', 'processing'] },
|
|
176
176
|
nextAttempt: { $lte: now },
|
|
177
|
-
});
|
|
177
|
+
}, limit);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
public static async findQueuedForRecovery(limit = 25): Promise<CachedEmail[]> {
|
|
181
|
+
return await CachedEmail.findLimited({
|
|
182
|
+
status: 'queued',
|
|
183
|
+
}, limit);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private static async findLimited(filter: Record<string, any>, limit: number): Promise<CachedEmail[]> {
|
|
187
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(0, Math.floor(limit)) : 25;
|
|
188
|
+
if (safeLimit === 0) {
|
|
189
|
+
return [];
|
|
190
|
+
}
|
|
191
|
+
const collection = (CachedEmail as typeof CachedEmail & {
|
|
192
|
+
collection: plugins.smartdata.SmartdataCollection<CachedEmail>;
|
|
193
|
+
}).collection;
|
|
194
|
+
const cursor = await collection.getCursor(
|
|
195
|
+
filter,
|
|
196
|
+
CachedEmail as unknown as typeof plugins.smartdata.SmartDataDbDoc,
|
|
197
|
+
);
|
|
198
|
+
cursor.mongodbCursor.limit(safeLimit);
|
|
199
|
+
try {
|
|
200
|
+
return await cursor.toArray();
|
|
201
|
+
} finally {
|
|
202
|
+
await cursor.close();
|
|
203
|
+
}
|
|
178
204
|
}
|
|
179
205
|
|
|
180
206
|
/**
|
|
@@ -6,6 +6,12 @@ import * as plugins from '../plugins.js';
|
|
|
6
6
|
import type * as interfaces from '../../ts_interfaces/index.js';
|
|
7
7
|
|
|
8
8
|
type TSyncRequest = interfaces.requests.IReq_SyncWorkAppMailIdentity['request'];
|
|
9
|
+
type TMailResourceOwner = plugins.servezoneInterfaces.data.IMailResourceOwner;
|
|
10
|
+
type TMailAddressBinding = plugins.servezoneInterfaces.data.IMailAddressBinding;
|
|
11
|
+
type TMailAddressBindingSync = plugins.servezoneInterfaces.requests.mail.TMailAddressBindingSync;
|
|
12
|
+
type TMailAddressBindingSyncResponse = plugins.servezoneInterfaces.requests.mail.IReq_SyncMailAddressBinding['response'];
|
|
13
|
+
type TMailAddressBindingDeleteResponse = plugins.servezoneInterfaces.requests.mail.IReq_DeleteMailAddressBinding['response'];
|
|
14
|
+
type TWorkAppMailBinding = plugins.servezoneInterfaces.data.IWorkAppMailBinding;
|
|
9
15
|
|
|
10
16
|
interface IStoredWorkAppMailIdentity extends interfaces.data.IWorkAppMailIdentity {
|
|
11
17
|
smtpPassword: string;
|
|
@@ -109,6 +115,89 @@ export class WorkAppMailManager {
|
|
|
109
115
|
return response;
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
public async listMailAddressBindings(options: {
|
|
119
|
+
owner?: Partial<TMailResourceOwner>;
|
|
120
|
+
domain?: string;
|
|
121
|
+
address?: string;
|
|
122
|
+
} = {}): Promise<TMailAddressBinding[]> {
|
|
123
|
+
const domain = options.domain ? this.normalizeDomain(options.domain) : undefined;
|
|
124
|
+
const address = options.address ? this.normalizeAddress(options.address) : undefined;
|
|
125
|
+
const identities = await this.readStoredIdentities();
|
|
126
|
+
|
|
127
|
+
return identities
|
|
128
|
+
.filter((identity) => this.matchesMailOwner(this.toMailOwner(identity.ownership), options.owner))
|
|
129
|
+
.filter((identity) => domain ? identity.domain === domain : true)
|
|
130
|
+
.filter((identity) => address ? identity.address === address : true)
|
|
131
|
+
.map((identity) => this.toMailAddressBinding(identity));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public async listWorkAppMailBindings(
|
|
135
|
+
owner?: Partial<TMailResourceOwner>,
|
|
136
|
+
): Promise<TWorkAppMailBinding[]> {
|
|
137
|
+
const identities = (await this.readStoredIdentities())
|
|
138
|
+
.filter((identity) => this.matchesMailOwner(this.toMailOwner(identity.ownership), owner));
|
|
139
|
+
const groups = new Map<string, IStoredWorkAppMailIdentity[]>();
|
|
140
|
+
|
|
141
|
+
for (const identity of identities) {
|
|
142
|
+
const ownerKey = this.buildMailOwnerKey(this.toMailOwner(identity.ownership));
|
|
143
|
+
const group = groups.get(ownerKey) || [];
|
|
144
|
+
group.push(identity);
|
|
145
|
+
groups.set(ownerKey, group);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Array.from(groups.values()).map((group) => this.toWorkAppMailBinding(group));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public async syncMailAddressBinding(
|
|
152
|
+
binding: TMailAddressBindingSync,
|
|
153
|
+
createdBy: string,
|
|
154
|
+
): Promise<TMailAddressBindingSyncResponse> {
|
|
155
|
+
const ownership = this.normalizeMailResourceOwner(binding.owner);
|
|
156
|
+
const { localPart, domain } = this.normalizeMailAddressParts(binding);
|
|
157
|
+
const syncRequest: TSyncRequest = {
|
|
158
|
+
ownership,
|
|
159
|
+
localPart,
|
|
160
|
+
domain,
|
|
161
|
+
inbound: this.toLegacyInboundRoute(binding.inboundTarget),
|
|
162
|
+
enabled: binding.enabled,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (binding.outboundIdentityId !== undefined) {
|
|
166
|
+
syncRequest.smtpEnabled = Boolean(binding.outboundIdentityId);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const result = await this.syncMailIdentity(syncRequest, createdBy);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
success: result.success,
|
|
173
|
+
binding: result.identity ? this.toMailAddressBinding(result.identity) : undefined,
|
|
174
|
+
message: result.message,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public async deleteMailAddressBinding(
|
|
179
|
+
id: string,
|
|
180
|
+
createdBy: string,
|
|
181
|
+
): Promise<TMailAddressBindingDeleteResponse> {
|
|
182
|
+
const identities = await this.readStoredIdentities();
|
|
183
|
+
const identity = identities.find((storedIdentity) => storedIdentity.id === id || storedIdentity.externalKey === id);
|
|
184
|
+
if (!identity) {
|
|
185
|
+
return { success: true };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const result = await this.syncMailIdentity({
|
|
189
|
+
ownership: identity.ownership,
|
|
190
|
+
localPart: identity.localPart,
|
|
191
|
+
domain: identity.domain,
|
|
192
|
+
delete: true,
|
|
193
|
+
}, createdBy);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
success: result.success,
|
|
197
|
+
message: result.message,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
112
201
|
public async applyStoredIdentitiesToEmailConfig<TConfig extends IUnifiedEmailServerOptions>(
|
|
113
202
|
emailConfig: TConfig,
|
|
114
203
|
): Promise<TConfig> {
|
|
@@ -251,6 +340,63 @@ export class WorkAppMailManager {
|
|
|
251
340
|
return normalized;
|
|
252
341
|
}
|
|
253
342
|
|
|
343
|
+
private normalizeAddress(address: string): string {
|
|
344
|
+
const normalized = address?.trim().toLowerCase();
|
|
345
|
+
const [localPart, domain, extra] = normalized?.split('@') || [];
|
|
346
|
+
if (!localPart || !domain || extra) {
|
|
347
|
+
throw new Error(`Invalid email address: ${address}`);
|
|
348
|
+
}
|
|
349
|
+
return `${this.normalizeLocalPart(localPart)}@${this.normalizeDomain(domain)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private normalizeMailResourceOwner(owner: TMailResourceOwner): interfaces.data.IWorkAppMailOwnership {
|
|
353
|
+
const gatewayClientType = owner.gatewayClientType;
|
|
354
|
+
const gatewayClientId = owner.gatewayClientId?.trim();
|
|
355
|
+
const appInstanceId = owner.appInstanceId?.trim();
|
|
356
|
+
|
|
357
|
+
if (gatewayClientType !== 'onebox' && gatewayClientType !== 'cloudly' && gatewayClientType !== 'custom') {
|
|
358
|
+
throw new Error(`Invalid gateway client type: ${gatewayClientType}`);
|
|
359
|
+
}
|
|
360
|
+
if (!gatewayClientId) throw new Error('gatewayClientId is required');
|
|
361
|
+
if (!appInstanceId) throw new Error('appInstanceId is required');
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
workHosterType: gatewayClientType as interfaces.data.TGatewayClientType,
|
|
365
|
+
workHosterId: gatewayClientId,
|
|
366
|
+
workAppId: appInstanceId,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private normalizeMailAddressParts(binding: TMailAddressBindingSync): {
|
|
371
|
+
localPart: string;
|
|
372
|
+
domain: string;
|
|
373
|
+
} {
|
|
374
|
+
const localPart = this.normalizeLocalPart(binding.localPart);
|
|
375
|
+
const domain = this.normalizeDomain(binding.domain);
|
|
376
|
+
const address = this.normalizeAddress(binding.address);
|
|
377
|
+
if (address !== `${localPart}@${domain}`) {
|
|
378
|
+
throw new Error('mail address, localPart, and domain do not match');
|
|
379
|
+
}
|
|
380
|
+
return { localPart, domain };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
private toLegacyInboundRoute(
|
|
384
|
+
inboundTarget?: TMailAddressBinding['inboundTarget'],
|
|
385
|
+
): interfaces.data.IWorkAppMailInboundRoute | undefined {
|
|
386
|
+
if (!inboundTarget) return undefined;
|
|
387
|
+
if (inboundTarget.type !== 'smtpForward' || !inboundTarget.smtpForward) {
|
|
388
|
+
throw new Error(`Unsupported WorkApp mail inbound target: ${inboundTarget.type}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return this.normalizeInboundRoute({
|
|
392
|
+
enabled: true,
|
|
393
|
+
targetHost: inboundTarget.smtpForward.host,
|
|
394
|
+
targetPort: inboundTarget.smtpForward.port,
|
|
395
|
+
preserveHeaders: inboundTarget.smtpForward.preserveHeaders,
|
|
396
|
+
addHeaders: inboundTarget.smtpForward.addHeaders,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
254
400
|
private normalizeInboundRoute(
|
|
255
401
|
inbound?: interfaces.data.IWorkAppMailInboundRoute,
|
|
256
402
|
): interfaces.data.IWorkAppMailInboundRoute | undefined {
|
|
@@ -282,6 +428,17 @@ export class WorkAppMailManager {
|
|
|
282
428
|
return true;
|
|
283
429
|
}
|
|
284
430
|
|
|
431
|
+
private matchesMailOwner(
|
|
432
|
+
owner: TMailResourceOwner,
|
|
433
|
+
filter?: Partial<TMailResourceOwner>,
|
|
434
|
+
): boolean {
|
|
435
|
+
if (!filter) return true;
|
|
436
|
+
if (filter.gatewayClientType && filter.gatewayClientType !== owner.gatewayClientType) return false;
|
|
437
|
+
if (filter.gatewayClientId && filter.gatewayClientId !== owner.gatewayClientId) return false;
|
|
438
|
+
if (filter.appInstanceId && filter.appInstanceId !== owner.appInstanceId) return false;
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
285
442
|
private buildExternalKey(
|
|
286
443
|
ownership: interfaces.data.IWorkAppMailOwnership,
|
|
287
444
|
address: string,
|
|
@@ -298,6 +455,14 @@ export class WorkAppMailManager {
|
|
|
298
455
|
return `workapp-${this.hashExternalKey(externalKey).slice(0, 24)}`;
|
|
299
456
|
}
|
|
300
457
|
|
|
458
|
+
private buildMailOwnerKey(owner: TMailResourceOwner): string {
|
|
459
|
+
return [
|
|
460
|
+
owner.gatewayClientType,
|
|
461
|
+
owner.gatewayClientId,
|
|
462
|
+
owner.appInstanceId,
|
|
463
|
+
].join(':');
|
|
464
|
+
}
|
|
465
|
+
|
|
301
466
|
private buildRouteName(externalKey: string): string {
|
|
302
467
|
return `workapp-mail-${this.hashExternalKey(externalKey).slice(0, 32)}`;
|
|
303
468
|
}
|
|
@@ -334,6 +499,75 @@ export class WorkAppMailManager {
|
|
|
334
499
|
};
|
|
335
500
|
}
|
|
336
501
|
|
|
502
|
+
private toMailOwner(ownership: interfaces.data.IWorkAppMailOwnership): TMailResourceOwner & { appInstanceId: string } {
|
|
503
|
+
return {
|
|
504
|
+
gatewayClientType: ownership.workHosterType,
|
|
505
|
+
gatewayClientId: ownership.workHosterId,
|
|
506
|
+
appInstanceId: ownership.workAppId,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private toMailInboundTarget(
|
|
511
|
+
inbound?: interfaces.data.IWorkAppMailInboundRoute,
|
|
512
|
+
): TMailAddressBinding['inboundTarget'] {
|
|
513
|
+
if (!inbound?.enabled) return undefined;
|
|
514
|
+
return {
|
|
515
|
+
type: 'smtpForward',
|
|
516
|
+
smtpForward: {
|
|
517
|
+
host: inbound.targetHost,
|
|
518
|
+
port: inbound.targetPort,
|
|
519
|
+
preserveHeaders: inbound.preserveHeaders,
|
|
520
|
+
addHeaders: inbound.addHeaders,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private toMailAddressBinding(
|
|
526
|
+
identity: interfaces.data.IWorkAppMailIdentity,
|
|
527
|
+
): TMailAddressBinding {
|
|
528
|
+
return {
|
|
529
|
+
id: identity.id,
|
|
530
|
+
owner: this.toMailOwner(identity.ownership),
|
|
531
|
+
address: identity.address,
|
|
532
|
+
localPart: identity.localPart,
|
|
533
|
+
domain: identity.domain,
|
|
534
|
+
enabled: identity.enabled,
|
|
535
|
+
status: identity.enabled ? 'active' : 'disabled',
|
|
536
|
+
inboundTarget: this.toMailInboundTarget(identity.inbound),
|
|
537
|
+
outboundIdentityId: identity.smtp.enabled ? identity.smtp.username : undefined,
|
|
538
|
+
recipientPolicy: {
|
|
539
|
+
mode: 'staticList',
|
|
540
|
+
staticRecipients: [identity.address],
|
|
541
|
+
},
|
|
542
|
+
createdAt: identity.createdAt,
|
|
543
|
+
updatedAt: identity.updatedAt,
|
|
544
|
+
createdBy: identity.createdBy,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private toWorkAppMailBinding(
|
|
549
|
+
identities: IStoredWorkAppMailIdentity[],
|
|
550
|
+
): TWorkAppMailBinding {
|
|
551
|
+
const [firstIdentity] = identities;
|
|
552
|
+
const owner = this.toMailOwner(firstIdentity.ownership);
|
|
553
|
+
const enabledIdentities = identities.filter((identity) => identity.enabled);
|
|
554
|
+
const smtpIdentities = identities.filter((identity) => identity.smtp.enabled);
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
id: `workapp-mail-${this.hashExternalKey(this.buildMailOwnerKey(owner)).slice(0, 32)}`,
|
|
558
|
+
owner,
|
|
559
|
+
enabled: enabledIdentities.length > 0,
|
|
560
|
+
status: enabledIdentities.length > 0 ? 'active' : 'disabled',
|
|
561
|
+
addressBindingIds: identities.map((identity) => identity.id),
|
|
562
|
+
outboundIdentityIds: smtpIdentities.map((identity) => identity.smtp.username),
|
|
563
|
+
defaultFrom: enabledIdentities[0]?.address || firstIdentity.address,
|
|
564
|
+
inboundTarget: identities.length === 1 ? this.toMailInboundTarget(firstIdentity.inbound) : undefined,
|
|
565
|
+
createdAt: Math.min(...identities.map((identity) => identity.createdAt)),
|
|
566
|
+
updatedAt: Math.max(...identities.map((identity) => identity.updatedAt)),
|
|
567
|
+
createdBy: firstIdentity.createdBy,
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
337
571
|
private toPublicIdentity(
|
|
338
572
|
identity: IStoredWorkAppMailIdentity,
|
|
339
573
|
): interfaces.data.IWorkAppMailIdentity {
|
|
@@ -257,6 +257,83 @@ export class WorkHosterHandler {
|
|
|
257
257
|
},
|
|
258
258
|
),
|
|
259
259
|
);
|
|
260
|
+
|
|
261
|
+
this.typedrouter.addTypedHandler(
|
|
262
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_ListMailAddressBindings>(
|
|
263
|
+
'listMailAddressBindings',
|
|
264
|
+
async (dataArg) => {
|
|
265
|
+
const auth = await this.requireAuth(dataArg.auth || {}, 'gateway-clients:read');
|
|
266
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
267
|
+
if (!manager) return { bindings: [] };
|
|
268
|
+
return {
|
|
269
|
+
bindings: await manager.listMailAddressBindings({
|
|
270
|
+
owner: this.resolveMailOwnerFilter(auth, dataArg.owner),
|
|
271
|
+
domain: dataArg.domain,
|
|
272
|
+
address: dataArg.address,
|
|
273
|
+
}),
|
|
274
|
+
};
|
|
275
|
+
},
|
|
276
|
+
),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
this.typedrouter.addTypedHandler(
|
|
280
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_SyncMailAddressBinding>(
|
|
281
|
+
'syncMailAddressBinding',
|
|
282
|
+
async (dataArg) => {
|
|
283
|
+
const auth = await this.requireAuth(dataArg.auth || {}, 'gateway-clients:write');
|
|
284
|
+
this.assertCapability(auth, 'syncRoutes');
|
|
285
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
286
|
+
if (!manager) {
|
|
287
|
+
return { success: false, message: 'WorkApp mail manager not initialized' };
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const binding = {
|
|
291
|
+
...dataArg.binding,
|
|
292
|
+
owner: this.resolveMailOwner(auth, dataArg.binding.owner),
|
|
293
|
+
};
|
|
294
|
+
this.assertMailForwardTargetAllowed(auth, binding.inboundTarget);
|
|
295
|
+
return await manager.syncMailAddressBinding(binding, auth.userId);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return { success: false, message: (error as Error).message };
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
),
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
this.typedrouter.addTypedHandler(
|
|
304
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_DeleteMailAddressBinding>(
|
|
305
|
+
'deleteMailAddressBinding',
|
|
306
|
+
async (dataArg) => {
|
|
307
|
+
const auth = await this.requireAuth(dataArg.auth || {}, 'gateway-clients:write');
|
|
308
|
+
this.assertCapability(auth, 'syncRoutes');
|
|
309
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
310
|
+
if (!manager) {
|
|
311
|
+
return { success: false, message: 'WorkApp mail manager not initialized' };
|
|
312
|
+
}
|
|
313
|
+
if (auth.token?.policy?.role === 'gatewayClient') {
|
|
314
|
+
const bindings = await manager.listMailAddressBindings({
|
|
315
|
+
owner: this.resolveMailOwnerFilter(auth),
|
|
316
|
+
});
|
|
317
|
+
const binding = bindings.find((candidate) => candidate.id === dataArg.id);
|
|
318
|
+
if (!binding) return { success: true };
|
|
319
|
+
return await manager.deleteMailAddressBinding(binding.id, auth.userId);
|
|
320
|
+
}
|
|
321
|
+
return await manager.deleteMailAddressBinding(dataArg.id, auth.userId);
|
|
322
|
+
},
|
|
323
|
+
),
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
this.typedrouter.addTypedHandler(
|
|
327
|
+
new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.mail.IReq_ListWorkAppMailBindings>(
|
|
328
|
+
'listWorkAppMailBindings',
|
|
329
|
+
async (dataArg) => {
|
|
330
|
+
const auth = await this.requireAuth(dataArg.auth || {}, 'gateway-clients:read');
|
|
331
|
+
const manager = this.opsServerRef.dcRouterRef.workAppMailManager;
|
|
332
|
+
if (!manager) return { bindings: [] };
|
|
333
|
+
return { bindings: await manager.listWorkAppMailBindings(this.resolveMailOwnerFilter(auth, dataArg.owner)) };
|
|
334
|
+
},
|
|
335
|
+
),
|
|
336
|
+
);
|
|
260
337
|
}
|
|
261
338
|
|
|
262
339
|
private getGatewayCapabilities(): interfaces.data.IGatewayCapabilities {
|
|
@@ -335,7 +412,7 @@ export class WorkHosterHandler {
|
|
|
335
412
|
const policy = auth.token?.policy;
|
|
336
413
|
if (!policy || policy.role !== 'gatewayClient') return;
|
|
337
414
|
if (policy.capabilities?.[capability] === true) return;
|
|
338
|
-
throw new plugins.typedrequest.TypedResponseError(`token capability missing: ${capability}`);
|
|
415
|
+
throw new plugins.typedrequest.TypedResponseError(`token capability missing: ${String(capability)}`);
|
|
339
416
|
}
|
|
340
417
|
|
|
341
418
|
private resolveGatewayClientId(auth: TAuthContext, requestedId?: string): string | undefined {
|
|
@@ -376,6 +453,39 @@ export class WorkHosterHandler {
|
|
|
376
453
|
return ownership as Required<interfaces.data.IGatewayClientOwnership>;
|
|
377
454
|
}
|
|
378
455
|
|
|
456
|
+
private resolveMailOwnerFilter(
|
|
457
|
+
auth: TAuthContext,
|
|
458
|
+
owner?: Partial<plugins.servezoneInterfaces.data.IMailResourceOwner>,
|
|
459
|
+
): Partial<plugins.servezoneInterfaces.data.IMailResourceOwner> | undefined {
|
|
460
|
+
const policy = auth.token?.policy;
|
|
461
|
+
if (policy?.role !== 'gatewayClient') return owner;
|
|
462
|
+
if (!policy.gatewayClient) {
|
|
463
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token is missing gatewayClient binding');
|
|
464
|
+
}
|
|
465
|
+
if (owner?.gatewayClientType && owner.gatewayClientType !== policy.gatewayClient.type) {
|
|
466
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
467
|
+
}
|
|
468
|
+
if (owner?.gatewayClientId && owner.gatewayClientId !== policy.gatewayClient.id) {
|
|
469
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token cannot act for this ownership');
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
...owner,
|
|
473
|
+
gatewayClientType: policy.gatewayClient.type,
|
|
474
|
+
gatewayClientId: policy.gatewayClient.id,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private resolveMailOwner(
|
|
479
|
+
auth: TAuthContext,
|
|
480
|
+
owner: plugins.servezoneInterfaces.data.IMailResourceOwner,
|
|
481
|
+
): plugins.servezoneInterfaces.data.IMailResourceOwner {
|
|
482
|
+
const resolvedOwner = this.resolveMailOwnerFilter(auth, owner);
|
|
483
|
+
if (!resolvedOwner?.gatewayClientType || !resolvedOwner.gatewayClientId) {
|
|
484
|
+
throw new plugins.typedrequest.TypedResponseError('mail owner is missing gateway client type or id');
|
|
485
|
+
}
|
|
486
|
+
return resolvedOwner as plugins.servezoneInterfaces.data.IMailResourceOwner;
|
|
487
|
+
}
|
|
488
|
+
|
|
379
489
|
private assertGatewayClientOwnership(auth: TAuthContext, ownership: Required<interfaces.data.IGatewayClientOwnership>): void {
|
|
380
490
|
const policy = auth.token?.policy;
|
|
381
491
|
if (!policy || policy.role !== 'gatewayClient') return;
|
|
@@ -404,6 +514,26 @@ export class WorkHosterHandler {
|
|
|
404
514
|
}
|
|
405
515
|
}
|
|
406
516
|
|
|
517
|
+
private assertMailForwardTargetAllowed(
|
|
518
|
+
auth: TAuthContext,
|
|
519
|
+
target?: plugins.servezoneInterfaces.data.IMailInboundTarget,
|
|
520
|
+
): void {
|
|
521
|
+
const policy = auth.token?.policy;
|
|
522
|
+
if (!policy || policy.role !== 'gatewayClient' || !target?.smtpForward) return;
|
|
523
|
+
const allowedTargets = policy.allowedRouteTargets || [];
|
|
524
|
+
if (allowedTargets.length === 0) {
|
|
525
|
+
throw new plugins.typedrequest.TypedResponseError('gateway client token has no allowed route targets');
|
|
526
|
+
}
|
|
527
|
+
const host = target.smtpForward.host.trim().toLowerCase();
|
|
528
|
+
const port = Number(target.smtpForward.port);
|
|
529
|
+
const allowed = allowedTargets.some((allowedTarget) => {
|
|
530
|
+
return allowedTarget.host.trim().toLowerCase() === host && allowedTarget.ports.includes(port);
|
|
531
|
+
});
|
|
532
|
+
if (!allowed) {
|
|
533
|
+
throw new plugins.typedrequest.TypedResponseError(`mail target is outside token policy: ${host}:${port}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
407
537
|
private matchesHostnamePatterns(hostname: string, patterns: string[]): boolean {
|
|
408
538
|
const normalizedHostname = hostname.trim().toLowerCase();
|
|
409
539
|
if (!normalizedHostname) return false;
|
package/ts/plugins.ts
CHANGED
|
@@ -4,6 +4,7 @@ import * as fs from 'node:fs';
|
|
|
4
4
|
import * as crypto from 'node:crypto';
|
|
5
5
|
import * as http from 'node:http';
|
|
6
6
|
import * as net from 'node:net';
|
|
7
|
+
import * as buffer from 'node:buffer';
|
|
7
8
|
import * as os from 'node:os';
|
|
8
9
|
import * as path from 'node:path';
|
|
9
10
|
import * as tls from 'node:tls';
|
|
@@ -15,6 +16,7 @@ export {
|
|
|
15
16
|
crypto,
|
|
16
17
|
http,
|
|
17
18
|
net,
|
|
19
|
+
buffer,
|
|
18
20
|
os,
|
|
19
21
|
path,
|
|
20
22
|
tls,
|
|
@@ -52,6 +54,7 @@ export {
|
|
|
52
54
|
import * as projectinfo from '@push.rocks/projectinfo';
|
|
53
55
|
import * as qenv from '@push.rocks/qenv';
|
|
54
56
|
import * as smartacme from '@push.rocks/smartacme';
|
|
57
|
+
import * as smartchallenge from '@push.rocks/smartchallenge';
|
|
55
58
|
import * as smartdata from '@push.rocks/smartdata';
|
|
56
59
|
import * as smartdns from '@push.rocks/smartdns';
|
|
57
60
|
import * as smartfs from '@push.rocks/smartfs';
|
|
@@ -72,7 +75,7 @@ import * as smartrx from '@push.rocks/smartrx';
|
|
|
72
75
|
import * as smartunique from '@push.rocks/smartunique';
|
|
73
76
|
import * as taskbuffer from '@push.rocks/taskbuffer';
|
|
74
77
|
|
|
75
|
-
export { projectinfo, qenv, smartacme, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
|
|
78
|
+
export { projectinfo, qenv, smartacme, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
|
|
76
79
|
|
|
77
80
|
// Define SmartLog types for use in error handling
|
|
78
81
|
export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
|
|
@@ -240,6 +240,7 @@ export class OpsViewSourceProfiles extends DeesElement {
|
|
|
240
240
|
ipBlockList,
|
|
241
241
|
...(maxConnections ? { maxConnections } : {}),
|
|
242
242
|
...(rateLimit ? { rateLimit } : {}),
|
|
243
|
+
...(profile.security?.challenge ? { challenge: profile.security.challenge } : {}),
|
|
243
244
|
},
|
|
244
245
|
});
|
|
245
246
|
modalArg.destroy();
|