@push.rocks/smartmta 7.0.0 → 8.0.1

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 (76) hide show
  1. package/changelog.md +36 -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 +11 -0
  10. package/dist_ts/mail/core/classes.bouncemanager.js +120 -38
  11. package/dist_ts/mail/core/classes.email.js +14 -12
  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/delivery/classes.delivery.queue.d.ts +77 -2
  15. package/dist_ts/mail/delivery/classes.delivery.queue.js +551 -46
  16. package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
  17. package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
  18. package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
  19. package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
  20. package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
  21. package/dist_ts/mail/delivery/index.d.ts +1 -0
  22. package/dist_ts/mail/delivery/index.js +2 -1
  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/index.d.ts +1 -0
  26. package/dist_ts/mail/index.js +1 -1
  27. package/dist_ts/mail/routing/classes.dkim.manager.d.ts +8 -4
  28. package/dist_ts/mail/routing/classes.dkim.manager.js +46 -29
  29. package/dist_ts/mail/routing/classes.dns.manager.d.ts +5 -3
  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 +3 -0
  34. package/dist_ts/mail/routing/classes.email.router.js +15 -9
  35. package/dist_ts/mail/routing/classes.unified.email.server.d.ts +4 -0
  36. package/dist_ts/mail/routing/classes.unified.email.server.js +61 -40
  37. package/dist_ts/mail/security/classes.dkimcreator.d.ts +7 -0
  38. package/dist_ts/mail/security/classes.dkimcreator.js +33 -8
  39. package/dist_ts/mail/security/classes.spfverifier.js +5 -3
  40. package/dist_ts/security/classes.contentscanner.js +14 -11
  41. package/dist_ts/security/classes.ipreputationchecker.d.ts +3 -0
  42. package/dist_ts/security/classes.ipreputationchecker.js +20 -11
  43. package/dist_ts/security/classes.rustsecuritybridge.d.ts +49 -1
  44. package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
  45. package/dist_ts/security/classes.securitylogger.js +7 -5
  46. package/dist_ts/security/index.d.ts +1 -1
  47. package/dist_ts/security/index.js +2 -2
  48. package/package.json +8 -8
  49. package/readme.hints.md +4 -3
  50. package/readme.md +41 -13
  51. package/readme.plan.md +6 -0
  52. package/ts/00_commitinfo_data.ts +1 -1
  53. package/ts/functions.errors.ts +8 -0
  54. package/ts/index.ts +3 -0
  55. package/ts/mail/core/classes.bouncemanager.ts +157 -45
  56. package/ts/mail/core/classes.email.ts +19 -13
  57. package/ts/mail/core/classes.emailvalidator.ts +9 -7
  58. package/ts/mail/delivery/classes.delivery.queue.ts +740 -58
  59. package/ts/mail/delivery/classes.delivery.system.ts +583 -170
  60. package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
  61. package/ts/mail/delivery/functions.safe-observers.ts +45 -0
  62. package/ts/mail/delivery/index.ts +1 -0
  63. package/ts/mail/delivery/interfaces.ts +27 -1
  64. package/ts/mail/index.ts +1 -0
  65. package/ts/mail/routing/classes.dkim.manager.ts +62 -37
  66. package/ts/mail/routing/classes.dns.manager.ts +36 -13
  67. package/ts/mail/routing/classes.email.action.executor.ts +64 -17
  68. package/ts/mail/routing/classes.email.router.ts +15 -8
  69. package/ts/mail/routing/classes.unified.email.server.ts +90 -44
  70. package/ts/mail/security/classes.dkimcreator.ts +50 -7
  71. package/ts/mail/security/classes.spfverifier.ts +4 -2
  72. package/ts/security/classes.contentscanner.ts +14 -11
  73. package/ts/security/classes.ipreputationchecker.ts +21 -10
  74. package/ts/security/classes.rustsecuritybridge.ts +269 -3
  75. package/ts/security/classes.securitylogger.ts +6 -4
  76. package/ts/security/index.ts +6 -1
package/ts/index.ts CHANGED
@@ -5,4 +5,7 @@ export { baseLogger, logger } from './logger.js';
5
5
 
6
6
  export const smartMtaCapabilities = Object.freeze({
7
7
  selectorCorrectDkimSigning: true,
8
+ callerManagedDkimKeys: true,
9
+ structuredSmtpTransactions: true,
10
+ typedDeliveryFailures: true,
8
11
  } as const);
@@ -5,6 +5,8 @@ import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../secur
5
5
  import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
6
6
  import { LRUCache } from 'lru-cache';
7
7
  import type { Email } from './classes.email.js';
8
+ import { callObserverSafely } from '../delivery/functions.safe-observers.js';
9
+ import { getErrorMessage } from '../../functions.errors.js';
8
10
 
9
11
  /**
10
12
  * Bounce types for categorizing the reasons for bounces
@@ -42,11 +44,17 @@ export enum BounceCategory {
42
44
  UNKNOWN = 'unknown'
43
45
  }
44
46
 
47
+ // This cache only accelerates hot duplicate checks. Durable correctness always
48
+ // comes from re-reading the processed marker by deterministic operation ID.
49
+ const COMPLETED_OPERATION_CACHE_MAX = 10_000;
50
+ const COMPLETED_OPERATION_CACHE_TTL = 24 * 60 * 60 * 1000;
51
+
45
52
  /**
46
53
  * Bounce data structure
47
54
  */
48
55
  export interface BounceRecord {
49
56
  id: string;
57
+ operationId?: string;
50
58
  originalEmailId?: string;
51
59
  recipient: string;
52
60
  sender: string;
@@ -108,6 +116,12 @@ export class BounceManager {
108
116
  }> = new Map();
109
117
 
110
118
  private storageManager?: IStorageManager;
119
+ private initializationPromise: Promise<void>;
120
+ private operationPromises = new Map<string, Promise<BounceRecord>>();
121
+ private completedOperations = new LRUCache<string, BounceRecord>({
122
+ max: COMPLETED_OPERATION_CACHE_MAX,
123
+ ttl: COMPLETED_OPERATION_CACHE_TTL,
124
+ });
111
125
 
112
126
  constructor(options?: {
113
127
  retryStrategy?: Partial<RetryStrategy>;
@@ -132,12 +146,11 @@ export class BounceManager {
132
146
  // Store storage manager reference
133
147
  this.storageManager = options?.storageManager;
134
148
 
135
- // Load suppression list from storage
136
- // Note: This is async but we can't await in constructor
137
- // The suppression list will be loaded asynchronously
138
- this.loadSuppressionList().catch(error => {
139
- logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
140
- });
149
+ // Capture hydration so server startup can establish a real readiness barrier.
150
+ // A handled mirror prevents an unhandled rejection before initialize() is awaited.
151
+ const hydrationPromise = this.loadSuppressionList();
152
+ hydrationPromise.catch(() => undefined);
153
+ this.initializationPromise = hydrationPromise;
141
154
 
142
155
  // Start periodic cleanup of old bounce records (every 1 hour, removes records older than 7 days)
143
156
  this.cleanupInterval = setInterval(() => {
@@ -148,6 +161,11 @@ export class BounceManager {
148
161
  }
149
162
  }, 60 * 60 * 1000);
150
163
  }
164
+
165
+ /** Wait until the managed suppression state has been hydrated. */
166
+ public async initialize(): Promise<void> {
167
+ await this.initializationPromise;
168
+ }
151
169
 
152
170
  /**
153
171
  * Process a bounce notification
@@ -156,12 +174,19 @@ export class BounceManager {
156
174
  */
157
175
  public async processBounce(bounceData: Partial<BounceRecord>): Promise<BounceRecord> {
158
176
  try {
177
+ const recipient = bounceData.recipient?.trim();
178
+ if (!recipient) throw new Error('Bounce recipient is required');
179
+ const sender = bounceData.sender || '';
180
+ const domain = bounceData.domain || recipient.split('@')[1];
181
+ if (!domain) throw new Error('Bounce recipient domain is required');
182
+
159
183
  // Add required fields if missing
160
184
  const bounce: BounceRecord = {
161
185
  id: bounceData.id || plugins.uuid.v4(),
162
- recipient: bounceData.recipient,
163
- sender: bounceData.sender,
164
- domain: bounceData.domain || bounceData.recipient.split('@')[1],
186
+ ...(bounceData.operationId ? { operationId: bounceData.operationId } : {}),
187
+ recipient,
188
+ sender,
189
+ domain,
165
190
  subject: bounceData.subject,
166
191
  bounceType: bounceData.bounceType || BounceType.UNKNOWN,
167
192
  bounceCategory: bounceData.bounceCategory || BounceCategory.UNKNOWN,
@@ -214,12 +239,17 @@ export class BounceManager {
214
239
  break;
215
240
  }
216
241
 
217
- // Store the bounce record
242
+ // Persist every side effect before this deterministic record becomes
243
+ // the operation's completion checkpoint.
218
244
  bounce.processed = true;
219
- this.bounceStore.push(bounce);
220
-
221
- // Update the bounce cache
222
- this.updateBounceCache(bounce);
245
+ await this.saveBounceRecord(bounce);
246
+ if (!this.bounceStore.some((stored) => stored.id === bounce.id)) {
247
+ this.bounceStore.push(bounce);
248
+ this.updateBounceCache(bounce);
249
+ }
250
+ if (bounce.operationId) {
251
+ this.completedOperations.set(bounce.operationId, bounce);
252
+ }
223
253
 
224
254
  // Log the bounce
225
255
  logger.log(
@@ -233,7 +263,7 @@ export class BounceManager {
233
263
  );
234
264
 
235
265
  // Enhanced security logging
236
- SecurityLogger.getInstance().logEvent({
266
+ await callObserverSafely('bounce security log', () => SecurityLogger.getInstance().logEvent({
237
267
  level: bounce.bounceCategory === BounceCategory.HARD
238
268
  ? SecurityLogLevel.WARN
239
269
  : SecurityLogLevel.INFO,
@@ -248,12 +278,13 @@ export class BounceManager {
248
278
  statusCode: bounce.statusCode
249
279
  },
250
280
  success: false
251
- });
252
-
281
+ }));
282
+
253
283
  return bounce;
254
284
  } catch (error) {
255
- logger.log('error', `Error processing bounce: ${error.message}`, {
256
- error: error.message,
285
+ const errorMessage = getErrorMessage(error);
286
+ logger.log('error', `Error processing bounce: ${errorMessage}`, {
287
+ error: errorMessage,
257
288
  bounceData
258
289
  });
259
290
  throw error;
@@ -271,14 +302,66 @@ export class BounceManager {
271
302
  recipient: string,
272
303
  smtpResponse: string,
273
304
  options: {
305
+ operationId?: string;
274
306
  sender?: string;
275
307
  originalEmailId?: string;
276
308
  statusCode?: string;
277
309
  headers?: Record<string, string>;
278
310
  } = {}
279
311
  ): Promise<BounceRecord> {
280
- // Create bounce data from SMTP failure
281
- const bounceData: Partial<BounceRecord> = {
312
+ const operationId = options.operationId;
313
+ if (!operationId) {
314
+ return this.processBounce({
315
+ recipient,
316
+ sender: options.sender || '',
317
+ domain: recipient.split('@')[1],
318
+ smtpResponse,
319
+ statusCode: options.statusCode,
320
+ headers: options.headers,
321
+ originalEmailId: options.originalEmailId,
322
+ timestamp: Date.now(),
323
+ });
324
+ }
325
+
326
+ const completed = this.completedOperations.get(operationId);
327
+ if (completed) return completed;
328
+ const active = this.operationPromises.get(operationId);
329
+ if (active) return active;
330
+
331
+ const operation = this.processSmtpFailureOperation(
332
+ operationId,
333
+ recipient,
334
+ smtpResponse,
335
+ options,
336
+ );
337
+ this.operationPromises.set(operationId, operation);
338
+ try {
339
+ return await operation;
340
+ } finally {
341
+ this.operationPromises.delete(operationId);
342
+ }
343
+ }
344
+
345
+ private async processSmtpFailureOperation(
346
+ operationId: string,
347
+ recipient: string,
348
+ smtpResponse: string,
349
+ options: {
350
+ sender?: string;
351
+ originalEmailId?: string;
352
+ statusCode?: string;
353
+ headers?: Record<string, string>;
354
+ },
355
+ ): Promise<BounceRecord> {
356
+ const stored = await this.readProcessedBounce(operationId);
357
+ if (stored) {
358
+ this.completedOperations.set(operationId, stored);
359
+ return stored;
360
+ }
361
+
362
+ return this.processBounce({
363
+ id: operationId,
364
+ operationId,
282
365
  recipient,
283
366
  sender: options.sender || '',
284
367
  domain: recipient.split('@')[1],
@@ -286,13 +369,23 @@ export class BounceManager {
286
369
  statusCode: options.statusCode,
287
370
  headers: options.headers,
288
371
  originalEmailId: options.originalEmailId,
289
- timestamp: Date.now()
290
- };
291
-
292
- // Process as a regular bounce
293
- return this.processBounce(bounceData);
372
+ timestamp: Date.now(),
373
+ });
294
374
  }
295
-
375
+
376
+ private async readProcessedBounce(operationId: string): Promise<BounceRecord | null> {
377
+ if (!this.storageManager) return null;
378
+ const serialized = await this.storageManager.get(
379
+ `/email/bounces/records/${operationId}.json`,
380
+ );
381
+ if (!serialized) return null;
382
+ const bounce = JSON.parse(serialized) as BounceRecord;
383
+ if (!bounce.processed || bounce.operationId !== operationId || bounce.id !== operationId) {
384
+ throw new Error(`Invalid processed bounce checkpoint for operation ${operationId}`);
385
+ }
386
+ return bounce;
387
+ }
388
+
296
389
  /**
297
390
  * Process a bounce notification email
298
391
  * @param bounceEmail The email containing bounce information
@@ -379,7 +472,7 @@ export class BounceManager {
379
472
  // Process as a regular bounce
380
473
  return this.processBounce(bounceData);
381
474
  } catch (error) {
382
- logger.log('error', `Error processing bounce email: ${error.message}`);
475
+ logger.log('error', `Error processing bounce email: ${getErrorMessage(error)}`);
383
476
  return null;
384
477
  }
385
478
  }
@@ -389,14 +482,11 @@ export class BounceManager {
389
482
  * @param bounce The bounce record
390
483
  */
391
484
  private async handleHardBounce(bounce: BounceRecord): Promise<void> {
392
- // Add to suppression list permanently (no expiry)
393
- this.addToSuppressionList(bounce.recipient, `Hard bounce: ${bounce.bounceType}`, undefined);
394
-
395
- // Increment bounce count in cache
396
- this.updateBounceCache(bounce);
397
-
398
- // Save to permanent storage
399
- await this.saveBounceRecord(bounce);
485
+ await this.addToSuppressionListAndPersist(
486
+ bounce.recipient,
487
+ `Hard bounce: ${bounce.bounceType}`,
488
+ undefined,
489
+ );
400
490
 
401
491
  // Log hard bounce for monitoring
402
492
  logger.log('warn', `Hard bounce for ${bounce.recipient}: ${bounce.bounceType}`, {
@@ -411,8 +501,9 @@ export class BounceManager {
411
501
  * @param bounce The bounce record
412
502
  */
413
503
  private async handleSoftBounce(bounce: BounceRecord): Promise<void> {
504
+ const retryCount = bounce.retryCount ?? 0;
414
505
  // Check if we've exceeded max retries
415
- if (bounce.retryCount >= this.retryStrategy.maxRetries) {
506
+ if (retryCount >= this.retryStrategy.maxRetries) {
416
507
  logger.log('warn', `Max retries exceeded for ${bounce.recipient}, treating as hard bounce`);
417
508
 
418
509
  // Convert to hard bounce after max retries
@@ -423,18 +514,17 @@ export class BounceManager {
423
514
 
424
515
  // Calculate next retry time with exponential backoff
425
516
  const delay = Math.min(
426
- this.retryStrategy.initialDelay * Math.pow(this.retryStrategy.backoffFactor, bounce.retryCount),
517
+ this.retryStrategy.initialDelay * Math.pow(this.retryStrategy.backoffFactor, retryCount),
427
518
  this.retryStrategy.maxDelay
428
519
  );
429
520
 
430
- bounce.retryCount++;
521
+ bounce.retryCount = retryCount + 1;
431
522
  bounce.nextRetryTime = Date.now() + delay;
432
523
 
433
- // Add to suppression list temporarily (with expiry)
434
- this.addToSuppressionList(
524
+ await this.addToSuppressionListAndPersist(
435
525
  bounce.recipient,
436
526
  `Soft bounce: ${bounce.bounceType}`,
437
- bounce.nextRetryTime
527
+ bounce.nextRetryTime,
438
528
  );
439
529
 
440
530
  // Log the retry schedule
@@ -451,16 +541,37 @@ export class BounceManager {
451
541
  * @param reason Reason for suppression
452
542
  * @param expiresAt Expiration timestamp (undefined for permanent)
453
543
  */
454
- public addToSuppressionList(
544
+ private setSuppressionListEntry(
455
545
  email: string,
456
546
  reason: string,
457
- expiresAt?: number
547
+ expiresAt?: number,
458
548
  ): void {
459
549
  this.suppressionList.set(email.toLowerCase(), {
460
550
  reason,
461
551
  timestamp: Date.now(),
462
- expiresAt
552
+ expiresAt,
553
+ });
554
+ }
555
+
556
+ private async addToSuppressionListAndPersist(
557
+ email: string,
558
+ reason: string,
559
+ expiresAt?: number,
560
+ ): Promise<void> {
561
+ this.setSuppressionListEntry(email, reason, expiresAt);
562
+ await this.saveSuppressionList();
563
+ logger.log('info', `Added ${email} to suppression list`, {
564
+ reason,
565
+ expiresAt: expiresAt ? new Date(expiresAt).toISOString() : 'permanent',
463
566
  });
567
+ }
568
+
569
+ public addToSuppressionList(
570
+ email: string,
571
+ reason: string,
572
+ expiresAt?: number
573
+ ): void {
574
+ this.setSuppressionListEntry(email, reason, expiresAt);
464
575
 
465
576
  // Save asynchronously without blocking
466
577
  this.saveSuppressionList().catch(error => {
@@ -671,5 +782,6 @@ export class BounceManager {
671
782
  clearInterval(this.cleanupInterval);
672
783
  this.cleanupInterval = undefined;
673
784
  }
785
+ this.completedOperations.clear();
674
786
  }
675
787
  }
@@ -1,5 +1,5 @@
1
1
  import * as plugins from '../../plugins.js';
2
- import { EmailValidator } from './classes.emailvalidator.js';
2
+ import { EmailValidator, type IEmailValidationResult } from './classes.emailvalidator.js';
3
3
 
4
4
  export interface IAttachment {
5
5
  filename: string;
@@ -512,20 +512,25 @@ export class Email {
512
512
  recipients: Array<{ email: string; result: any }>;
513
513
  isValid: boolean;
514
514
  }> {
515
- const result = {
515
+ const result: {
516
+ sender: { email: string; result: IEmailValidationResult | null };
517
+ recipients: Array<{ email: string; result: IEmailValidationResult }>;
518
+ isValid: boolean;
519
+ } = {
516
520
  sender: { email: this.from, result: null },
517
521
  recipients: [],
518
522
  isValid: true
519
523
  };
520
524
 
521
525
  // Validate sender
522
- result.sender.result = await Email.emailValidator.validate(this.from, {
526
+ const senderResult = await Email.emailValidator.validate(this.from, {
523
527
  checkMx: options.checkMx !== false,
524
528
  checkDisposable: options.checkDisposable !== false
525
529
  });
530
+ result.sender.result = senderResult;
526
531
 
527
532
  // If sender fails validation, the whole email is considered invalid
528
- if (!result.sender.result.isValid) {
533
+ if (!senderResult.isValid) {
529
534
  result.isValid = false;
530
535
  }
531
536
 
@@ -900,19 +905,20 @@ export class Email {
900
905
  return emails.filter(email => email && email.length > 0);
901
906
  };
902
907
 
903
- // Convert TO recipients
904
- if (smartmail.options.to?.length > 0) {
905
- options.to = filterValidEmails(smartmail.options.to.map(extractEmail));
908
+ // Convert recipients from Smartmail's optional arrays.
909
+ const toRecipients = smartmail.options.to;
910
+ if (toRecipients && toRecipients.length > 0) {
911
+ options.to = filterValidEmails(toRecipients.map(extractEmail));
906
912
  }
907
913
 
908
- // Convert CC recipients
909
- if (smartmail.options.cc?.length > 0) {
910
- options.cc = filterValidEmails(smartmail.options.cc.map(extractEmail));
914
+ const ccRecipients = smartmail.options.cc;
915
+ if (ccRecipients && ccRecipients.length > 0) {
916
+ options.cc = filterValidEmails(ccRecipients.map(extractEmail));
911
917
  }
912
918
 
913
- // Convert BCC recipients
914
- if (smartmail.options.bcc?.length > 0) {
915
- options.bcc = filterValidEmails(smartmail.options.bcc.map(extractEmail));
919
+ const bccRecipients = smartmail.options.bcc;
920
+ if (bccRecipients && bccRecipients.length > 0) {
921
+ options.bcc = filterValidEmails(bccRecipients.map(extractEmail));
916
922
  }
917
923
 
918
924
  // Convert attachments (note: this handles the synchronous case only)
@@ -1,18 +1,19 @@
1
1
  import * as plugins from '../../plugins.js';
2
2
  import { logger } from '../../logger.js';
3
3
  import { LRUCache } from 'lru-cache';
4
+ import { getErrorMessage } from '../../functions.errors.js';
4
5
 
5
6
  export interface IEmailValidationResult {
6
7
  isValid: boolean;
7
8
  hasMx: boolean;
8
9
  hasSpamMarkings: boolean;
9
10
  score: number;
10
- details?: {
11
- formatValid?: boolean;
11
+ details: {
12
+ formatValid: boolean;
12
13
  mxRecords?: string[];
13
14
  disposable?: boolean;
14
15
  role?: boolean;
15
- spamIndicators?: string[];
16
+ spamIndicators: string[];
16
17
  errorMessage?: string;
17
18
  };
18
19
  }
@@ -102,7 +103,7 @@ export class EmailValidator {
102
103
  result.details.errorMessage = 'Domain has no MX records';
103
104
  }
104
105
  } catch (error) {
105
- logger.log('error', `Error checking MX records: ${error.message}`);
106
+ logger.log('error', `Error checking MX records: ${getErrorMessage(error)}`);
106
107
  result.details.errorMessage = 'Unable to check MX records';
107
108
  }
108
109
  }
@@ -160,7 +161,8 @@ export class EmailValidator {
160
161
 
161
162
  return result;
162
163
  } catch (error) {
163
- logger.log('error', `Email validation error: ${error.message}`);
164
+ const errorMessage = getErrorMessage(error);
165
+ logger.log('error', `Email validation error: ${errorMessage}`);
164
166
  return {
165
167
  isValid: false,
166
168
  hasMx: false,
@@ -168,7 +170,7 @@ export class EmailValidator {
168
170
  score: 0,
169
171
  details: {
170
172
  formatValid: false,
171
- errorMessage: `Validation error: ${error.message}`,
173
+ errorMessage: `Validation error: ${errorMessage}`,
172
174
  spamIndicators: ['Validation error']
173
175
  }
174
176
  };
@@ -198,7 +200,7 @@ export class EmailValidator {
198
200
 
199
201
  return records;
200
202
  } catch (error) {
201
- logger.log('error', `Error fetching MX records for ${domain}: ${error.message}`);
203
+ logger.log('error', `Error fetching MX records for ${domain}: ${getErrorMessage(error)}`);
202
204
  return [];
203
205
  }
204
206
  }