@push.rocks/smartmta 8.0.1 → 8.1.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/changelog.md +35 -0
- package/dist_rust/mailer-bin_linux_amd64 +0 -0
- package/dist_rust/mailer-bin_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +5 -0
- package/dist_ts/index.js +6 -1
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +15 -2
- package/dist_ts/mail/delivery/classes.delivery.queue.js +91 -8
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -2
- package/dist_ts/mail/delivery/classes.delivery.system.js +106 -36
- package/dist_ts/mail/routing/classes.domain.registry.js +5 -1
- package/dist_ts/mail/routing/classes.email.action.executor.js +3 -1
- package/dist_ts/mail/routing/classes.email.router.d.ts +2 -0
- package/dist_ts/mail/routing/classes.email.router.js +28 -21
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +85 -3
- package/dist_ts/mail/routing/classes.unified.email.server.js +517 -60
- package/dist_ts/mail/routing/functions.routing.validation.d.ts +8 -0
- package/dist_ts/mail/routing/functions.routing.validation.js +18 -0
- package/dist_ts/mail/routing/interfaces.d.ts +11 -1
- package/dist_ts/plugins.d.ts +2 -1
- package/dist_ts/plugins.js +3 -2
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +32 -1
- package/dist_ts/security/classes.rustsecuritybridge.js +13 -1
- package/dist_ts/security/index.d.ts +1 -1
- package/dist_ts/security/index.js +1 -1
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +5 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +126 -6
- package/ts/mail/delivery/classes.delivery.system.ts +137 -41
- package/ts/mail/routing/classes.domain.registry.ts +4 -0
- package/ts/mail/routing/classes.email.action.executor.ts +4 -0
- package/ts/mail/routing/classes.email.router.ts +33 -20
- package/ts/mail/routing/classes.unified.email.server.ts +703 -72
- package/ts/mail/routing/functions.routing.validation.ts +36 -0
- package/ts/mail/routing/interfaces.ts +13 -1
- package/ts/plugins.ts +2 -0
- package/ts/security/classes.rustsecuritybridge.ts +46 -1
- package/ts/security/index.ts +2 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
UnifiedDeliveryQueue,
|
|
11
11
|
type IQueueItem,
|
|
12
12
|
type IRecipientPermanentFailure,
|
|
13
|
+
type IBounceOperation,
|
|
13
14
|
} from './classes.delivery.queue.js';
|
|
14
15
|
import { Email } from '../core/classes.email.js';
|
|
15
16
|
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
|
|
@@ -41,6 +42,17 @@ export interface IDeliveryHandler {
|
|
|
41
42
|
deliver(item: IQueueItem): Promise<any>;
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
export interface IDeliveryFailureNotice {
|
|
46
|
+
itemId: string;
|
|
47
|
+
operation: Readonly<IBounceOperation>;
|
|
48
|
+
sender: string;
|
|
49
|
+
headers: Readonly<Record<string, string>>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface IDeliveryFailureHandler {
|
|
53
|
+
processDeliveryFailure(notice: IDeliveryFailureNotice): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/**
|
|
45
57
|
* Delivery options
|
|
46
58
|
*/
|
|
@@ -71,7 +83,9 @@ export interface IMultiModeDeliveryOptions {
|
|
|
71
83
|
bounceHandler?: {
|
|
72
84
|
processSmtpFailure: (recipient: string, smtpResponse: string, options: any) => Promise<any>;
|
|
73
85
|
};
|
|
74
|
-
|
|
86
|
+
/** Durable, idempotent terminal-delivery notice consumer. */
|
|
87
|
+
deliveryFailureHandler?: IDeliveryFailureHandler;
|
|
88
|
+
|
|
75
89
|
// Event hooks
|
|
76
90
|
onDeliveryStart?: (item: IQueueItem) => Promise<void>;
|
|
77
91
|
onDeliverySuccess?: (item: IQueueItem, result: any) => Promise<void>;
|
|
@@ -112,9 +126,10 @@ export interface IDeliveryStats {
|
|
|
112
126
|
*/
|
|
113
127
|
type TResolvedMultiModeDeliveryOptions = Omit<
|
|
114
128
|
Required<IMultiModeDeliveryOptions>,
|
|
115
|
-
'bounceHandler'
|
|
129
|
+
'bounceHandler' | 'deliveryFailureHandler'
|
|
116
130
|
> & {
|
|
117
131
|
bounceHandler?: NonNullable<IMultiModeDeliveryOptions['bounceHandler']>;
|
|
132
|
+
deliveryFailureHandler: IDeliveryFailureHandler;
|
|
118
133
|
};
|
|
119
134
|
|
|
120
135
|
export class MultiModeDeliverySystem extends EventEmitter {
|
|
@@ -165,6 +180,12 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
165
180
|
perPatternRateLimit: options.perPatternRateLimit || {},
|
|
166
181
|
processBounces: options.processBounces !== false, // Default to true
|
|
167
182
|
bounceHandler: options.bounceHandler,
|
|
183
|
+
deliveryFailureHandler: options.deliveryFailureHandler || {
|
|
184
|
+
processDeliveryFailure: async (notice) => {
|
|
185
|
+
logger.log('warn', `Terminal delivery failure persisted for ${notice.operation.recipient}`);
|
|
186
|
+
await emitSafely(this, 'deliveryFailureNotice', notice);
|
|
187
|
+
},
|
|
188
|
+
},
|
|
168
189
|
onDeliveryStart: options.onDeliveryStart || (async () => {}),
|
|
169
190
|
onDeliverySuccess: options.onDeliverySuccess || (async () => {}),
|
|
170
191
|
onDeliveryFailed: options.onDeliveryFailed || (async () => {})
|
|
@@ -313,34 +334,68 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
313
334
|
}
|
|
314
335
|
|
|
315
336
|
/**
|
|
316
|
-
* Drain durable
|
|
337
|
+
* Drain durable failure work with one in-process flight. Custom handlers
|
|
317
338
|
* receive operationId and must use it for their own idempotency.
|
|
318
339
|
*/
|
|
319
340
|
public async drainBounceOutbox(): Promise<void> {
|
|
320
341
|
if (this.bounceDrainPromise) return this.bounceDrainPromise;
|
|
321
342
|
const drain = (async () => {
|
|
322
|
-
if (
|
|
323
|
-
|
|
343
|
+
if (this.options.processBounces && this.options.bounceHandler) {
|
|
344
|
+
for (const { itemId, operation } of this.queue.listPendingBounceOperations()) {
|
|
345
|
+
const item = this.queue.getItem(itemId);
|
|
346
|
+
if (!item) continue;
|
|
347
|
+
const email = item.processingResult as Email;
|
|
348
|
+
try {
|
|
349
|
+
await this.options.bounceHandler.processSmtpFailure(
|
|
350
|
+
operation.recipient,
|
|
351
|
+
operation.smtpResponse,
|
|
352
|
+
{
|
|
353
|
+
operationId: operation.operationId,
|
|
354
|
+
sender: email.from,
|
|
355
|
+
originalEmailId: item.id,
|
|
356
|
+
headers: email.headers,
|
|
357
|
+
statusCode: String(operation.smtpCode),
|
|
358
|
+
},
|
|
359
|
+
);
|
|
360
|
+
await this.queue.markBounceOperationCompleted(itemId, operation.operationId);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
logger.log(
|
|
363
|
+
'error',
|
|
364
|
+
`Bounce operation ${operation.operationId} failed: ${(error as Error).message}`,
|
|
365
|
+
);
|
|
366
|
+
try {
|
|
367
|
+
await this.queue.markBounceOperationFailed(
|
|
368
|
+
itemId,
|
|
369
|
+
operation.operationId,
|
|
370
|
+
(error as Error).message,
|
|
371
|
+
);
|
|
372
|
+
} catch (checkpointError) {
|
|
373
|
+
logger.log(
|
|
374
|
+
'error',
|
|
375
|
+
`Could not checkpoint failed bounce operation ${operation.operationId}: ${(checkpointError as Error).message}`,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const { itemId, operation } of this.queue.listPendingDeliveryFailureOperations()) {
|
|
324
383
|
const item = this.queue.getItem(itemId);
|
|
325
384
|
if (!item) continue;
|
|
326
385
|
const email = item.processingResult as Email;
|
|
386
|
+
const notice: IDeliveryFailureNotice = {
|
|
387
|
+
itemId,
|
|
388
|
+
operation,
|
|
389
|
+
sender: email.from,
|
|
390
|
+
headers: { ...email.headers },
|
|
391
|
+
};
|
|
327
392
|
try {
|
|
328
|
-
await this.options.
|
|
329
|
-
operation.recipient,
|
|
330
|
-
operation.smtpResponse,
|
|
331
|
-
{
|
|
332
|
-
operationId: operation.operationId,
|
|
333
|
-
sender: email.from,
|
|
334
|
-
originalEmailId: item.id,
|
|
335
|
-
headers: email.headers,
|
|
336
|
-
statusCode: String(operation.smtpCode),
|
|
337
|
-
},
|
|
338
|
-
);
|
|
393
|
+
await this.options.deliveryFailureHandler.processDeliveryFailure(notice);
|
|
339
394
|
await this.queue.markBounceOperationCompleted(itemId, operation.operationId);
|
|
340
395
|
} catch (error) {
|
|
341
396
|
logger.log(
|
|
342
397
|
'error',
|
|
343
|
-
`
|
|
398
|
+
`Delivery-failure operation ${operation.operationId} failed: ${(error as Error).message}`,
|
|
344
399
|
);
|
|
345
400
|
try {
|
|
346
401
|
await this.queue.markBounceOperationFailed(
|
|
@@ -351,7 +406,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
351
406
|
} catch (checkpointError) {
|
|
352
407
|
logger.log(
|
|
353
408
|
'error',
|
|
354
|
-
`Could not checkpoint failed
|
|
409
|
+
`Could not checkpoint failed delivery-failure operation ${operation.operationId}: ${(checkpointError as Error).message}`,
|
|
355
410
|
);
|
|
356
411
|
}
|
|
357
412
|
}
|
|
@@ -619,6 +674,12 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
619
674
|
|
|
620
675
|
const transition = await this.queue.markFailedWithTransition(item.id, deliveryError.message, {
|
|
621
676
|
retryable: deliveryError.retryable,
|
|
677
|
+
terminalFailure: {
|
|
678
|
+
failureClass: deliveryError.errorType.startsWith('self_mx_')
|
|
679
|
+
? 'localTopology'
|
|
680
|
+
: 'transport',
|
|
681
|
+
...(deliveryError.smtpCode !== undefined ? { smtpCode: deliveryError.smtpCode } : {}),
|
|
682
|
+
},
|
|
622
683
|
});
|
|
623
684
|
if (this.running) void this.drainBounceOutbox();
|
|
624
685
|
if (!transition.updated) return;
|
|
@@ -832,6 +893,9 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
832
893
|
&& typeof failure.smtpCode === 'number'
|
|
833
894
|
&& failure.smtpCode >= 500
|
|
834
895
|
));
|
|
896
|
+
const allLocalTopologyFailures = normalized.every((failure) => (
|
|
897
|
+
failure.errorType.startsWith('self_mx_')
|
|
898
|
+
));
|
|
835
899
|
const selected = normalized.find((failure) => failure.retryable)
|
|
836
900
|
|| normalized[normalized.length - 1];
|
|
837
901
|
const smtpCode = allPermanentRecipientRejections
|
|
@@ -839,7 +903,11 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
839
903
|
: selected.smtpCode;
|
|
840
904
|
return new SmtpDeliveryError({
|
|
841
905
|
message: normalized.map((failure) => failure.message).join('; '),
|
|
842
|
-
errorType: allPermanentRecipientRejections
|
|
906
|
+
errorType: allPermanentRecipientRejections
|
|
907
|
+
? 'protocol'
|
|
908
|
+
: allLocalTopologyFailures
|
|
909
|
+
? selected.errorType
|
|
910
|
+
: 'smtp_aggregate',
|
|
843
911
|
retryable,
|
|
844
912
|
...(smtpCode !== undefined ? { smtpCode } : {}),
|
|
845
913
|
phase: allPermanentRecipientRejections ? 'rcpt_to' : selected.phase,
|
|
@@ -903,37 +971,65 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
|
|
903
971
|
|
|
904
972
|
for (const [domain, recipients] of domainGroups) {
|
|
905
973
|
const allMxHosts = await this.resolveMxForDomain(domain);
|
|
974
|
+
let recipientsForMx = [...recipients];
|
|
975
|
+
const domainFailures: SmtpDeliveryError[] = [];
|
|
976
|
+
const acceptedForDomain: string[] = [];
|
|
977
|
+
const mxHosts: typeof allMxHosts = [];
|
|
978
|
+
|
|
979
|
+
// RFC 5321 §5.3.6: skip local MX identities but continue to non-local
|
|
980
|
+
// backup MX hosts. Proxy connection targets are intentionally absent
|
|
981
|
+
// from this identity check.
|
|
982
|
+
for (const mx of allMxHosts) {
|
|
983
|
+
const identity = await this.emailServer.resolveSelfMxIdentity(mx.exchange);
|
|
984
|
+
if (!identity.isSelf && !identity.indeterminate) {
|
|
985
|
+
mxHosts.push(mx);
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
906
988
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
989
|
+
const startedAt = new Date();
|
|
990
|
+
const retryable = identity.source !== 'explicit';
|
|
991
|
+
const errorType = identity.indeterminate
|
|
992
|
+
? 'self_mx_dns_indeterminate'
|
|
993
|
+
: identity.source === 'explicit'
|
|
994
|
+
? 'self_mx_config'
|
|
995
|
+
: 'self_mx_loop';
|
|
996
|
+
const reason = identity.indeterminate
|
|
997
|
+
? 'could not be safely classified against current DNS and listener topology'
|
|
998
|
+
: identity.source === 'explicit'
|
|
999
|
+
? 'exactly matches a configured listener identity'
|
|
1000
|
+
: 'resolves to the current listener topology';
|
|
1001
|
+
const evidence = identity.evidence.length > 0
|
|
1002
|
+
? ` (${identity.evidence.join('; ')})`
|
|
1003
|
+
: '';
|
|
913
1004
|
const loopError = new SmtpDeliveryError({
|
|
914
|
-
message:
|
|
915
|
-
|
|
916
|
-
|
|
1005
|
+
message: identity.indeterminate
|
|
1006
|
+
? `MX ${mx.exchange} for ${domain} ${reason}${evidence}`
|
|
1007
|
+
: `MX ${mx.exchange} for ${domain} points back at this server because it ${reason}${evidence}`,
|
|
1008
|
+
errorType,
|
|
1009
|
+
retryable,
|
|
917
1010
|
phase: 'connect',
|
|
918
|
-
transactionLog: [
|
|
1011
|
+
transactionLog: [{
|
|
1012
|
+
timestampMs: Date.now(),
|
|
1013
|
+
phase: 'connect',
|
|
1014
|
+
direction: 'system',
|
|
1015
|
+
text: `Skipped ${mx.exchange}: ${reason}`,
|
|
1016
|
+
}],
|
|
919
1017
|
recipients,
|
|
920
1018
|
recipientDomain: domain,
|
|
921
1019
|
});
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1020
|
+
await this.persistSmtpAttempt({
|
|
1021
|
+
item,
|
|
1022
|
+
targetHost: mx.exchange,
|
|
1023
|
+
targetPort: 25,
|
|
1024
|
+
recipientDomain: domain,
|
|
1025
|
+
recipients,
|
|
1026
|
+
startedAt,
|
|
1027
|
+
error: loopError,
|
|
928
1028
|
});
|
|
929
|
-
|
|
930
|
-
|
|
1029
|
+
domainFailures.push(loopError);
|
|
1030
|
+
logger.log(retryable ? 'warn' : 'error', `MTA: skipped MX ${mx.exchange} for ${domain}: ${loopError.message}`);
|
|
931
1031
|
}
|
|
932
1032
|
|
|
933
|
-
let recipientsForMx = [...recipients];
|
|
934
|
-
const domainFailures: SmtpDeliveryError[] = [];
|
|
935
|
-
const acceptedForDomain: string[] = [];
|
|
936
|
-
|
|
937
1033
|
for (const mx of mxHosts) {
|
|
938
1034
|
if (recipientsForMx.length === 0) break;
|
|
939
1035
|
const startedAt = new Date();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IEmailDomainConfig } from './interfaces.js';
|
|
2
2
|
import { logger } from '../../logger.js';
|
|
3
|
+
import { assertValidInboundPolicy } from './functions.routing.validation.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Registry for email domain configurations
|
|
@@ -30,6 +31,9 @@ export class DomainRegistry {
|
|
|
30
31
|
|
|
31
32
|
// Process and store domain configurations
|
|
32
33
|
for (const config of domainConfigs) {
|
|
34
|
+
if (config.inboundPolicy) {
|
|
35
|
+
assertValidInboundPolicy(config.inboundPolicy, config.domain);
|
|
36
|
+
}
|
|
33
37
|
const processedConfig = this.applyDefaults(config);
|
|
34
38
|
this.domains.set(config.domain.toLowerCase(), processedConfig);
|
|
35
39
|
logger.log('info', `Registered domain: ${config.domain} with DNS mode: ${processedConfig.dnsMode}`);
|
|
@@ -72,6 +72,10 @@ export class EmailActionExecutor {
|
|
|
72
72
|
case 'deliver':
|
|
73
73
|
await this.handleDeliverAction(action, email, context);
|
|
74
74
|
break;
|
|
75
|
+
case 'store':
|
|
76
|
+
throw new Error(
|
|
77
|
+
'Store actions require raw SMTP context and an awaited hooks.onAcceptEnvelope handler',
|
|
78
|
+
);
|
|
75
79
|
case 'reject':
|
|
76
80
|
await this.handleRejectAction(action, email, context);
|
|
77
81
|
break;
|
|
@@ -4,6 +4,7 @@ import type { IStorageManager } from '../interfaces.storage.js';
|
|
|
4
4
|
import type { IEmailRoute, IEmailMatch, IEmailAction, IEmailContext } from './interfaces.js';
|
|
5
5
|
import type { Email } from '../core/classes.email.js';
|
|
6
6
|
import { getErrorMessage } from '../../functions.errors.js';
|
|
7
|
+
import { assertValidRetentionDays } from './functions.routing.validation.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Email router that evaluates routes and determines actions
|
|
@@ -25,6 +26,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
25
26
|
persistChanges?: boolean;
|
|
26
27
|
}) {
|
|
27
28
|
super();
|
|
29
|
+
this.validateRoutes(routes);
|
|
28
30
|
this.routes = this.sortRoutesByPriority(routes);
|
|
29
31
|
this.storageManager = options?.storageManager;
|
|
30
32
|
this.persistChanges = options?.persistChanges ?? !!this.storageManager;
|
|
@@ -41,6 +43,32 @@ export class EmailRouter extends EventEmitter {
|
|
|
41
43
|
public async initialize(): Promise<void> {
|
|
42
44
|
await this.initializationPromise;
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
private validateRoute(route: IEmailRoute): void {
|
|
48
|
+
if (!route?.name || !route.match || !route.action) {
|
|
49
|
+
throw new Error('Invalid route: missing required fields');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (route.action.type !== 'store') {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!route.action.store) {
|
|
56
|
+
throw new Error(`Store route ${route.name} requires store configuration`);
|
|
57
|
+
}
|
|
58
|
+
assertValidRetentionDays(
|
|
59
|
+
route.action.store.retentionDays,
|
|
60
|
+
`Store route ${route.name}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private validateRoutes(routes: IEmailRoute[]): void {
|
|
65
|
+
if (!Array.isArray(routes)) {
|
|
66
|
+
throw new Error('Invalid routes: expected an array');
|
|
67
|
+
}
|
|
68
|
+
for (const route of routes) {
|
|
69
|
+
this.validateRoute(route);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
44
72
|
|
|
45
73
|
/**
|
|
46
74
|
* Sort routes by priority (higher priority first)
|
|
@@ -69,6 +97,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
69
97
|
* @param persist Whether to persist changes (defaults to persistChanges setting)
|
|
70
98
|
*/
|
|
71
99
|
public async updateRoutes(routes: IEmailRoute[], persist?: boolean): Promise<void> {
|
|
100
|
+
this.validateRoutes(routes);
|
|
72
101
|
this.routes = this.sortRoutesByPriority(routes);
|
|
73
102
|
this.clearCache();
|
|
74
103
|
this.emit('routesUpdated', this.routes);
|
|
@@ -409,11 +438,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
409
438
|
|
|
410
439
|
try {
|
|
411
440
|
// Validate all routes before saving
|
|
412
|
-
|
|
413
|
-
if (!route.name || !route.match || !route.action) {
|
|
414
|
-
throw new Error(`Invalid route: ${JSON.stringify(route)}`);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
441
|
+
this.validateRoutes(this.routes);
|
|
417
442
|
|
|
418
443
|
const routesData = JSON.stringify(this.routes, null, 2);
|
|
419
444
|
await this.storageManager.set('/email/routes/config.json', routesData);
|
|
@@ -447,13 +472,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
447
472
|
|
|
448
473
|
const loadedRoutes = JSON.parse(routesData) as IEmailRoute[];
|
|
449
474
|
|
|
450
|
-
|
|
451
|
-
for (const route of loadedRoutes) {
|
|
452
|
-
if (!route.name || !route.match || !route.action) {
|
|
453
|
-
console.warn(`Skipping invalid route: ${JSON.stringify(route)}`);
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
475
|
+
this.validateRoutes(loadedRoutes);
|
|
457
476
|
|
|
458
477
|
if (options?.replace) {
|
|
459
478
|
// Replace all routes
|
|
@@ -491,10 +510,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
491
510
|
* @param persist Whether to persist changes
|
|
492
511
|
*/
|
|
493
512
|
public async addRoute(route: IEmailRoute, persist?: boolean): Promise<void> {
|
|
494
|
-
|
|
495
|
-
if (!route.name || !route.match || !route.action) {
|
|
496
|
-
throw new Error('Invalid route: missing required fields');
|
|
497
|
-
}
|
|
513
|
+
this.validateRoute(route);
|
|
498
514
|
|
|
499
515
|
// Check if route already exists
|
|
500
516
|
const existingIndex = this.routes.findIndex(r => r.name === route.name);
|
|
@@ -547,10 +563,7 @@ export class EmailRouter extends EventEmitter {
|
|
|
547
563
|
* @param persist Whether to persist changes
|
|
548
564
|
*/
|
|
549
565
|
public async updateRoute(name: string, route: IEmailRoute, persist?: boolean): Promise<void> {
|
|
550
|
-
|
|
551
|
-
if (!route.name || !route.match || !route.action) {
|
|
552
|
-
throw new Error('Invalid route: missing required fields');
|
|
553
|
-
}
|
|
566
|
+
this.validateRoute(route);
|
|
554
567
|
|
|
555
568
|
const index = this.routes.findIndex(r => r.name === name);
|
|
556
569
|
|