@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.
- package/changelog.md +36 -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/functions.errors.d.ts +3 -0
- package/dist_ts/functions.errors.js +8 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -1
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +11 -0
- package/dist_ts/mail/core/classes.bouncemanager.js +120 -38
- package/dist_ts/mail/core/classes.email.js +14 -12
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +3 -3
- package/dist_ts/mail/core/classes.emailvalidator.js +7 -5
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +77 -2
- package/dist_ts/mail/delivery/classes.delivery.queue.js +551 -46
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +13 -7
- package/dist_ts/mail/delivery/classes.delivery.system.js +458 -145
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +9 -8
- package/dist_ts/mail/delivery/functions.safe-observers.d.ts +10 -0
- package/dist_ts/mail/delivery/functions.safe-observers.js +37 -0
- package/dist_ts/mail/delivery/index.d.ts +1 -0
- package/dist_ts/mail/delivery/index.js +2 -1
- package/dist_ts/mail/delivery/interfaces.d.ts +21 -0
- package/dist_ts/mail/delivery/interfaces.js +1 -1
- package/dist_ts/mail/index.d.ts +1 -0
- package/dist_ts/mail/index.js +1 -1
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +8 -4
- package/dist_ts/mail/routing/classes.dkim.manager.js +46 -29
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +5 -3
- package/dist_ts/mail/routing/classes.dns.manager.js +22 -11
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +2 -1
- package/dist_ts/mail/routing/classes.email.action.executor.js +45 -16
- package/dist_ts/mail/routing/classes.email.router.d.ts +3 -0
- package/dist_ts/mail/routing/classes.email.router.js +15 -9
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +4 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +61 -40
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +7 -0
- package/dist_ts/mail/security/classes.dkimcreator.js +33 -8
- package/dist_ts/mail/security/classes.spfverifier.js +5 -3
- package/dist_ts/security/classes.contentscanner.js +14 -11
- package/dist_ts/security/classes.ipreputationchecker.d.ts +3 -0
- package/dist_ts/security/classes.ipreputationchecker.js +20 -11
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +49 -1
- package/dist_ts/security/classes.rustsecuritybridge.js +201 -4
- package/dist_ts/security/classes.securitylogger.js +7 -5
- package/dist_ts/security/index.d.ts +1 -1
- package/dist_ts/security/index.js +2 -2
- package/package.json +8 -8
- package/readme.hints.md +4 -3
- package/readme.md +41 -13
- package/readme.plan.md +6 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/functions.errors.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/mail/core/classes.bouncemanager.ts +157 -45
- package/ts/mail/core/classes.email.ts +19 -13
- package/ts/mail/core/classes.emailvalidator.ts +9 -7
- package/ts/mail/delivery/classes.delivery.queue.ts +740 -58
- package/ts/mail/delivery/classes.delivery.system.ts +583 -170
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +9 -8
- package/ts/mail/delivery/functions.safe-observers.ts +45 -0
- package/ts/mail/delivery/index.ts +1 -0
- package/ts/mail/delivery/interfaces.ts +27 -1
- package/ts/mail/index.ts +1 -0
- package/ts/mail/routing/classes.dkim.manager.ts +62 -37
- package/ts/mail/routing/classes.dns.manager.ts +36 -13
- package/ts/mail/routing/classes.email.action.executor.ts +64 -17
- package/ts/mail/routing/classes.email.router.ts +15 -8
- package/ts/mail/routing/classes.unified.email.server.ts +90 -44
- package/ts/mail/security/classes.dkimcreator.ts +50 -7
- package/ts/mail/security/classes.spfverifier.ts +4 -2
- package/ts/security/classes.contentscanner.ts +14 -11
- package/ts/security/classes.ipreputationchecker.ts +21 -10
- package/ts/security/classes.rustsecuritybridge.ts +269 -3
- package/ts/security/classes.securitylogger.ts +6 -4
- package/ts/security/index.ts +6 -1
package/ts/index.ts
CHANGED
|
@@ -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
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
//
|
|
242
|
+
// Persist every side effect before this deterministic record becomes
|
|
243
|
+
// the operation's completion checkpoint.
|
|
218
244
|
bounce.processed = true;
|
|
219
|
-
this.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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 (
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
|
904
|
-
|
|
905
|
-
|
|
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
|
-
|
|
909
|
-
if (
|
|
910
|
-
options.cc = filterValidEmails(
|
|
914
|
+
const ccRecipients = smartmail.options.cc;
|
|
915
|
+
if (ccRecipients && ccRecipients.length > 0) {
|
|
916
|
+
options.cc = filterValidEmails(ccRecipients.map(extractEmail));
|
|
911
917
|
}
|
|
912
918
|
|
|
913
|
-
|
|
914
|
-
if (
|
|
915
|
-
options.bcc = filterValidEmails(
|
|
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
|
|
11
|
+
details: {
|
|
12
|
+
formatValid: boolean;
|
|
12
13
|
mxRecords?: string[];
|
|
13
14
|
disposable?: boolean;
|
|
14
15
|
role?: boolean;
|
|
15
|
-
spamIndicators
|
|
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
|
|
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
|
-
|
|
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: ${
|
|
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
|
|
203
|
+
logger.log('error', `Error fetching MX records for ${domain}: ${getErrorMessage(error)}`);
|
|
202
204
|
return [];
|
|
203
205
|
}
|
|
204
206
|
}
|