@push.rocks/smartmta 6.5.2 → 8.0.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.
Files changed (84) hide show
  1. package/changelog.md +40 -0
  2. package/dist_rust/mailer-bin_linux_amd64 +0 -0
  3. package/dist_rust/mailer-bin_linux_arm64 +0 -0
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/functions.errors.d.ts +3 -0
  6. package/dist_ts/functions.errors.js +8 -0
  7. package/dist_ts/index.d.ts +3 -0
  8. package/dist_ts/index.js +4 -1
  9. package/dist_ts/mail/core/classes.bouncemanager.d.ts +16 -12
  10. package/dist_ts/mail/core/classes.bouncemanager.js +146 -129
  11. package/dist_ts/mail/core/classes.email.js +15 -13
  12. package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
  13. package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
  14. package/dist_ts/mail/core/classes.templatemanager.d.ts +10 -6
  15. package/dist_ts/mail/core/classes.templatemanager.js +35 -51
  16. package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +92 -22
  17. package/dist_ts/mail/delivery/classes.delivery.queue.js +738 -151
  18. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
  19. package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
  20. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
  21. package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
  22. package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
  23. package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
  24. package/dist_ts/mail/delivery/interfaces.js +1 -1
  25. package/dist_ts/mail/interfaces.storage.d.ts +37 -6
  26. package/dist_ts/mail/interfaces.storage.js +33 -3
  27. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +10 -6
  28. package/dist_ts/mail/routing/classes.dkim.manager.js +47 -31
  29. package/dist_ts/mail/routing/classes.dns.manager.d.ts +7 -5
  30. package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
  31. package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
  32. package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
  33. package/dist_ts/mail/routing/classes.email.router.d.ts +5 -2
  34. package/dist_ts/mail/routing/classes.email.router.js +17 -12
  35. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +12 -6
  36. package/dist_ts/mail/routing/classes.unified.email.server.js +69 -78
  37. package/dist_ts/mail/routing/interfaces.d.ts +0 -2
  38. package/dist_ts/mail/security/classes.dkimcreator.d.ts +22 -45
  39. package/dist_ts/mail/security/classes.dkimcreator.js +93 -296
  40. package/dist_ts/mail/security/classes.spfverifier.js +5 -3
  41. package/dist_ts/paths.d.ts +0 -12
  42. package/dist_ts/paths.js +3 -36
  43. package/dist_ts/plugins.d.ts +2 -5
  44. package/dist_ts/plugins.js +3 -6
  45. package/dist_ts/security/classes.contentscanner.js +14 -12
  46. package/dist_ts/security/classes.ipreputationchecker.d.ts +9 -6
  47. package/dist_ts/security/classes.ipreputationchecker.js +42 -93
  48. package/dist_ts/security/classes.rustsecuritybridge.d.ts +52 -4
  49. package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
  50. package/dist_ts/security/classes.securitylogger.js +7 -5
  51. package/dist_ts/security/index.d.ts +1 -1
  52. package/dist_ts/security/index.js +2 -2
  53. package/package.json +8 -9
  54. package/readme.hints.md +4 -3
  55. package/readme.md +50 -18
  56. package/readme.plan.md +6 -0
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/functions.errors.ts +8 -0
  59. package/ts/index.ts +3 -0
  60. package/ts/mail/core/classes.bouncemanager.ts +188 -155
  61. package/ts/mail/core/classes.email.ts +20 -14
  62. package/ts/mail/core/classes.emailvalidator.ts +9 -7
  63. package/ts/mail/core/classes.templatemanager.ts +42 -57
  64. package/ts/mail/delivery/classes.delivery.queue.ts +996 -185
  65. package/ts/mail/delivery/classes.delivery.system.ts +583 -170
  66. package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
  67. package/ts/mail/delivery/functions.safe-observers.ts +45 -0
  68. package/ts/mail/delivery/interfaces.ts +27 -1
  69. package/ts/mail/interfaces.storage.ts +64 -10
  70. package/ts/mail/routing/classes.dkim.manager.ts +65 -40
  71. package/ts/mail/routing/classes.dns.manager.ts +39 -16
  72. package/ts/mail/routing/classes.email.action.executor.ts +64 -17
  73. package/ts/mail/routing/classes.email.router.ts +20 -13
  74. package/ts/mail/routing/classes.unified.email.server.ts +107 -86
  75. package/ts/mail/routing/interfaces.ts +0 -2
  76. package/ts/mail/security/classes.dkimcreator.ts +150 -355
  77. package/ts/mail/security/classes.spfverifier.ts +4 -2
  78. package/ts/paths.ts +2 -41
  79. package/ts/plugins.ts +1 -6
  80. package/ts/security/classes.contentscanner.ts +14 -12
  81. package/ts/security/classes.ipreputationchecker.ts +46 -99
  82. package/ts/security/classes.rustsecuritybridge.ts +272 -6
  83. package/ts/security/classes.securitylogger.ts +6 -4
  84. package/ts/security/index.ts +5 -1
@@ -69,6 +69,251 @@ interface IContentScanResult {
69
69
 
70
70
  // --- SMTP Client types ---
71
71
 
72
+ export type TSmtpDeliveryPhase =
73
+ | 'connect'
74
+ | 'greeting'
75
+ | 'ehlo'
76
+ | 'starttls'
77
+ | 'tls_handshake'
78
+ | 'post_tls_ehlo'
79
+ | 'auth'
80
+ | 'mail_from'
81
+ | 'rcpt_to'
82
+ | 'data_command'
83
+ | 'message_body'
84
+ | 'final_response'
85
+ | 'rset'
86
+ | 'quit'
87
+ | 'unknown';
88
+
89
+ export interface ISmtpTranscriptEntry {
90
+ timestampMs: number;
91
+ phase: TSmtpDeliveryPhase;
92
+ direction: 'client' | 'server' | 'system';
93
+ text: string;
94
+ responseCode?: number;
95
+ }
96
+
97
+ /** Persisted SMTP traces are bounded by entries, Unicode code points, and UTF-8 bytes. */
98
+ export const SMTP_TRANSCRIPT_MAX_ENTRIES = 200;
99
+ export const SMTP_TRANSCRIPT_MAX_ENTRY_CODE_POINTS = 2_048;
100
+ export const SMTP_TRANSCRIPT_MAX_UTF8_BYTES = 64 * 1_024;
101
+ export const SMTP_TRANSCRIPT_TRUNCATION_MARKER = 'SMTP transcript truncated';
102
+
103
+ const utf8ByteLength = (valueArg: string): number => Buffer.byteLength(valueArg, 'utf8');
104
+
105
+ const truncateCodePoints = (valueArg: string): string => {
106
+ const codePoints = [...valueArg];
107
+ if (codePoints.length <= SMTP_TRANSCRIPT_MAX_ENTRY_CODE_POINTS) return valueArg;
108
+ return `${codePoints.slice(0, SMTP_TRANSCRIPT_MAX_ENTRY_CODE_POINTS - 1).join('')}…`;
109
+ };
110
+
111
+ const redactAuthTranscriptText = (
112
+ phaseArg: TSmtpDeliveryPhase,
113
+ directionArg: ISmtpTranscriptEntry['direction'],
114
+ textArg: string,
115
+ ): string => {
116
+ if (phaseArg !== 'auth') return textArg;
117
+ if (directionArg === 'client') return '[AUTH command redacted]';
118
+ if (directionArg === 'server') return '[AUTH response redacted]';
119
+ return '[AUTH diagnostic redacted]';
120
+ };
121
+
122
+ /**
123
+ * Normalize an untrusted SMTP trace before exposing or persisting it.
124
+ * Entry text is measured in Unicode code points; the aggregate is measured in UTF-8 bytes.
125
+ */
126
+ export const normalizeSmtpTranscript = (entriesArg: unknown[]): ISmtpTranscriptEntry[] => {
127
+ const normalized: ISmtpTranscriptEntry[] = [];
128
+ let totalUtf8Bytes = 0;
129
+
130
+ const appendTruncationMarker = (referenceArg?: Partial<ISmtpTranscriptEntry>): void => {
131
+ const markerBytes = utf8ByteLength(SMTP_TRANSCRIPT_TRUNCATION_MARKER);
132
+ while (
133
+ normalized.length >= SMTP_TRANSCRIPT_MAX_ENTRIES
134
+ || totalUtf8Bytes + markerBytes > SMTP_TRANSCRIPT_MAX_UTF8_BYTES
135
+ ) {
136
+ const removed = normalized.pop();
137
+ if (!removed) break;
138
+ totalUtf8Bytes -= utf8ByteLength(removed.text);
139
+ }
140
+ if (
141
+ normalized.length < SMTP_TRANSCRIPT_MAX_ENTRIES
142
+ && totalUtf8Bytes + markerBytes <= SMTP_TRANSCRIPT_MAX_UTF8_BYTES
143
+ ) {
144
+ normalized.push({
145
+ timestampMs: typeof referenceArg?.timestampMs === 'number'
146
+ && Number.isFinite(referenceArg.timestampMs)
147
+ ? referenceArg.timestampMs
148
+ : Date.now(),
149
+ phase: isSmtpDeliveryPhase(referenceArg?.phase) ? referenceArg.phase : 'unknown',
150
+ direction: 'system',
151
+ text: SMTP_TRANSCRIPT_TRUNCATION_MARKER,
152
+ });
153
+ totalUtf8Bytes += markerBytes;
154
+ }
155
+ };
156
+
157
+ for (const valueArg of entriesArg) {
158
+ if (!valueArg || typeof valueArg !== 'object') continue;
159
+ const value = valueArg as Partial<ISmtpTranscriptEntry>;
160
+ if (
161
+ typeof value.timestampMs !== 'number'
162
+ || !Number.isFinite(value.timestampMs)
163
+ || !isSmtpDeliveryPhase(value.phase)
164
+ || !['client', 'server', 'system'].includes(value.direction as string)
165
+ || typeof value.text !== 'string'
166
+ ) {
167
+ continue;
168
+ }
169
+
170
+ const direction = value.direction as ISmtpTranscriptEntry['direction'];
171
+ const text = truncateCodePoints(redactAuthTranscriptText(value.phase, direction, value.text));
172
+ const textBytes = utf8ByteLength(text);
173
+ if (
174
+ normalized.length >= SMTP_TRANSCRIPT_MAX_ENTRIES
175
+ || totalUtf8Bytes + textBytes > SMTP_TRANSCRIPT_MAX_UTF8_BYTES
176
+ ) {
177
+ appendTruncationMarker(value);
178
+ break;
179
+ }
180
+
181
+ normalized.push({
182
+ timestampMs: value.timestampMs,
183
+ phase: value.phase,
184
+ direction,
185
+ text,
186
+ ...(typeof value.responseCode === 'number' && Number.isInteger(value.responseCode)
187
+ ? { responseCode: value.responseCode }
188
+ : {}),
189
+ });
190
+ totalUtf8Bytes += textBytes;
191
+ }
192
+
193
+ return normalized;
194
+ };
195
+
196
+ export interface ISmtpDeliveryErrorDetails {
197
+ message: string;
198
+ errorType: string;
199
+ retryable: boolean;
200
+ smtpCode?: number;
201
+ phase: TSmtpDeliveryPhase;
202
+ transactionLog: ISmtpTranscriptEntry[];
203
+ recipients?: string[];
204
+ recipientDomain?: string;
205
+ recipientResults?: ISmtpRecipientResult[];
206
+ }
207
+
208
+ interface ISmtpRecipientResult {
209
+ recipient: string;
210
+ accepted: boolean;
211
+ responseCode: number;
212
+ }
213
+
214
+ export class SmtpDeliveryError extends Error implements ISmtpDeliveryErrorDetails {
215
+ public readonly errorType: string;
216
+ public readonly retryable: boolean;
217
+ public readonly smtpCode?: number;
218
+ public readonly phase: TSmtpDeliveryPhase;
219
+ public readonly transactionLog: ISmtpTranscriptEntry[];
220
+ public readonly recipients: string[];
221
+ public readonly recipientDomain?: string;
222
+ public readonly recipientResults: ISmtpRecipientResult[];
223
+
224
+ constructor(details: ISmtpDeliveryErrorDetails, cause?: unknown) {
225
+ const transactionLog = normalizeSmtpTranscript(details.transactionLog);
226
+ const authFailure = (
227
+ details.phase === 'auth'
228
+ || details.errorType === 'authentication'
229
+ || transactionLog.some((entry) => entry.phase === 'auth')
230
+ );
231
+ super(authFailure ? 'SMTP authentication failed' : details.message);
232
+ this.name = 'SmtpDeliveryError';
233
+ this.errorType = details.errorType;
234
+ this.retryable = details.retryable;
235
+ this.smtpCode = details.smtpCode;
236
+ this.phase = details.phase;
237
+ this.transactionLog = transactionLog;
238
+ this.recipients = details.recipients || [];
239
+ this.recipientDomain = details.recipientDomain;
240
+ this.recipientResults = (details.recipientResults || []).map((result) => ({ ...result }));
241
+ if (cause !== undefined && !authFailure) {
242
+ (this as Error & { cause?: unknown }).cause = cause;
243
+ }
244
+ }
245
+
246
+ public static from(errorArg: unknown): SmtpDeliveryError {
247
+ if (errorArg instanceof SmtpDeliveryError) {
248
+ return errorArg;
249
+ }
250
+ const source = errorArg instanceof Error ? errorArg : new Error(String(errorArg));
251
+ try {
252
+ const parsed = JSON.parse(source.message) as Partial<ISmtpDeliveryErrorDetails>;
253
+ if (typeof parsed.message !== 'string' || typeof parsed.retryable !== 'boolean') {
254
+ throw new Error('Incomplete SMTP error payload');
255
+ }
256
+ const phase = isSmtpDeliveryPhase(parsed.phase) ? parsed.phase : 'unknown';
257
+ const transactionLog = Array.isArray(parsed.transactionLog)
258
+ ? normalizeSmtpTranscript(parsed.transactionLog)
259
+ : [];
260
+ return new SmtpDeliveryError({
261
+ message: parsed.message,
262
+ errorType: typeof parsed.errorType === 'string' ? parsed.errorType : 'unknown',
263
+ retryable: parsed.retryable,
264
+ ...(typeof parsed.smtpCode === 'number' ? { smtpCode: parsed.smtpCode } : {}),
265
+ phase,
266
+ transactionLog,
267
+ recipients: Array.isArray(parsed.recipients)
268
+ ? parsed.recipients.filter((value): value is string => typeof value === 'string')
269
+ : [],
270
+ recipientResults: Array.isArray(parsed.recipientResults)
271
+ ? parsed.recipientResults.filter((value): value is ISmtpRecipientResult => (
272
+ Boolean(value)
273
+ && typeof value === 'object'
274
+ && typeof (value as ISmtpRecipientResult).recipient === 'string'
275
+ && typeof (value as ISmtpRecipientResult).accepted === 'boolean'
276
+ && Number.isInteger((value as ISmtpRecipientResult).responseCode)
277
+ )).map((value) => ({ ...value }))
278
+ : [],
279
+ ...(typeof parsed.recipientDomain === 'string'
280
+ ? { recipientDomain: parsed.recipientDomain }
281
+ : {}),
282
+ }, source);
283
+ } catch {
284
+ return new SmtpDeliveryError({
285
+ message: source.message,
286
+ errorType: 'unknown',
287
+ retryable: true,
288
+ phase: 'unknown',
289
+ transactionLog: [],
290
+ recipients: [],
291
+ }, source);
292
+ }
293
+ }
294
+ }
295
+
296
+ const smtpDeliveryPhases = new Set<TSmtpDeliveryPhase>([
297
+ 'connect',
298
+ 'greeting',
299
+ 'ehlo',
300
+ 'starttls',
301
+ 'tls_handshake',
302
+ 'post_tls_ehlo',
303
+ 'auth',
304
+ 'mail_from',
305
+ 'rcpt_to',
306
+ 'data_command',
307
+ 'message_body',
308
+ 'final_response',
309
+ 'rset',
310
+ 'quit',
311
+ 'unknown',
312
+ ]);
313
+
314
+ const isSmtpDeliveryPhase = (valueArg: unknown): valueArg is TSmtpDeliveryPhase =>
315
+ typeof valueArg === 'string' && smtpDeliveryPhases.has(valueArg as TSmtpDeliveryPhase);
316
+
72
317
  interface IOutboundEmail {
73
318
  from: string;
74
319
  to: string[];
@@ -83,9 +328,11 @@ interface IOutboundEmail {
83
328
  interface ISmtpSendResult {
84
329
  accepted: string[];
85
330
  rejected: string[];
331
+ recipientResults?: ISmtpRecipientResult[];
86
332
  messageId?: string;
87
333
  response: string;
88
334
  envelope: { from: string; to: string[] };
335
+ transactionLog: ISmtpTranscriptEntry[];
89
336
  }
90
337
 
91
338
  interface ISmtpSendOptions {
@@ -159,6 +406,7 @@ interface ISmtpServerConfig {
159
406
  additionalTlsCerts?: Array<{ domains: string[]; certPem: string; keyPem: string }>;
160
407
  maxMessageSize?: number;
161
408
  maxConnections?: number;
409
+ maxConcurrentMessages?: number;
162
410
  maxRecipients?: number;
163
411
  connectionTimeoutSecs?: number;
164
412
  dataTimeoutSecs?: number;
@@ -183,9 +431,8 @@ interface IRateLimitConfig {
183
431
  }
184
432
 
185
433
  interface IEmailData {
186
- type: 'inline' | 'file';
187
- base64?: string;
188
- path?: string;
434
+ type: 'inline';
435
+ base64: string;
189
436
  }
190
437
 
191
438
  interface IEmailReceivedEvent {
@@ -442,7 +689,9 @@ export class RustSecurityBridge extends EventEmitter {
442
689
  this.bridge = new plugins.smartrust.RustBridge<TMailerCommands>({
443
690
  binaryName: 'mailer-bin',
444
691
  cliArgs: ['--management'],
445
- requestTimeoutMs: 30_000,
692
+ // A full SMTP transaction can legitimately span several socket waits.
693
+ // Health checks retain their own short timeout and run concurrently.
694
+ requestTimeoutMs: 300_000,
446
695
  readyTimeoutMs: 10_000,
447
696
  localPaths,
448
697
  searchSystemPath: false,
@@ -877,13 +1126,29 @@ export class RustSecurityBridge extends EventEmitter {
877
1126
  /** Send a structured email via the Rust SMTP client. */
878
1127
  public async sendOutboundEmail(opts: ISmtpSendOptions): Promise<ISmtpSendResult> {
879
1128
  this.ensureRunning();
880
- return this.bridge.sendCommand('sendEmail', opts);
1129
+ try {
1130
+ const result = await this.bridge.sendCommand('sendEmail', opts);
1131
+ return {
1132
+ ...result,
1133
+ transactionLog: normalizeSmtpTranscript(result.transactionLog),
1134
+ };
1135
+ } catch (error) {
1136
+ throw SmtpDeliveryError.from(error);
1137
+ }
881
1138
  }
882
1139
 
883
1140
  /** Send a pre-formatted raw email via the Rust SMTP client. */
884
1141
  public async sendRawEmail(opts: ISmtpSendRawOptions): Promise<ISmtpSendResult> {
885
1142
  this.ensureRunning();
886
- return this.bridge.sendCommand('sendRawEmail', opts);
1143
+ try {
1144
+ const result = await this.bridge.sendCommand('sendRawEmail', opts);
1145
+ return {
1146
+ ...result,
1147
+ transactionLog: normalizeSmtpTranscript(result.transactionLog),
1148
+ };
1149
+ } catch (error) {
1150
+ throw SmtpDeliveryError.from(error);
1151
+ }
887
1152
  }
888
1153
 
889
1154
  /** Verify connectivity to an SMTP server. */
@@ -1065,6 +1330,7 @@ export type {
1065
1330
  IScramCredentialRequestEvent,
1066
1331
  IOutboundEmail,
1067
1332
  ISmtpSendResult,
1333
+ ISmtpRecipientResult,
1068
1334
  ISmtpSendOptions,
1069
1335
  ISmtpSendRawOptions,
1070
1336
  ISmtpVerifyOptions,
@@ -146,12 +146,14 @@ export class SecurityLogger {
146
146
  filteredEvents = filteredEvents.filter(event => event.type === filter.type);
147
147
  }
148
148
 
149
- if (filter.fromTimestamp) {
150
- filteredEvents = filteredEvents.filter(event => event.timestamp >= filter.fromTimestamp);
149
+ const fromTimestamp = filter.fromTimestamp;
150
+ if (fromTimestamp !== undefined) {
151
+ filteredEvents = filteredEvents.filter(event => event.timestamp >= fromTimestamp);
151
152
  }
152
153
 
153
- if (filter.toTimestamp) {
154
- filteredEvents = filteredEvents.filter(event => event.timestamp <= filter.toTimestamp);
154
+ const toTimestamp = filter.toTimestamp;
155
+ if (toTimestamp !== undefined) {
156
+ filteredEvents = filteredEvents.filter(event => event.timestamp <= toTimestamp);
155
157
  }
156
158
  }
157
159
 
@@ -23,7 +23,11 @@ export {
23
23
  export {
24
24
  RustSecurityBridge,
25
25
  BridgeState,
26
+ SmtpDeliveryError,
26
27
  type IBridgeResilienceConfig,
28
+ type TSmtpDeliveryPhase,
29
+ type ISmtpTranscriptEntry,
30
+ type ISmtpDeliveryErrorDetails,
27
31
  type IDkimVerificationResult,
28
32
  type ISpfResult,
29
33
  type IDmarcResult,
@@ -37,4 +41,4 @@ export {
37
41
  type ISmtpSendOptions,
38
42
  type ISmtpVerifyResult,
39
43
  type ISmtpPoolStatus,
40
- } from './classes.rustsecuritybridge.js';
44
+ } from './classes.rustsecuritybridge.js';