@push.rocks/smartmta 8.2.1 → 9.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.
@@ -6,6 +6,21 @@ import type { Email } from '../core/classes.email.js';
6
6
  import { getErrorMessage } from '../../functions.errors.js';
7
7
  import { assertValidRetentionDays } from './functions.routing.validation.js';
8
8
 
9
+ /**
10
+ * Convert an address glob pattern ('a@b.com' | '*@b.com' | 'admin@*') into a
11
+ * case-insensitive full-match RegExp. Shared by route matching and auth
12
+ * account scope enforcement so both use identical pattern semantics.
13
+ */
14
+ export function addressGlobToRegExp(pattern: string): RegExp {
15
+ // Escape special regex characters except * and ?
16
+ const regexString = pattern
17
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
18
+ .replace(/\*/g, '.*')
19
+ .replace(/\?/g, '.');
20
+
21
+ return new RegExp(`^${regexString}$`, 'i');
22
+ }
23
+
9
24
  /**
10
25
  * Email router that evaluates routes and determines actions
11
26
  */
@@ -14,8 +29,9 @@ export class EmailRouter extends EventEmitter {
14
29
  private patternCache: Map<string, boolean> = new Map();
15
30
  private storageManager?: IStorageManager;
16
31
  private persistChanges: boolean;
32
+ private validateAction?: (route: IEmailRoute) => void;
17
33
  private initializationPromise: Promise<void>;
18
-
34
+
19
35
  /**
20
36
  * Create a new email router
21
37
  * @param routes Array of email routes
@@ -24,8 +40,15 @@ export class EmailRouter extends EventEmitter {
24
40
  constructor(routes: IEmailRoute[], options?: {
25
41
  storageManager?: IStorageManager;
26
42
  persistChanges?: boolean;
43
+ /**
44
+ * Additional owner-level action policy applied to every configured,
45
+ * updated, and hydrated route (e.g. an embedding server requiring a
46
+ * durable local-delivery owner for `deliver` actions).
47
+ */
48
+ validateAction?: (route: IEmailRoute) => void;
27
49
  }) {
28
50
  super();
51
+ this.validateAction = options?.validateAction;
29
52
  this.validateRoutes(routes);
30
53
  this.routes = this.sortRoutesByPriority(routes);
31
54
  this.storageManager = options?.storageManager;
@@ -49,6 +72,8 @@ export class EmailRouter extends EventEmitter {
49
72
  throw new Error('Invalid route: missing required fields');
50
73
  }
51
74
 
75
+ this.validateAction?.(route);
76
+
52
77
  if (route.action.type !== 'store') {
53
78
  return;
54
79
  }
@@ -69,6 +94,16 @@ export class EmailRouter extends EventEmitter {
69
94
  this.validateRoute(route);
70
95
  }
71
96
  }
97
+
98
+ /**
99
+ * Synchronously validate routes against structural rules and the owner's
100
+ * action policy without mutating router state. Callers that stage route
101
+ * updates in their own configuration should validate first so an invalid
102
+ * route set fails loudly before any state changes.
103
+ */
104
+ public assertRoutesValid(routes: IEmailRoute[]): void {
105
+ this.validateRoutes(routes);
106
+ }
72
107
 
73
108
  /**
74
109
  * Sort routes by priority (higher priority first)
@@ -165,11 +200,17 @@ export class EmailRouter extends EventEmitter {
165
200
  }
166
201
 
167
202
  // Check authentication
168
- if (match.authenticated !== undefined &&
203
+ if (match.authenticated !== undefined &&
169
204
  context.session.authenticated !== match.authenticated) {
170
205
  return false;
171
206
  }
172
-
207
+
208
+ // Check authenticated username binding
209
+ if (match.authenticatedUser !== undefined &&
210
+ !this.matchesAuthenticatedUser(context.session, match.authenticatedUser)) {
211
+ return false;
212
+ }
213
+
173
214
  // Check headers
174
215
  if (match.headers && !this.matchesHeaders(context.email, match.headers)) {
175
216
  return false;
@@ -233,6 +274,25 @@ export class EmailRouter extends EventEmitter {
233
274
  return false;
234
275
  }
235
276
 
277
+ /**
278
+ * Check if the session is authenticated as one of the required usernames.
279
+ * Unauthenticated sessions never match.
280
+ * @param session SMTP session
281
+ * @param usernames Required username or usernames
282
+ * @returns True if the session's authenticated username is in the set
283
+ */
284
+ private matchesAuthenticatedUser(
285
+ session: IEmailContext['session'],
286
+ usernames: string | string[],
287
+ ): boolean {
288
+ const username = session.user?.username;
289
+ if (!username) {
290
+ return false;
291
+ }
292
+ const allowed = Array.isArray(usernames) ? usernames : [usernames];
293
+ return allowed.includes(username);
294
+ }
295
+
236
296
  /**
237
297
  * Check if client IP matches patterns
238
298
  * @param context Email context
@@ -354,13 +414,7 @@ export class EmailRouter extends EventEmitter {
354
414
  * @returns Regular expression
355
415
  */
356
416
  private globToRegExp(pattern: string): RegExp {
357
- // Escape special regex characters except * and ?
358
- let regexString = pattern
359
- .replace(/[.+^${}()|[\]\\]/g, '\\$&')
360
- .replace(/\*/g, '.*')
361
- .replace(/\?/g, '.');
362
-
363
- return new RegExp(`^${regexString}$`, 'i');
417
+ return addressGlobToRegExp(pattern);
364
418
  }
365
419
 
366
420
  /**