@push.rocks/smartmta 5.1.3 → 5.2.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 +7 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/logger.d.ts +17 -0
- package/dist_ts/logger.js +76 -0
- package/dist_ts/mail/core/classes.bouncemanager.d.ts +185 -0
- package/dist_ts/mail/core/classes.bouncemanager.js +569 -0
- package/dist_ts/mail/core/classes.email.d.ts +291 -0
- package/dist_ts/mail/core/classes.email.js +802 -0
- package/dist_ts/mail/core/classes.emailvalidator.d.ts +61 -0
- package/dist_ts/mail/core/classes.emailvalidator.js +184 -0
- package/dist_ts/mail/core/classes.templatemanager.d.ts +95 -0
- package/dist_ts/mail/core/classes.templatemanager.js +240 -0
- package/dist_ts/mail/core/index.d.ts +4 -0
- package/dist_ts/mail/core/index.js +6 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.d.ts +163 -0
- package/dist_ts/mail/delivery/classes.delivery.queue.js +488 -0
- package/dist_ts/mail/delivery/classes.delivery.system.d.ts +160 -0
- package/dist_ts/mail/delivery/classes.delivery.system.js +630 -0
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.d.ts +200 -0
- package/dist_ts/mail/delivery/classes.unified.rate.limiter.js +820 -0
- package/dist_ts/mail/delivery/index.d.ts +4 -0
- package/dist_ts/mail/delivery/index.js +6 -0
- package/dist_ts/mail/delivery/interfaces.d.ts +140 -0
- package/dist_ts/mail/delivery/interfaces.js +17 -0
- package/dist_ts/mail/index.d.ts +7 -0
- package/dist_ts/mail/index.js +12 -0
- package/dist_ts/mail/routing/classes.dkim.manager.d.ts +25 -0
- package/dist_ts/mail/routing/classes.dkim.manager.js +127 -0
- package/dist_ts/mail/routing/classes.dns.manager.d.ts +79 -0
- package/dist_ts/mail/routing/classes.dns.manager.js +415 -0
- package/dist_ts/mail/routing/classes.domain.registry.d.ts +54 -0
- package/dist_ts/mail/routing/classes.domain.registry.js +119 -0
- package/dist_ts/mail/routing/classes.email.action.executor.d.ts +33 -0
- package/dist_ts/mail/routing/classes.email.action.executor.js +137 -0
- package/dist_ts/mail/routing/classes.email.router.d.ts +171 -0
- package/dist_ts/mail/routing/classes.email.router.js +494 -0
- package/dist_ts/mail/routing/classes.unified.email.server.d.ts +241 -0
- package/dist_ts/mail/routing/classes.unified.email.server.js +935 -0
- package/dist_ts/mail/routing/index.d.ts +7 -0
- package/dist_ts/mail/routing/index.js +9 -0
- package/dist_ts/mail/routing/interfaces.d.ts +187 -0
- package/dist_ts/mail/routing/interfaces.js +2 -0
- package/dist_ts/mail/security/classes.dkimcreator.d.ts +72 -0
- package/dist_ts/mail/security/classes.dkimcreator.js +360 -0
- package/dist_ts/mail/security/classes.spfverifier.d.ts +62 -0
- package/dist_ts/mail/security/classes.spfverifier.js +87 -0
- package/dist_ts/mail/security/index.d.ts +2 -0
- package/dist_ts/mail/security/index.js +4 -0
- package/dist_ts/paths.d.ts +14 -0
- package/dist_ts/paths.js +39 -0
- package/dist_ts/plugins.d.ts +24 -0
- package/dist_ts/plugins.js +28 -0
- package/dist_ts/security/classes.contentscanner.d.ts +130 -0
- package/dist_ts/security/classes.contentscanner.js +338 -0
- package/dist_ts/security/classes.ipreputationchecker.d.ts +73 -0
- package/dist_ts/security/classes.ipreputationchecker.js +263 -0
- package/dist_ts/security/classes.rustsecuritybridge.d.ts +398 -0
- package/dist_ts/security/classes.rustsecuritybridge.js +484 -0
- package/dist_ts/security/classes.securitylogger.d.ts +140 -0
- package/dist_ts/security/classes.securitylogger.js +235 -0
- package/dist_ts/security/index.d.ts +4 -0
- package/dist_ts/security/index.js +5 -0
- package/package.json +6 -1
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/index.ts +3 -0
- package/ts/logger.ts +91 -0
- package/ts/mail/core/classes.bouncemanager.ts +731 -0
- package/ts/mail/core/classes.email.ts +942 -0
- package/ts/mail/core/classes.emailvalidator.ts +239 -0
- package/ts/mail/core/classes.templatemanager.ts +320 -0
- package/ts/mail/core/index.ts +5 -0
- package/ts/mail/delivery/classes.delivery.queue.ts +645 -0
- package/ts/mail/delivery/classes.delivery.system.ts +816 -0
- package/ts/mail/delivery/classes.unified.rate.limiter.ts +1053 -0
- package/ts/mail/delivery/index.ts +5 -0
- package/ts/mail/delivery/interfaces.ts +167 -0
- package/ts/mail/index.ts +17 -0
- package/ts/mail/routing/classes.dkim.manager.ts +157 -0
- package/ts/mail/routing/classes.dns.manager.ts +573 -0
- package/ts/mail/routing/classes.domain.registry.ts +139 -0
- package/ts/mail/routing/classes.email.action.executor.ts +175 -0
- package/ts/mail/routing/classes.email.router.ts +575 -0
- package/ts/mail/routing/classes.unified.email.server.ts +1207 -0
- package/ts/mail/routing/index.ts +9 -0
- package/ts/mail/routing/interfaces.ts +202 -0
- package/ts/mail/security/classes.dkimcreator.ts +447 -0
- package/ts/mail/security/classes.spfverifier.ts +126 -0
- package/ts/mail/security/index.ts +3 -0
- package/ts/paths.ts +48 -0
- package/ts/plugins.ts +53 -0
- package/ts/security/classes.contentscanner.ts +400 -0
- package/ts/security/classes.ipreputationchecker.ts +315 -0
- package/ts/security/classes.rustsecuritybridge.ts +943 -0
- package/ts/security/classes.securitylogger.ts +299 -0
- package/ts/security/index.ts +40 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as paths from '../../paths.js';
|
|
3
|
+
import { logger } from '../../logger.js';
|
|
4
|
+
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
|
|
5
|
+
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
6
|
+
import { LRUCache } from 'lru-cache';
|
|
7
|
+
/**
|
|
8
|
+
* Bounce types for categorizing the reasons for bounces
|
|
9
|
+
*/
|
|
10
|
+
export var BounceType;
|
|
11
|
+
(function (BounceType) {
|
|
12
|
+
// Hard bounces (permanent failures)
|
|
13
|
+
BounceType["INVALID_RECIPIENT"] = "invalid_recipient";
|
|
14
|
+
BounceType["DOMAIN_NOT_FOUND"] = "domain_not_found";
|
|
15
|
+
BounceType["MAILBOX_FULL"] = "mailbox_full";
|
|
16
|
+
BounceType["MAILBOX_INACTIVE"] = "mailbox_inactive";
|
|
17
|
+
BounceType["BLOCKED"] = "blocked";
|
|
18
|
+
BounceType["SPAM_RELATED"] = "spam_related";
|
|
19
|
+
BounceType["POLICY_RELATED"] = "policy_related";
|
|
20
|
+
// Soft bounces (temporary failures)
|
|
21
|
+
BounceType["SERVER_UNAVAILABLE"] = "server_unavailable";
|
|
22
|
+
BounceType["TEMPORARY_FAILURE"] = "temporary_failure";
|
|
23
|
+
BounceType["QUOTA_EXCEEDED"] = "quota_exceeded";
|
|
24
|
+
BounceType["NETWORK_ERROR"] = "network_error";
|
|
25
|
+
BounceType["TIMEOUT"] = "timeout";
|
|
26
|
+
// Special cases
|
|
27
|
+
BounceType["AUTO_RESPONSE"] = "auto_response";
|
|
28
|
+
BounceType["CHALLENGE_RESPONSE"] = "challenge_response";
|
|
29
|
+
BounceType["UNKNOWN"] = "unknown";
|
|
30
|
+
})(BounceType || (BounceType = {}));
|
|
31
|
+
/**
|
|
32
|
+
* Hard vs soft bounce classification
|
|
33
|
+
*/
|
|
34
|
+
export var BounceCategory;
|
|
35
|
+
(function (BounceCategory) {
|
|
36
|
+
BounceCategory["HARD"] = "hard";
|
|
37
|
+
BounceCategory["SOFT"] = "soft";
|
|
38
|
+
BounceCategory["AUTO_RESPONSE"] = "auto_response";
|
|
39
|
+
BounceCategory["UNKNOWN"] = "unknown";
|
|
40
|
+
})(BounceCategory || (BounceCategory = {}));
|
|
41
|
+
/**
|
|
42
|
+
* Manager for handling email bounces
|
|
43
|
+
*/
|
|
44
|
+
export class BounceManager {
|
|
45
|
+
// Retry strategy with exponential backoff
|
|
46
|
+
retryStrategy = {
|
|
47
|
+
maxRetries: 5,
|
|
48
|
+
initialDelay: 15 * 60 * 1000, // 15 minutes
|
|
49
|
+
maxDelay: 24 * 60 * 60 * 1000, // 24 hours
|
|
50
|
+
backoffFactor: 2
|
|
51
|
+
};
|
|
52
|
+
// Store of bounced emails
|
|
53
|
+
bounceStore = [];
|
|
54
|
+
// Cache of recently bounced email addresses to avoid sending to known bad addresses
|
|
55
|
+
bounceCache;
|
|
56
|
+
// Suppression list for addresses that should not receive emails
|
|
57
|
+
suppressionList = new Map();
|
|
58
|
+
storageManager; // StorageManager instance
|
|
59
|
+
constructor(options) {
|
|
60
|
+
// Set retry strategy with defaults
|
|
61
|
+
if (options?.retryStrategy) {
|
|
62
|
+
this.retryStrategy = {
|
|
63
|
+
...this.retryStrategy,
|
|
64
|
+
...options.retryStrategy
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Initialize bounce cache with LRU (least recently used) caching
|
|
68
|
+
this.bounceCache = new LRUCache({
|
|
69
|
+
max: options?.maxCacheSize || 10000,
|
|
70
|
+
ttl: options?.cacheTTL || 30 * 24 * 60 * 60 * 1000, // 30 days default
|
|
71
|
+
});
|
|
72
|
+
// Store storage manager reference
|
|
73
|
+
this.storageManager = options?.storageManager;
|
|
74
|
+
// Load suppression list from storage
|
|
75
|
+
// Note: This is async but we can't await in constructor
|
|
76
|
+
// The suppression list will be loaded asynchronously
|
|
77
|
+
this.loadSuppressionList().catch(error => {
|
|
78
|
+
logger.log('error', `Failed to load suppression list on startup: ${error.message}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Process a bounce notification
|
|
83
|
+
* @param bounceData Bounce data to process
|
|
84
|
+
* @returns Processed bounce record
|
|
85
|
+
*/
|
|
86
|
+
async processBounce(bounceData) {
|
|
87
|
+
try {
|
|
88
|
+
// Add required fields if missing
|
|
89
|
+
const bounce = {
|
|
90
|
+
id: bounceData.id || plugins.uuid.v4(),
|
|
91
|
+
recipient: bounceData.recipient,
|
|
92
|
+
sender: bounceData.sender,
|
|
93
|
+
domain: bounceData.domain || bounceData.recipient.split('@')[1],
|
|
94
|
+
subject: bounceData.subject,
|
|
95
|
+
bounceType: bounceData.bounceType || BounceType.UNKNOWN,
|
|
96
|
+
bounceCategory: bounceData.bounceCategory || BounceCategory.UNKNOWN,
|
|
97
|
+
timestamp: bounceData.timestamp || Date.now(),
|
|
98
|
+
smtpResponse: bounceData.smtpResponse,
|
|
99
|
+
diagnosticCode: bounceData.diagnosticCode,
|
|
100
|
+
statusCode: bounceData.statusCode,
|
|
101
|
+
headers: bounceData.headers,
|
|
102
|
+
processed: false,
|
|
103
|
+
originalEmailId: bounceData.originalEmailId,
|
|
104
|
+
retryCount: bounceData.retryCount || 0,
|
|
105
|
+
nextRetryTime: bounceData.nextRetryTime
|
|
106
|
+
};
|
|
107
|
+
// Determine bounce type and category via Rust bridge if not provided
|
|
108
|
+
if (!bounceData.bounceType || bounceData.bounceType === BounceType.UNKNOWN) {
|
|
109
|
+
const bridge = RustSecurityBridge.getInstance();
|
|
110
|
+
const rustResult = await bridge.detectBounce({
|
|
111
|
+
smtpResponse: bounce.smtpResponse,
|
|
112
|
+
diagnosticCode: bounce.diagnosticCode,
|
|
113
|
+
statusCode: bounce.statusCode,
|
|
114
|
+
});
|
|
115
|
+
bounce.bounceType = rustResult.bounce_type;
|
|
116
|
+
bounce.bounceCategory = rustResult.category;
|
|
117
|
+
}
|
|
118
|
+
// Process the bounce based on category
|
|
119
|
+
switch (bounce.bounceCategory) {
|
|
120
|
+
case BounceCategory.HARD:
|
|
121
|
+
// Handle hard bounce - add to suppression list
|
|
122
|
+
await this.handleHardBounce(bounce);
|
|
123
|
+
break;
|
|
124
|
+
case BounceCategory.SOFT:
|
|
125
|
+
// Handle soft bounce - schedule retry if eligible
|
|
126
|
+
await this.handleSoftBounce(bounce);
|
|
127
|
+
break;
|
|
128
|
+
case BounceCategory.AUTO_RESPONSE:
|
|
129
|
+
// Handle auto-response - typically no action needed
|
|
130
|
+
logger.log('info', `Auto-response detected for ${bounce.recipient}`);
|
|
131
|
+
break;
|
|
132
|
+
default:
|
|
133
|
+
// Unknown bounce type - log for investigation
|
|
134
|
+
logger.log('warn', `Unknown bounce type for ${bounce.recipient}`, {
|
|
135
|
+
bounceType: bounce.bounceType,
|
|
136
|
+
smtpResponse: bounce.smtpResponse
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
// Store the bounce record
|
|
141
|
+
bounce.processed = true;
|
|
142
|
+
this.bounceStore.push(bounce);
|
|
143
|
+
// Update the bounce cache
|
|
144
|
+
this.updateBounceCache(bounce);
|
|
145
|
+
// Log the bounce
|
|
146
|
+
logger.log(bounce.bounceCategory === BounceCategory.HARD ? 'warn' : 'info', `Email bounce processed: ${bounce.bounceCategory} bounce for ${bounce.recipient}`, {
|
|
147
|
+
bounceType: bounce.bounceType,
|
|
148
|
+
domain: bounce.domain,
|
|
149
|
+
category: bounce.bounceCategory
|
|
150
|
+
});
|
|
151
|
+
// Enhanced security logging
|
|
152
|
+
SecurityLogger.getInstance().logEvent({
|
|
153
|
+
level: bounce.bounceCategory === BounceCategory.HARD
|
|
154
|
+
? SecurityLogLevel.WARN
|
|
155
|
+
: SecurityLogLevel.INFO,
|
|
156
|
+
type: SecurityEventType.EMAIL_VALIDATION,
|
|
157
|
+
message: `Email bounce detected: ${bounce.bounceCategory} bounce for recipient`,
|
|
158
|
+
domain: bounce.domain,
|
|
159
|
+
details: {
|
|
160
|
+
recipient: bounce.recipient,
|
|
161
|
+
bounceType: bounce.bounceType,
|
|
162
|
+
smtpResponse: bounce.smtpResponse,
|
|
163
|
+
diagnosticCode: bounce.diagnosticCode,
|
|
164
|
+
statusCode: bounce.statusCode
|
|
165
|
+
},
|
|
166
|
+
success: false
|
|
167
|
+
});
|
|
168
|
+
return bounce;
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
logger.log('error', `Error processing bounce: ${error.message}`, {
|
|
172
|
+
error: error.message,
|
|
173
|
+
bounceData
|
|
174
|
+
});
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Process an SMTP failure as a bounce
|
|
180
|
+
* @param recipient Recipient email
|
|
181
|
+
* @param smtpResponse SMTP error response
|
|
182
|
+
* @param options Additional options
|
|
183
|
+
* @returns Processed bounce record
|
|
184
|
+
*/
|
|
185
|
+
async processSmtpFailure(recipient, smtpResponse, options = {}) {
|
|
186
|
+
// Create bounce data from SMTP failure
|
|
187
|
+
const bounceData = {
|
|
188
|
+
recipient,
|
|
189
|
+
sender: options.sender || '',
|
|
190
|
+
domain: recipient.split('@')[1],
|
|
191
|
+
smtpResponse,
|
|
192
|
+
statusCode: options.statusCode,
|
|
193
|
+
headers: options.headers,
|
|
194
|
+
originalEmailId: options.originalEmailId,
|
|
195
|
+
timestamp: Date.now()
|
|
196
|
+
};
|
|
197
|
+
// Process as a regular bounce
|
|
198
|
+
return this.processBounce(bounceData);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Process a bounce notification email
|
|
202
|
+
* @param bounceEmail The email containing bounce information
|
|
203
|
+
* @returns Processed bounce record or null if not a bounce
|
|
204
|
+
*/
|
|
205
|
+
async processBounceEmail(bounceEmail) {
|
|
206
|
+
try {
|
|
207
|
+
// Check if this is a bounce notification
|
|
208
|
+
const subject = bounceEmail.getSubject();
|
|
209
|
+
const body = bounceEmail.getBody();
|
|
210
|
+
// Check for common bounce notification subject patterns
|
|
211
|
+
const isBounceSubject = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject);
|
|
212
|
+
if (!isBounceSubject) {
|
|
213
|
+
// Not a bounce notification based on subject
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
// Extract original recipient from the body or headers
|
|
217
|
+
let recipient = '';
|
|
218
|
+
let originalMessageId = '';
|
|
219
|
+
// Extract recipient from common bounce formats
|
|
220
|
+
const recipientMatch = body.match(/(?:failed recipient|to[:=]\s*|recipient:|delivery failed:)\s*<?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})>?/i);
|
|
221
|
+
if (recipientMatch && recipientMatch[1]) {
|
|
222
|
+
recipient = recipientMatch[1];
|
|
223
|
+
}
|
|
224
|
+
// Extract diagnostic code
|
|
225
|
+
let diagnosticCode = '';
|
|
226
|
+
const diagnosticMatch = body.match(/diagnostic(?:-|\\s+)code:\s*(.+)(?:\n|$)/i);
|
|
227
|
+
if (diagnosticMatch && diagnosticMatch[1]) {
|
|
228
|
+
diagnosticCode = diagnosticMatch[1].trim();
|
|
229
|
+
}
|
|
230
|
+
// Extract SMTP status code
|
|
231
|
+
let statusCode = '';
|
|
232
|
+
const statusMatch = body.match(/status(?:-|\\s+)code:\s*([0-9.]+)/i);
|
|
233
|
+
if (statusMatch && statusMatch[1]) {
|
|
234
|
+
statusCode = statusMatch[1].trim();
|
|
235
|
+
}
|
|
236
|
+
// If recipient not found in standard patterns, try DSN (Delivery Status Notification) format
|
|
237
|
+
if (!recipient) {
|
|
238
|
+
// Look for DSN format with Original-Recipient or Final-Recipient fields
|
|
239
|
+
const originalRecipientMatch = body.match(/original-recipient:.*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
|
240
|
+
const finalRecipientMatch = body.match(/final-recipient:.*?([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/i);
|
|
241
|
+
if (originalRecipientMatch && originalRecipientMatch[1]) {
|
|
242
|
+
recipient = originalRecipientMatch[1];
|
|
243
|
+
}
|
|
244
|
+
else if (finalRecipientMatch && finalRecipientMatch[1]) {
|
|
245
|
+
recipient = finalRecipientMatch[1];
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// If still no recipient, can't process as bounce
|
|
249
|
+
if (!recipient) {
|
|
250
|
+
logger.log('warn', 'Could not extract recipient from bounce notification', {
|
|
251
|
+
subject,
|
|
252
|
+
sender: bounceEmail.from
|
|
253
|
+
});
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
// Extract original message ID if available
|
|
257
|
+
const messageIdMatch = body.match(/original[ -]message[ -]id:[ \t]*<?([^>]+)>?/i);
|
|
258
|
+
if (messageIdMatch && messageIdMatch[1]) {
|
|
259
|
+
originalMessageId = messageIdMatch[1].trim();
|
|
260
|
+
}
|
|
261
|
+
// Create bounce data
|
|
262
|
+
const bounceData = {
|
|
263
|
+
recipient,
|
|
264
|
+
sender: bounceEmail.from,
|
|
265
|
+
domain: recipient.split('@')[1],
|
|
266
|
+
subject: bounceEmail.getSubject(),
|
|
267
|
+
diagnosticCode,
|
|
268
|
+
statusCode,
|
|
269
|
+
timestamp: Date.now(),
|
|
270
|
+
headers: {}
|
|
271
|
+
};
|
|
272
|
+
// Process as a regular bounce
|
|
273
|
+
return this.processBounce(bounceData);
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
logger.log('error', `Error processing bounce email: ${error.message}`);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Handle a hard bounce by adding to suppression list
|
|
282
|
+
* @param bounce The bounce record
|
|
283
|
+
*/
|
|
284
|
+
async handleHardBounce(bounce) {
|
|
285
|
+
// Add to suppression list permanently (no expiry)
|
|
286
|
+
this.addToSuppressionList(bounce.recipient, `Hard bounce: ${bounce.bounceType}`, undefined);
|
|
287
|
+
// Increment bounce count in cache
|
|
288
|
+
this.updateBounceCache(bounce);
|
|
289
|
+
// Save to permanent storage
|
|
290
|
+
await this.saveBounceRecord(bounce);
|
|
291
|
+
// Log hard bounce for monitoring
|
|
292
|
+
logger.log('warn', `Hard bounce for ${bounce.recipient}: ${bounce.bounceType}`, {
|
|
293
|
+
domain: bounce.domain,
|
|
294
|
+
smtpResponse: bounce.smtpResponse,
|
|
295
|
+
diagnosticCode: bounce.diagnosticCode
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Handle a soft bounce by scheduling a retry if eligible
|
|
300
|
+
* @param bounce The bounce record
|
|
301
|
+
*/
|
|
302
|
+
async handleSoftBounce(bounce) {
|
|
303
|
+
// Check if we've exceeded max retries
|
|
304
|
+
if (bounce.retryCount >= this.retryStrategy.maxRetries) {
|
|
305
|
+
logger.log('warn', `Max retries exceeded for ${bounce.recipient}, treating as hard bounce`);
|
|
306
|
+
// Convert to hard bounce after max retries
|
|
307
|
+
bounce.bounceCategory = BounceCategory.HARD;
|
|
308
|
+
await this.handleHardBounce(bounce);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Calculate next retry time with exponential backoff
|
|
312
|
+
const delay = Math.min(this.retryStrategy.initialDelay * Math.pow(this.retryStrategy.backoffFactor, bounce.retryCount), this.retryStrategy.maxDelay);
|
|
313
|
+
bounce.retryCount++;
|
|
314
|
+
bounce.nextRetryTime = Date.now() + delay;
|
|
315
|
+
// Add to suppression list temporarily (with expiry)
|
|
316
|
+
this.addToSuppressionList(bounce.recipient, `Soft bounce: ${bounce.bounceType}`, bounce.nextRetryTime);
|
|
317
|
+
// Log the retry schedule
|
|
318
|
+
logger.log('info', `Scheduled retry ${bounce.retryCount} for ${bounce.recipient} at ${new Date(bounce.nextRetryTime).toISOString()}`, {
|
|
319
|
+
bounceType: bounce.bounceType,
|
|
320
|
+
retryCount: bounce.retryCount,
|
|
321
|
+
nextRetry: bounce.nextRetryTime
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Add an email address to the suppression list
|
|
326
|
+
* @param email Email address to suppress
|
|
327
|
+
* @param reason Reason for suppression
|
|
328
|
+
* @param expiresAt Expiration timestamp (undefined for permanent)
|
|
329
|
+
*/
|
|
330
|
+
addToSuppressionList(email, reason, expiresAt) {
|
|
331
|
+
this.suppressionList.set(email.toLowerCase(), {
|
|
332
|
+
reason,
|
|
333
|
+
timestamp: Date.now(),
|
|
334
|
+
expiresAt
|
|
335
|
+
});
|
|
336
|
+
// Save asynchronously without blocking
|
|
337
|
+
this.saveSuppressionList().catch(error => {
|
|
338
|
+
logger.log('error', `Failed to save suppression list after adding ${email}: ${error.message}`);
|
|
339
|
+
});
|
|
340
|
+
logger.log('info', `Added ${email} to suppression list`, {
|
|
341
|
+
reason,
|
|
342
|
+
expiresAt: expiresAt ? new Date(expiresAt).toISOString() : 'permanent'
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Remove an email address from the suppression list
|
|
347
|
+
* @param email Email address to remove
|
|
348
|
+
*/
|
|
349
|
+
removeFromSuppressionList(email) {
|
|
350
|
+
const wasRemoved = this.suppressionList.delete(email.toLowerCase());
|
|
351
|
+
if (wasRemoved) {
|
|
352
|
+
// Save asynchronously without blocking
|
|
353
|
+
this.saveSuppressionList().catch(error => {
|
|
354
|
+
logger.log('error', `Failed to save suppression list after removing ${email}: ${error.message}`);
|
|
355
|
+
});
|
|
356
|
+
logger.log('info', `Removed ${email} from suppression list`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Check if an email is on the suppression list
|
|
361
|
+
* @param email Email address to check
|
|
362
|
+
* @returns Whether the email is suppressed
|
|
363
|
+
*/
|
|
364
|
+
isEmailSuppressed(email) {
|
|
365
|
+
const lowercaseEmail = email.toLowerCase();
|
|
366
|
+
const suppression = this.suppressionList.get(lowercaseEmail);
|
|
367
|
+
if (!suppression) {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
// Check if suppression has expired
|
|
371
|
+
if (suppression.expiresAt && Date.now() > suppression.expiresAt) {
|
|
372
|
+
this.suppressionList.delete(lowercaseEmail);
|
|
373
|
+
// Save asynchronously without blocking
|
|
374
|
+
this.saveSuppressionList().catch(error => {
|
|
375
|
+
logger.log('error', `Failed to save suppression list after expiry cleanup: ${error.message}`);
|
|
376
|
+
});
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Get suppression information for an email
|
|
383
|
+
* @param email Email address to check
|
|
384
|
+
* @returns Suppression information or null if not suppressed
|
|
385
|
+
*/
|
|
386
|
+
getSuppressionInfo(email) {
|
|
387
|
+
const lowercaseEmail = email.toLowerCase();
|
|
388
|
+
const suppression = this.suppressionList.get(lowercaseEmail);
|
|
389
|
+
if (!suppression) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
// Check if suppression has expired
|
|
393
|
+
if (suppression.expiresAt && Date.now() > suppression.expiresAt) {
|
|
394
|
+
this.suppressionList.delete(lowercaseEmail);
|
|
395
|
+
// Save asynchronously without blocking
|
|
396
|
+
this.saveSuppressionList().catch(error => {
|
|
397
|
+
logger.log('error', `Failed to save suppression list after expiry cleanup: ${error.message}`);
|
|
398
|
+
});
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return suppression;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Save suppression list to disk
|
|
405
|
+
*/
|
|
406
|
+
async saveSuppressionList() {
|
|
407
|
+
try {
|
|
408
|
+
const suppressionData = JSON.stringify(Array.from(this.suppressionList.entries()));
|
|
409
|
+
if (this.storageManager) {
|
|
410
|
+
// Use storage manager
|
|
411
|
+
await this.storageManager.set('/email/bounces/suppression-list.json', suppressionData);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
// Fall back to filesystem
|
|
415
|
+
await plugins.smartfs.file(plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json')).write(suppressionData);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (error) {
|
|
419
|
+
logger.log('error', `Failed to save suppression list: ${error.message}`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Load suppression list from disk
|
|
424
|
+
*/
|
|
425
|
+
async loadSuppressionList() {
|
|
426
|
+
try {
|
|
427
|
+
let entries = null;
|
|
428
|
+
let needsMigration = false;
|
|
429
|
+
if (this.storageManager) {
|
|
430
|
+
// Try to load from storage manager first
|
|
431
|
+
const suppressionData = await this.storageManager.get('/email/bounces/suppression-list.json');
|
|
432
|
+
if (suppressionData) {
|
|
433
|
+
entries = JSON.parse(suppressionData);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// Check if data exists in filesystem and migrate
|
|
437
|
+
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
|
|
438
|
+
if (plugins.fs.existsSync(suppressionPath)) {
|
|
439
|
+
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
|
|
440
|
+
entries = JSON.parse(data);
|
|
441
|
+
needsMigration = true;
|
|
442
|
+
logger.log('info', 'Migrating suppression list from filesystem to StorageManager');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
// No storage manager, use filesystem directly
|
|
448
|
+
const suppressionPath = plugins.path.join(paths.dataDir, 'emails', 'suppression_list.json');
|
|
449
|
+
if (plugins.fs.existsSync(suppressionPath)) {
|
|
450
|
+
const data = plugins.fs.readFileSync(suppressionPath, 'utf8');
|
|
451
|
+
entries = JSON.parse(data);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (entries) {
|
|
455
|
+
this.suppressionList = new Map(entries);
|
|
456
|
+
// Clean expired entries
|
|
457
|
+
const now = Date.now();
|
|
458
|
+
let expiredCount = 0;
|
|
459
|
+
for (const [email, info] of this.suppressionList.entries()) {
|
|
460
|
+
if (info.expiresAt && now > info.expiresAt) {
|
|
461
|
+
this.suppressionList.delete(email);
|
|
462
|
+
expiredCount++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
if (expiredCount > 0 || needsMigration) {
|
|
466
|
+
logger.log('info', `Cleaned ${expiredCount} expired entries from suppression list`);
|
|
467
|
+
await this.saveSuppressionList();
|
|
468
|
+
}
|
|
469
|
+
logger.log('info', `Loaded ${this.suppressionList.size} entries from suppression list`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
logger.log('error', `Failed to load suppression list: ${error.message}`);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Save bounce record to disk
|
|
478
|
+
* @param bounce Bounce record to save
|
|
479
|
+
*/
|
|
480
|
+
async saveBounceRecord(bounce) {
|
|
481
|
+
try {
|
|
482
|
+
const bounceData = JSON.stringify(bounce, null, 2);
|
|
483
|
+
if (this.storageManager) {
|
|
484
|
+
// Use storage manager
|
|
485
|
+
await this.storageManager.set(`/email/bounces/records/${bounce.id}.json`, bounceData);
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
// Fall back to filesystem
|
|
489
|
+
const bouncePath = plugins.path.join(paths.dataDir, 'emails', 'bounces', `${bounce.id}.json`);
|
|
490
|
+
// Ensure directory exists
|
|
491
|
+
const bounceDir = plugins.path.join(paths.dataDir, 'emails', 'bounces');
|
|
492
|
+
await plugins.smartfs.directory(bounceDir).recursive().create();
|
|
493
|
+
await plugins.smartfs.file(bouncePath).write(bounceData);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
logger.log('error', `Failed to save bounce record: ${error.message}`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* Update bounce cache with new bounce information
|
|
502
|
+
* @param bounce Bounce record to update cache with
|
|
503
|
+
*/
|
|
504
|
+
updateBounceCache(bounce) {
|
|
505
|
+
const email = bounce.recipient.toLowerCase();
|
|
506
|
+
const existing = this.bounceCache.get(email);
|
|
507
|
+
if (existing) {
|
|
508
|
+
// Update existing cache entry
|
|
509
|
+
existing.lastBounce = bounce.timestamp;
|
|
510
|
+
existing.count++;
|
|
511
|
+
existing.type = bounce.bounceType;
|
|
512
|
+
existing.category = bounce.bounceCategory;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
// Create new cache entry
|
|
516
|
+
this.bounceCache.set(email, {
|
|
517
|
+
lastBounce: bounce.timestamp,
|
|
518
|
+
count: 1,
|
|
519
|
+
type: bounce.bounceType,
|
|
520
|
+
category: bounce.bounceCategory
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Check bounce history for an email address
|
|
526
|
+
* @param email Email address to check
|
|
527
|
+
* @returns Bounce information or null if no bounces
|
|
528
|
+
*/
|
|
529
|
+
getBounceInfo(email) {
|
|
530
|
+
return this.bounceCache.get(email.toLowerCase()) || null;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Get all known hard bounced addresses
|
|
534
|
+
* @returns Array of hard bounced email addresses
|
|
535
|
+
*/
|
|
536
|
+
getHardBouncedAddresses() {
|
|
537
|
+
const hardBounced = [];
|
|
538
|
+
for (const [email, info] of this.bounceCache.entries()) {
|
|
539
|
+
if (info.category === BounceCategory.HARD) {
|
|
540
|
+
hardBounced.push(email);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return hardBounced;
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Get suppression list
|
|
547
|
+
* @returns Array of suppressed email addresses
|
|
548
|
+
*/
|
|
549
|
+
getSuppressionList() {
|
|
550
|
+
return Array.from(this.suppressionList.keys());
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Clear old bounce records (for maintenance)
|
|
554
|
+
* @param olderThan Timestamp to remove records older than
|
|
555
|
+
* @returns Number of records removed
|
|
556
|
+
*/
|
|
557
|
+
clearOldBounceRecords(olderThan) {
|
|
558
|
+
let removed = 0;
|
|
559
|
+
this.bounceStore = this.bounceStore.filter(bounce => {
|
|
560
|
+
if (bounce.timestamp < olderThan) {
|
|
561
|
+
removed++;
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
return true;
|
|
565
|
+
});
|
|
566
|
+
return removed;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5ib3VuY2VtYW5hZ2VyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvbWFpbC9jb3JlL2NsYXNzZXMuYm91bmNlbWFuYWdlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxLQUFLLE1BQU0sZ0JBQWdCLENBQUM7QUFDeEMsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3pDLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUM5RixPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSw4Q0FBOEMsQ0FBQztBQUNsRixPQUFPLEVBQUUsUUFBUSxFQUFFLE1BQU0sV0FBVyxDQUFDO0FBR3JDOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksVUFxQlg7QUFyQkQsV0FBWSxVQUFVO0lBQ3BCLG9DQUFvQztJQUNwQyxxREFBdUMsQ0FBQTtJQUN2QyxtREFBcUMsQ0FBQTtJQUNyQywyQ0FBNkIsQ0FBQTtJQUM3QixtREFBcUMsQ0FBQTtJQUNyQyxpQ0FBbUIsQ0FBQTtJQUNuQiwyQ0FBNkIsQ0FBQTtJQUM3QiwrQ0FBaUMsQ0FBQTtJQUVqQyxvQ0FBb0M7SUFDcEMsdURBQXlDLENBQUE7SUFDekMscURBQXVDLENBQUE7SUFDdkMsK0NBQWlDLENBQUE7SUFDakMsNkNBQStCLENBQUE7SUFDL0IsaUNBQW1CLENBQUE7SUFFbkIsZ0JBQWdCO0lBQ2hCLDZDQUErQixDQUFBO0lBQy9CLHVEQUF5QyxDQUFBO0lBQ3pDLGlDQUFtQixDQUFBO0FBQ3JCLENBQUMsRUFyQlcsVUFBVSxLQUFWLFVBQVUsUUFxQnJCO0FBRUQ7O0dBRUc7QUFDSCxNQUFNLENBQU4sSUFBWSxjQUtYO0FBTEQsV0FBWSxjQUFjO0lBQ3hCLCtCQUFhLENBQUE7SUFDYiwrQkFBYSxD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
|