@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,1207 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import * as paths from '../../paths.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
import { logger } from '../../logger.js';
|
|
5
|
+
import {
|
|
6
|
+
SecurityLogger,
|
|
7
|
+
SecurityLogLevel,
|
|
8
|
+
SecurityEventType
|
|
9
|
+
} from '../../security/index.js';
|
|
10
|
+
import { DKIMCreator } from '../security/classes.dkimcreator.js';
|
|
11
|
+
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
|
12
|
+
import type { IEmailReceivedEvent, IAuthRequestEvent, IEmailData } from '../../security/classes.rustsecuritybridge.js';
|
|
13
|
+
import { EmailRouter } from './classes.email.router.js';
|
|
14
|
+
import type { IEmailRoute, IEmailAction, IEmailContext, IEmailDomainConfig } from './interfaces.js';
|
|
15
|
+
import { Email } from '../core/classes.email.js';
|
|
16
|
+
import { DomainRegistry } from './classes.domain.registry.js';
|
|
17
|
+
import { DnsManager } from './classes.dns.manager.js';
|
|
18
|
+
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
|
|
19
|
+
import type { ISmtpSendResult, IOutboundEmail } from '../../security/classes.rustsecuritybridge.js';
|
|
20
|
+
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
|
21
|
+
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
|
|
22
|
+
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
|
|
23
|
+
import { SmtpState } from '../delivery/interfaces.js';
|
|
24
|
+
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
|
|
25
|
+
import { EmailActionExecutor } from './classes.email.action.executor.js';
|
|
26
|
+
import { DkimManager } from './classes.dkim.manager.js';
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/** External DcRouter interface shape used by UnifiedEmailServer */
|
|
30
|
+
interface DcRouter {
|
|
31
|
+
storageManager: any;
|
|
32
|
+
dnsServer?: any;
|
|
33
|
+
options?: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extended SMTP session interface with route information
|
|
38
|
+
*/
|
|
39
|
+
export interface IExtendedSmtpSession extends ISmtpSession {
|
|
40
|
+
/**
|
|
41
|
+
* Matched route for this session
|
|
42
|
+
*/
|
|
43
|
+
matchedRoute?: IEmailRoute;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Options for the unified email server
|
|
48
|
+
*/
|
|
49
|
+
export interface IUnifiedEmailServerOptions {
|
|
50
|
+
// Base server options
|
|
51
|
+
ports: number[];
|
|
52
|
+
hostname: string;
|
|
53
|
+
domains: IEmailDomainConfig[]; // Domain configurations
|
|
54
|
+
banner?: string;
|
|
55
|
+
debug?: boolean;
|
|
56
|
+
useSocketHandler?: boolean; // Use socket-handler mode instead of port listening
|
|
57
|
+
|
|
58
|
+
// Authentication options
|
|
59
|
+
auth?: {
|
|
60
|
+
required?: boolean;
|
|
61
|
+
methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
|
|
62
|
+
users?: Array<{username: string, password: string}>;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// TLS options
|
|
66
|
+
tls?: {
|
|
67
|
+
certPath?: string;
|
|
68
|
+
keyPath?: string;
|
|
69
|
+
caPath?: string;
|
|
70
|
+
minVersion?: string;
|
|
71
|
+
ciphers?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Limits
|
|
75
|
+
maxMessageSize?: number;
|
|
76
|
+
maxClients?: number;
|
|
77
|
+
maxConnections?: number;
|
|
78
|
+
|
|
79
|
+
// Connection options
|
|
80
|
+
connectionTimeout?: number;
|
|
81
|
+
socketTimeout?: number;
|
|
82
|
+
|
|
83
|
+
// Email routing rules
|
|
84
|
+
routes: IEmailRoute[];
|
|
85
|
+
|
|
86
|
+
// Global defaults for all domains
|
|
87
|
+
defaults?: {
|
|
88
|
+
dnsMode?: 'forward' | 'internal-dns' | 'external-dns';
|
|
89
|
+
dkim?: IEmailDomainConfig['dkim'];
|
|
90
|
+
rateLimits?: IEmailDomainConfig['rateLimits'];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// Outbound settings
|
|
94
|
+
outbound?: {
|
|
95
|
+
maxConnections?: number;
|
|
96
|
+
connectionTimeout?: number;
|
|
97
|
+
socketTimeout?: number;
|
|
98
|
+
retryAttempts?: number;
|
|
99
|
+
defaultFrom?: string;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Rate limiting (global limits, can be overridden per domain)
|
|
103
|
+
rateLimits?: IHierarchicalRateLimits;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extended SMTP session interface for UnifiedEmailServer
|
|
109
|
+
*/
|
|
110
|
+
export interface ISmtpSession extends IBaseSmtpSession {
|
|
111
|
+
/**
|
|
112
|
+
* User information if authenticated
|
|
113
|
+
*/
|
|
114
|
+
user?: {
|
|
115
|
+
username: string;
|
|
116
|
+
[key: string]: any;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Matched route for this session
|
|
121
|
+
*/
|
|
122
|
+
matchedRoute?: IEmailRoute;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Authentication data for SMTP
|
|
127
|
+
*/
|
|
128
|
+
import type { ISmtpAuth } from '../delivery/interfaces.js';
|
|
129
|
+
export type IAuthData = ISmtpAuth;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Server statistics
|
|
133
|
+
*/
|
|
134
|
+
export interface IServerStats {
|
|
135
|
+
startTime: Date;
|
|
136
|
+
connections: {
|
|
137
|
+
current: number;
|
|
138
|
+
total: number;
|
|
139
|
+
};
|
|
140
|
+
messages: {
|
|
141
|
+
processed: number;
|
|
142
|
+
delivered: number;
|
|
143
|
+
failed: number;
|
|
144
|
+
};
|
|
145
|
+
processingTime: {
|
|
146
|
+
avg: number;
|
|
147
|
+
max: number;
|
|
148
|
+
min: number;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Unified email server that handles all email traffic with pattern-based routing
|
|
154
|
+
*/
|
|
155
|
+
export class UnifiedEmailServer extends EventEmitter {
|
|
156
|
+
private dcRouter: DcRouter;
|
|
157
|
+
private options: IUnifiedEmailServerOptions;
|
|
158
|
+
private emailRouter: EmailRouter;
|
|
159
|
+
public domainRegistry: DomainRegistry;
|
|
160
|
+
private servers: any[] = [];
|
|
161
|
+
private stats: IServerStats;
|
|
162
|
+
|
|
163
|
+
// Add components needed for sending and securing emails
|
|
164
|
+
public dkimCreator: DKIMCreator;
|
|
165
|
+
private rustBridge: RustSecurityBridge;
|
|
166
|
+
private bounceManager: BounceManager;
|
|
167
|
+
public deliveryQueue: UnifiedDeliveryQueue;
|
|
168
|
+
public deliverySystem: MultiModeDeliverySystem;
|
|
169
|
+
private rateLimiter: UnifiedRateLimiter; // TODO: Implement rate limiting in SMTP server handlers
|
|
170
|
+
|
|
171
|
+
// Extracted subsystems
|
|
172
|
+
private actionExecutor: EmailActionExecutor;
|
|
173
|
+
private dkimManager: DkimManager;
|
|
174
|
+
|
|
175
|
+
constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
|
|
176
|
+
super();
|
|
177
|
+
this.dcRouter = dcRouter;
|
|
178
|
+
|
|
179
|
+
// Set default options
|
|
180
|
+
this.options = {
|
|
181
|
+
...options,
|
|
182
|
+
banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`,
|
|
183
|
+
maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB
|
|
184
|
+
maxClients: options.maxClients || 100,
|
|
185
|
+
maxConnections: options.maxConnections || 1000,
|
|
186
|
+
connectionTimeout: options.connectionTimeout || 60000, // 1 minute
|
|
187
|
+
socketTimeout: options.socketTimeout || 60000 // 1 minute
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Initialize Rust security bridge (singleton)
|
|
191
|
+
this.rustBridge = RustSecurityBridge.getInstance();
|
|
192
|
+
|
|
193
|
+
// Initialize DKIM creator with storage manager
|
|
194
|
+
this.dkimCreator = new DKIMCreator(paths.keysDir, dcRouter.storageManager);
|
|
195
|
+
|
|
196
|
+
// Initialize bounce manager with storage manager
|
|
197
|
+
this.bounceManager = new BounceManager({
|
|
198
|
+
maxCacheSize: 10000,
|
|
199
|
+
cacheTTL: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
200
|
+
storageManager: dcRouter.storageManager
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// Initialize domain registry
|
|
204
|
+
this.domainRegistry = new DomainRegistry(options.domains, options.defaults);
|
|
205
|
+
|
|
206
|
+
// Initialize email router with routes and storage manager
|
|
207
|
+
this.emailRouter = new EmailRouter(options.routes || [], {
|
|
208
|
+
storageManager: dcRouter.storageManager,
|
|
209
|
+
persistChanges: true
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Initialize rate limiter
|
|
213
|
+
this.rateLimiter = new UnifiedRateLimiter(options.rateLimits || {
|
|
214
|
+
global: {
|
|
215
|
+
maxConnectionsPerIP: 10,
|
|
216
|
+
maxMessagesPerMinute: 100,
|
|
217
|
+
maxRecipientsPerMessage: 50,
|
|
218
|
+
maxErrorsPerIP: 10,
|
|
219
|
+
maxAuthFailuresPerIP: 5,
|
|
220
|
+
blockDuration: 300000 // 5 minutes
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Initialize delivery components
|
|
225
|
+
const queueOptions: IQueueOptions = {
|
|
226
|
+
storageType: 'memory', // Default to memory storage
|
|
227
|
+
maxRetries: 3,
|
|
228
|
+
baseRetryDelay: 300000, // 5 minutes
|
|
229
|
+
maxRetryDelay: 3600000 // 1 hour
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
|
|
233
|
+
|
|
234
|
+
const deliveryOptions: IMultiModeDeliveryOptions = {
|
|
235
|
+
globalRateLimit: 100, // Default to 100 emails per minute
|
|
236
|
+
concurrentDeliveries: 10,
|
|
237
|
+
processBounces: true,
|
|
238
|
+
bounceHandler: {
|
|
239
|
+
processSmtpFailure: this.processSmtpFailure.bind(this)
|
|
240
|
+
},
|
|
241
|
+
onDeliverySuccess: async (_item, _result) => {
|
|
242
|
+
// Delivery success recorded via delivery system
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions, this);
|
|
247
|
+
|
|
248
|
+
// Initialize action executor
|
|
249
|
+
this.actionExecutor = new EmailActionExecutor({
|
|
250
|
+
sendOutboundEmail: this.sendOutboundEmail.bind(this),
|
|
251
|
+
bounceManager: this.bounceManager,
|
|
252
|
+
deliveryQueue: this.deliveryQueue,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Initialize DKIM manager
|
|
256
|
+
this.dkimManager = new DkimManager(this.dkimCreator, this.domainRegistry, dcRouter, this.rustBridge);
|
|
257
|
+
|
|
258
|
+
// Initialize statistics
|
|
259
|
+
this.stats = {
|
|
260
|
+
startTime: new Date(),
|
|
261
|
+
connections: {
|
|
262
|
+
current: 0,
|
|
263
|
+
total: 0
|
|
264
|
+
},
|
|
265
|
+
messages: {
|
|
266
|
+
processed: 0,
|
|
267
|
+
delivered: 0,
|
|
268
|
+
failed: 0
|
|
269
|
+
},
|
|
270
|
+
processingTime: {
|
|
271
|
+
avg: 0,
|
|
272
|
+
max: 0,
|
|
273
|
+
min: 0
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// We'll create the SMTP servers during the start() method
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Send an outbound email via the Rust SMTP client.
|
|
282
|
+
* Uses connection pooling in the Rust binary for efficiency.
|
|
283
|
+
*/
|
|
284
|
+
public async sendOutboundEmail(host: string, port: number, email: Email, options?: {
|
|
285
|
+
auth?: { user: string; pass: string };
|
|
286
|
+
dkimDomain?: string;
|
|
287
|
+
dkimSelector?: string;
|
|
288
|
+
tlsOpportunistic?: boolean;
|
|
289
|
+
}): Promise<ISmtpSendResult> {
|
|
290
|
+
// Build DKIM config if domain has keys
|
|
291
|
+
let dkim: { domain: string; selector: string; privateKey: string } | undefined;
|
|
292
|
+
if (options?.dkimDomain) {
|
|
293
|
+
try {
|
|
294
|
+
const { privateKey } = await this.dkimCreator.readDKIMKeys(options.dkimDomain);
|
|
295
|
+
dkim = { domain: options.dkimDomain, selector: options.dkimSelector || 'default', privateKey };
|
|
296
|
+
} catch (err) {
|
|
297
|
+
logger.log('warn', `Failed to read DKIM keys for ${options.dkimDomain}: ${(err as Error).message}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Serialize the Email to the outbound format
|
|
302
|
+
const outboundEmail: IOutboundEmail = {
|
|
303
|
+
from: email.from,
|
|
304
|
+
to: email.to,
|
|
305
|
+
cc: email.cc || [],
|
|
306
|
+
bcc: email.bcc || [],
|
|
307
|
+
subject: email.subject || '',
|
|
308
|
+
text: email.text || '',
|
|
309
|
+
html: email.html || undefined,
|
|
310
|
+
headers: email.headers as Record<string, string> || {},
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return this.rustBridge.sendOutboundEmail({
|
|
314
|
+
host,
|
|
315
|
+
port,
|
|
316
|
+
secure: port === 465,
|
|
317
|
+
domain: this.options.hostname,
|
|
318
|
+
auth: options?.auth,
|
|
319
|
+
email: outboundEmail,
|
|
320
|
+
dkim,
|
|
321
|
+
connectionTimeoutSecs: Math.floor((this.options.outbound?.connectionTimeout || 30000) / 1000),
|
|
322
|
+
socketTimeoutSecs: Math.floor((this.options.outbound?.socketTimeout || 120000) / 1000),
|
|
323
|
+
poolKey: `${host}:${port}`,
|
|
324
|
+
maxPoolConnections: this.options.outbound?.maxConnections || 10,
|
|
325
|
+
tlsOpportunistic: options?.tlsOpportunistic ?? (port === 25),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Start the unified email server
|
|
331
|
+
*/
|
|
332
|
+
public async start(): Promise<void> {
|
|
333
|
+
logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`);
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
await this.startDeliveryPipeline();
|
|
337
|
+
await this.startRustBridge();
|
|
338
|
+
await this.initializeDkimAndDns();
|
|
339
|
+
this.registerBridgeEventHandlers();
|
|
340
|
+
await this.startSmtpServer();
|
|
341
|
+
logger.log('info', 'UnifiedEmailServer started successfully');
|
|
342
|
+
this.emit('started');
|
|
343
|
+
} catch (error) {
|
|
344
|
+
logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private async startDeliveryPipeline(): Promise<void> {
|
|
350
|
+
await this.deliveryQueue.initialize();
|
|
351
|
+
logger.log('info', 'Email delivery queue initialized');
|
|
352
|
+
|
|
353
|
+
await this.deliverySystem.start();
|
|
354
|
+
logger.log('info', 'Email delivery system started');
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
private async startRustBridge(): Promise<void> {
|
|
358
|
+
const bridgeOk = await this.rustBridge.start();
|
|
359
|
+
if (!bridgeOk) {
|
|
360
|
+
throw new Error('Rust security bridge failed to start. The mailer-bin binary is required. Run "pnpm build" to compile it.');
|
|
361
|
+
}
|
|
362
|
+
logger.log('info', 'Rust security bridge started — Rust is the primary security backend');
|
|
363
|
+
|
|
364
|
+
this.rustBridge.on('stateChange', ({ oldState, newState }: { oldState: string; newState: string }) => {
|
|
365
|
+
if (newState === 'failed') this.emit('bridgeFailed');
|
|
366
|
+
else if (newState === 'restarting') this.emit('bridgeRestarting');
|
|
367
|
+
else if (newState === 'running' && oldState === 'restarting') this.emit('bridgeRecovered');
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private async initializeDkimAndDns(): Promise<void> {
|
|
372
|
+
await this.dkimManager.setupDkimForDomains();
|
|
373
|
+
logger.log('info', 'DKIM configuration completed for all domains');
|
|
374
|
+
|
|
375
|
+
const dnsManager = new DnsManager(this.dcRouter);
|
|
376
|
+
await dnsManager.ensureDnsRecords(this.domainRegistry.getAllConfigs(), this.dkimCreator);
|
|
377
|
+
logger.log('info', 'DNS records ensured for all configured domains');
|
|
378
|
+
|
|
379
|
+
this.applyDomainRateLimits();
|
|
380
|
+
logger.log('info', 'Per-domain rate limits configured');
|
|
381
|
+
|
|
382
|
+
await this.dkimManager.checkAndRotateDkimKeys();
|
|
383
|
+
logger.log('info', 'DKIM key rotation check completed');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private registerBridgeEventHandlers(): void {
|
|
387
|
+
this.rustBridge.onEmailReceived(async (data) => {
|
|
388
|
+
try {
|
|
389
|
+
await this.handleRustEmailReceived(data);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
logger.log('error', `Error handling email from Rust SMTP: ${(err as Error).message}`);
|
|
392
|
+
try {
|
|
393
|
+
await this.rustBridge.sendEmailProcessingResult({
|
|
394
|
+
correlationId: data.correlationId,
|
|
395
|
+
accepted: false,
|
|
396
|
+
smtpCode: 451,
|
|
397
|
+
smtpMessage: 'Internal processing error',
|
|
398
|
+
});
|
|
399
|
+
} catch (sendErr) {
|
|
400
|
+
logger.log('warn', `Could not send rejection back to Rust: ${(sendErr as Error).message}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
this.rustBridge.onAuthRequest(async (data) => {
|
|
406
|
+
try {
|
|
407
|
+
await this.handleRustAuthRequest(data);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
logger.log('error', `Error handling auth from Rust SMTP: ${(err as Error).message}`);
|
|
410
|
+
try {
|
|
411
|
+
await this.rustBridge.sendAuthResult({
|
|
412
|
+
correlationId: data.correlationId,
|
|
413
|
+
success: false,
|
|
414
|
+
message: 'Internal auth error',
|
|
415
|
+
});
|
|
416
|
+
} catch (sendErr) {
|
|
417
|
+
logger.log('warn', `Could not send auth rejection back to Rust: ${(sendErr as Error).message}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
this.rustBridge.onScramCredentialRequest(async (data) => {
|
|
423
|
+
try {
|
|
424
|
+
await this.handleScramCredentialRequest(data);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
logger.log('error', `Error handling SCRAM credential request: ${(err as Error).message}`);
|
|
427
|
+
try {
|
|
428
|
+
await this.rustBridge.sendScramCredentialResult({
|
|
429
|
+
correlationId: data.correlationId,
|
|
430
|
+
found: false,
|
|
431
|
+
});
|
|
432
|
+
} catch (sendErr) {
|
|
433
|
+
logger.log('warn', `Could not send SCRAM credential rejection: ${(sendErr as Error).message}`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private async startSmtpServer(): Promise<void> {
|
|
440
|
+
const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
|
|
441
|
+
let tlsCertPem: string | undefined;
|
|
442
|
+
let tlsKeyPem: string | undefined;
|
|
443
|
+
|
|
444
|
+
if (hasTlsConfig) {
|
|
445
|
+
try {
|
|
446
|
+
tlsKeyPem = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
|
|
447
|
+
tlsCertPem = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
|
|
448
|
+
logger.log('info', 'TLS certificates loaded successfully');
|
|
449
|
+
} catch (error) {
|
|
450
|
+
logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const smtpPorts = (this.options.ports as number[]).filter(p => p !== 465);
|
|
455
|
+
const securePort = (this.options.ports as number[]).find(p => p === 465);
|
|
456
|
+
|
|
457
|
+
const started = await this.rustBridge.startSmtpServer({
|
|
458
|
+
hostname: this.options.hostname,
|
|
459
|
+
ports: smtpPorts,
|
|
460
|
+
securePort: securePort,
|
|
461
|
+
tlsCertPem,
|
|
462
|
+
tlsKeyPem,
|
|
463
|
+
maxMessageSize: this.options.maxMessageSize || 10 * 1024 * 1024,
|
|
464
|
+
maxConnections: this.options.maxConnections || this.options.maxClients || 100,
|
|
465
|
+
maxRecipients: 100,
|
|
466
|
+
connectionTimeoutSecs: this.options.connectionTimeout ? Math.floor(this.options.connectionTimeout / 1000) : 30,
|
|
467
|
+
dataTimeoutSecs: 60,
|
|
468
|
+
authEnabled: !!this.options.auth?.required || !!(this.options.auth?.users?.length),
|
|
469
|
+
maxAuthFailures: 3,
|
|
470
|
+
socketTimeoutSecs: this.options.socketTimeout ? Math.floor(this.options.socketTimeout / 1000) : 300,
|
|
471
|
+
processingTimeoutSecs: 30,
|
|
472
|
+
rateLimits: this.options.rateLimits ? {
|
|
473
|
+
maxConnectionsPerIp: this.options.rateLimits.global?.maxConnectionsPerIP || 50,
|
|
474
|
+
maxMessagesPerSender: this.options.rateLimits.global?.maxMessagesPerMinute || 100,
|
|
475
|
+
maxAuthFailuresPerIp: this.options.rateLimits.global?.maxAuthFailuresPerIP || 5,
|
|
476
|
+
windowSecs: 60,
|
|
477
|
+
} : undefined,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
if (!started) {
|
|
481
|
+
throw new Error('Failed to start Rust SMTP server');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
logger.log('info', `Rust SMTP server listening on ports: ${smtpPorts.join(', ')}${securePort ? ` + ${securePort} (TLS)` : ''}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Stop the unified email server
|
|
489
|
+
*/
|
|
490
|
+
public async stop(): Promise<void> {
|
|
491
|
+
logger.log('info', 'Stopping UnifiedEmailServer');
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
// Stop the Rust SMTP server first
|
|
495
|
+
try {
|
|
496
|
+
await this.rustBridge.stopSmtpServer();
|
|
497
|
+
logger.log('info', 'Rust SMTP server stopped');
|
|
498
|
+
} catch (err) {
|
|
499
|
+
logger.log('warn', `Error stopping Rust SMTP server: ${(err as Error).message}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Clear the servers array - servers will be garbage collected
|
|
503
|
+
this.servers = [];
|
|
504
|
+
|
|
505
|
+
// Remove bridge state change listener and stop bridge
|
|
506
|
+
this.rustBridge.removeAllListeners('stateChange');
|
|
507
|
+
await this.rustBridge.stop();
|
|
508
|
+
|
|
509
|
+
// Stop the delivery system
|
|
510
|
+
if (this.deliverySystem) {
|
|
511
|
+
await this.deliverySystem.stop();
|
|
512
|
+
logger.log('info', 'Email delivery system stopped');
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Shut down the delivery queue
|
|
516
|
+
if (this.deliveryQueue) {
|
|
517
|
+
await this.deliveryQueue.shutdown();
|
|
518
|
+
logger.log('info', 'Email delivery queue shut down');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Close all Rust SMTP client connection pools
|
|
522
|
+
try {
|
|
523
|
+
await this.rustBridge.closeSmtpPool();
|
|
524
|
+
} catch {
|
|
525
|
+
// Bridge may already be stopped
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
logger.log('info', 'UnifiedEmailServer stopped successfully');
|
|
529
|
+
this.emit('stopped');
|
|
530
|
+
} catch (error) {
|
|
531
|
+
logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
|
|
532
|
+
throw error;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// -----------------------------------------------------------------------
|
|
537
|
+
// Rust SMTP server event handlers
|
|
538
|
+
// -----------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Handle an emailReceived event from the Rust SMTP server.
|
|
542
|
+
*/
|
|
543
|
+
private async handleRustEmailReceived(data: IEmailReceivedEvent): Promise<void> {
|
|
544
|
+
const { correlationId, mailFrom, rcptTo, remoteAddr, clientHostname, secure, authenticatedUser } = data;
|
|
545
|
+
|
|
546
|
+
logger.log('info', `Rust SMTP received email from=${mailFrom} to=${rcptTo.join(',')} remote=${remoteAddr}`);
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
// Decode the email data
|
|
550
|
+
let rawMessageBuffer: Buffer;
|
|
551
|
+
if (data.data.type === 'inline' && data.data.base64) {
|
|
552
|
+
rawMessageBuffer = Buffer.from(data.data.base64, 'base64');
|
|
553
|
+
} else if (data.data.type === 'file' && data.data.path) {
|
|
554
|
+
rawMessageBuffer = plugins.fs.readFileSync(data.data.path);
|
|
555
|
+
// Clean up temp file
|
|
556
|
+
try {
|
|
557
|
+
plugins.fs.unlinkSync(data.data.path);
|
|
558
|
+
} catch {
|
|
559
|
+
// Ignore cleanup errors
|
|
560
|
+
}
|
|
561
|
+
} else {
|
|
562
|
+
throw new Error('Invalid email data transport');
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Build a session-like object for processEmailByMode
|
|
566
|
+
const session: IExtendedSmtpSession = {
|
|
567
|
+
id: data.sessionId || 'rust-' + Math.random().toString(36).substring(2),
|
|
568
|
+
state: SmtpState.FINISHED,
|
|
569
|
+
mailFrom: mailFrom,
|
|
570
|
+
rcptTo: rcptTo,
|
|
571
|
+
emailData: rawMessageBuffer.toString('utf8'),
|
|
572
|
+
useTLS: secure,
|
|
573
|
+
connectionEnded: false,
|
|
574
|
+
remoteAddress: remoteAddr,
|
|
575
|
+
clientHostname: clientHostname || '',
|
|
576
|
+
secure: secure,
|
|
577
|
+
authenticated: !!authenticatedUser,
|
|
578
|
+
envelope: {
|
|
579
|
+
mailFrom: { address: mailFrom, args: {} },
|
|
580
|
+
rcptTo: rcptTo.map(addr => ({ address: addr, args: {} })),
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
if (authenticatedUser) {
|
|
585
|
+
session.user = { username: authenticatedUser };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Attach pre-computed security results from Rust in-process pipeline
|
|
589
|
+
if (data.securityResults) {
|
|
590
|
+
(session as any)._precomputedSecurityResults = data.securityResults;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Process the email through the routing system
|
|
594
|
+
await this.processEmailByMode(rawMessageBuffer, session);
|
|
595
|
+
|
|
596
|
+
// Send acceptance back to Rust
|
|
597
|
+
await this.rustBridge.sendEmailProcessingResult({
|
|
598
|
+
correlationId,
|
|
599
|
+
accepted: true,
|
|
600
|
+
smtpCode: 250,
|
|
601
|
+
smtpMessage: '2.0.0 Message accepted for delivery',
|
|
602
|
+
});
|
|
603
|
+
} catch (err) {
|
|
604
|
+
logger.log('error', `Failed to process email from Rust SMTP: ${(err as Error).message}`);
|
|
605
|
+
await this.rustBridge.sendEmailProcessingResult({
|
|
606
|
+
correlationId,
|
|
607
|
+
accepted: false,
|
|
608
|
+
smtpCode: 550,
|
|
609
|
+
smtpMessage: `5.0.0 Processing failed: ${(err as Error).message}`,
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Handle an authRequest event from the Rust SMTP server.
|
|
616
|
+
*/
|
|
617
|
+
private async handleRustAuthRequest(data: IAuthRequestEvent): Promise<void> {
|
|
618
|
+
const { correlationId, username, password, remoteAddr } = data;
|
|
619
|
+
|
|
620
|
+
logger.log('info', `Rust SMTP auth request for user=${username} from=${remoteAddr}`);
|
|
621
|
+
|
|
622
|
+
// Check against configured users
|
|
623
|
+
const users = this.options.auth?.users || [];
|
|
624
|
+
const matched = users.find(
|
|
625
|
+
u => u.username === username && u.password === password
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
if (matched) {
|
|
629
|
+
await this.rustBridge.sendAuthResult({
|
|
630
|
+
correlationId,
|
|
631
|
+
success: true,
|
|
632
|
+
});
|
|
633
|
+
} else {
|
|
634
|
+
logger.log('warn', `Auth failed for user=${username} from=${remoteAddr}`);
|
|
635
|
+
await this.rustBridge.sendAuthResult({
|
|
636
|
+
correlationId,
|
|
637
|
+
success: false,
|
|
638
|
+
message: 'Invalid credentials',
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Handle a SCRAM credential request from the Rust SMTP server.
|
|
645
|
+
* Computes SCRAM-SHA-256 credentials from the stored password for the given user.
|
|
646
|
+
*/
|
|
647
|
+
private async handleScramCredentialRequest(data: { correlationId: string; username: string; remoteAddr: string }): Promise<void> {
|
|
648
|
+
const { correlationId, username, remoteAddr } = data;
|
|
649
|
+
const crypto = await import('crypto');
|
|
650
|
+
|
|
651
|
+
logger.log('info', `SCRAM credential request for user=${username} from=${remoteAddr}`);
|
|
652
|
+
|
|
653
|
+
const users = this.options.auth?.users || [];
|
|
654
|
+
const matched = users.find(u => u.username === username);
|
|
655
|
+
|
|
656
|
+
if (!matched) {
|
|
657
|
+
await this.rustBridge.sendScramCredentialResult({
|
|
658
|
+
correlationId,
|
|
659
|
+
found: false,
|
|
660
|
+
});
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Compute SCRAM-SHA-256 credentials from plaintext password
|
|
665
|
+
const salt = crypto.randomBytes(16);
|
|
666
|
+
const iterations = 4096;
|
|
667
|
+
|
|
668
|
+
// SaltedPassword = PBKDF2-HMAC-SHA256(password, salt, iterations, 32)
|
|
669
|
+
const saltedPassword = crypto.pbkdf2Sync(matched.password, salt, iterations, 32, 'sha256');
|
|
670
|
+
|
|
671
|
+
// ClientKey = HMAC-SHA256(SaltedPassword, "Client Key")
|
|
672
|
+
const clientKey = crypto.createHmac('sha256', saltedPassword).update('Client Key').digest();
|
|
673
|
+
|
|
674
|
+
// StoredKey = SHA256(ClientKey)
|
|
675
|
+
const storedKey = crypto.createHash('sha256').update(clientKey).digest();
|
|
676
|
+
|
|
677
|
+
// ServerKey = HMAC-SHA256(SaltedPassword, "Server Key")
|
|
678
|
+
const serverKey = crypto.createHmac('sha256', saltedPassword).update('Server Key').digest();
|
|
679
|
+
|
|
680
|
+
await this.rustBridge.sendScramCredentialResult({
|
|
681
|
+
correlationId,
|
|
682
|
+
found: true,
|
|
683
|
+
salt: salt.toString('base64'),
|
|
684
|
+
iterations,
|
|
685
|
+
storedKey: storedKey.toString('base64'),
|
|
686
|
+
serverKey: serverKey.toString('base64'),
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Verify inbound email security (DKIM/SPF/DMARC) using pre-computed Rust results
|
|
692
|
+
* or falling back to IPC call if no pre-computed results are available.
|
|
693
|
+
*/
|
|
694
|
+
private async verifyInboundSecurity(email: Email, session: IExtendedSmtpSession): Promise<void> {
|
|
695
|
+
try {
|
|
696
|
+
// Check for pre-computed results from Rust in-process security pipeline
|
|
697
|
+
const precomputed = (session as any)._precomputedSecurityResults;
|
|
698
|
+
let result: any;
|
|
699
|
+
|
|
700
|
+
if (precomputed) {
|
|
701
|
+
logger.log('info', 'Using pre-computed security results from Rust in-process pipeline');
|
|
702
|
+
result = precomputed;
|
|
703
|
+
} else {
|
|
704
|
+
// Fallback: IPC round-trip to Rust (for backward compat)
|
|
705
|
+
const rawMessage = session.emailData || email.toRFC822String();
|
|
706
|
+
result = await this.rustBridge.verifyEmail({
|
|
707
|
+
rawMessage,
|
|
708
|
+
ip: session.remoteAddress,
|
|
709
|
+
heloDomain: session.clientHostname || '',
|
|
710
|
+
hostname: this.options.hostname,
|
|
711
|
+
mailFrom: session.envelope?.mailFrom?.address || session.mailFrom || '',
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Apply DKIM result headers
|
|
716
|
+
if (result.dkim && result.dkim.length > 0) {
|
|
717
|
+
const dkimSummary = result.dkim
|
|
718
|
+
.map((d: any) => `${d.status}${d.domain ? ` (${d.domain})` : ''}`)
|
|
719
|
+
.join(', ');
|
|
720
|
+
email.addHeader('X-DKIM-Result', dkimSummary);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Apply SPF result header
|
|
724
|
+
if (result.spf) {
|
|
725
|
+
email.addHeader('Received-SPF', `${result.spf.result} (domain: ${result.spf.domain}, ip: ${result.spf.ip})`);
|
|
726
|
+
|
|
727
|
+
// Mark as spam on SPF hard fail
|
|
728
|
+
if (result.spf.result === 'fail') {
|
|
729
|
+
email.mightBeSpam = true;
|
|
730
|
+
logger.log('warn', `SPF fail for ${session.remoteAddress} — marking as potential spam`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Apply DMARC result header and policy
|
|
735
|
+
if (result.dmarc) {
|
|
736
|
+
email.addHeader('X-DMARC-Result', `${result.dmarc.action} (policy=${result.dmarc.policy}, dkim=${result.dmarc.dkim_result}, spf=${result.dmarc.spf_result})`);
|
|
737
|
+
|
|
738
|
+
if (result.dmarc.action === 'reject') {
|
|
739
|
+
email.mightBeSpam = true;
|
|
740
|
+
logger.log('warn', `DMARC reject for domain ${result.dmarc.domain} — marking as spam`);
|
|
741
|
+
} else if (result.dmarc.action === 'quarantine') {
|
|
742
|
+
email.mightBeSpam = true;
|
|
743
|
+
logger.log('info', `DMARC quarantine for domain ${result.dmarc.domain} — marking as potential spam`);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Apply content scan results (from pre-computed pipeline)
|
|
748
|
+
if (result.contentScan) {
|
|
749
|
+
const scan = result.contentScan;
|
|
750
|
+
if (scan.threatScore > 0) {
|
|
751
|
+
email.addHeader('X-Spam-Score', String(scan.threatScore));
|
|
752
|
+
if (scan.threatType) {
|
|
753
|
+
email.addHeader('X-Spam-Type', scan.threatType);
|
|
754
|
+
}
|
|
755
|
+
if (scan.threatScore >= 50) {
|
|
756
|
+
email.mightBeSpam = true;
|
|
757
|
+
logger.log('warn', `Content scan threat score ${scan.threatScore} (${scan.threatType}) — marking as potential spam`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Apply IP reputation results (from pre-computed pipeline)
|
|
763
|
+
if (result.ipReputation) {
|
|
764
|
+
const rep = result.ipReputation;
|
|
765
|
+
email.addHeader('X-IP-Reputation-Score', String(rep.score));
|
|
766
|
+
if (rep.is_spam) {
|
|
767
|
+
email.mightBeSpam = true;
|
|
768
|
+
logger.log('warn', `IP ${rep.ip} flagged by reputation check (score=${rep.score}) — marking as potential spam`);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
logger.log('info', `Inbound security verified for email from ${session.remoteAddress}: DKIM=${result.dkim?.[0]?.status ?? 'none'}, SPF=${result.spf?.result ?? 'none'}, DMARC=${result.dmarc?.action ?? 'none'}`);
|
|
773
|
+
} catch (err) {
|
|
774
|
+
logger.log('warn', `Inbound security verification failed: ${(err as Error).message} — accepting email`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Process email based on routing rules
|
|
780
|
+
*/
|
|
781
|
+
public async processEmailByMode(emailData: Email | Buffer, session: IExtendedSmtpSession): Promise<Email> {
|
|
782
|
+
// Convert Buffer to Email if needed
|
|
783
|
+
let email: Email;
|
|
784
|
+
if (Buffer.isBuffer(emailData)) {
|
|
785
|
+
// Parse the email data buffer into an Email object
|
|
786
|
+
try {
|
|
787
|
+
const parsed = await plugins.mailparser.simpleParser(emailData);
|
|
788
|
+
email = new Email({
|
|
789
|
+
from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address,
|
|
790
|
+
to: session.envelope.rcptTo[0]?.address || '',
|
|
791
|
+
subject: parsed.subject || '',
|
|
792
|
+
text: parsed.text || '',
|
|
793
|
+
html: parsed.html || undefined,
|
|
794
|
+
attachments: parsed.attachments?.map(att => ({
|
|
795
|
+
filename: att.filename || '',
|
|
796
|
+
content: att.content,
|
|
797
|
+
contentType: att.contentType
|
|
798
|
+
})) || []
|
|
799
|
+
});
|
|
800
|
+
} catch (error) {
|
|
801
|
+
logger.log('error', `Error parsing email data: ${error.message}`);
|
|
802
|
+
throw new Error(`Error parsing email data: ${error.message}`);
|
|
803
|
+
}
|
|
804
|
+
} else {
|
|
805
|
+
email = emailData;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Run inbound security verification (DKIM/SPF/DMARC) via Rust bridge
|
|
809
|
+
if (session.remoteAddress && session.remoteAddress !== '127.0.0.1') {
|
|
810
|
+
await this.verifyInboundSecurity(email, session);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// First check if this is a bounce notification email
|
|
814
|
+
const subject = email.subject || '';
|
|
815
|
+
const isBounceLike = /mail delivery|delivery (failed|status|notification)|failure notice|returned mail|undeliverable|delivery problem/i.test(subject);
|
|
816
|
+
|
|
817
|
+
if (isBounceLike) {
|
|
818
|
+
logger.log('info', `Email subject matches bounce notification pattern: "${subject}"`);
|
|
819
|
+
|
|
820
|
+
const isBounce = await this.processBounceNotification(email);
|
|
821
|
+
|
|
822
|
+
if (isBounce) {
|
|
823
|
+
logger.log('info', 'Successfully processed as bounce notification, skipping regular processing');
|
|
824
|
+
return email;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
logger.log('info', 'Not a valid bounce notification, continuing with regular processing');
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Find matching route
|
|
831
|
+
const context: IEmailContext = { email, session };
|
|
832
|
+
const route = await this.emailRouter.evaluateRoutes(context);
|
|
833
|
+
|
|
834
|
+
if (!route) {
|
|
835
|
+
throw new Error('No matching route for email');
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Store matched route in session
|
|
839
|
+
session.matchedRoute = route;
|
|
840
|
+
|
|
841
|
+
// Execute action based on route
|
|
842
|
+
await this.actionExecutor.executeAction(route.action, email, context);
|
|
843
|
+
|
|
844
|
+
// Return the processed email
|
|
845
|
+
return email;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Apply per-domain rate limits from domain configurations
|
|
850
|
+
*/
|
|
851
|
+
private applyDomainRateLimits(): void {
|
|
852
|
+
const domainConfigs = this.domainRegistry.getAllConfigs();
|
|
853
|
+
|
|
854
|
+
for (const domainConfig of domainConfigs) {
|
|
855
|
+
if (domainConfig.rateLimits) {
|
|
856
|
+
const domain = domainConfig.domain;
|
|
857
|
+
const rateLimitConfig: any = {};
|
|
858
|
+
|
|
859
|
+
if (domainConfig.rateLimits.outbound) {
|
|
860
|
+
if (domainConfig.rateLimits.outbound.messagesPerMinute) {
|
|
861
|
+
rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.outbound.messagesPerMinute;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (domainConfig.rateLimits.inbound) {
|
|
866
|
+
if (domainConfig.rateLimits.inbound.messagesPerMinute) {
|
|
867
|
+
rateLimitConfig.maxMessagesPerMinute = domainConfig.rateLimits.inbound.messagesPerMinute;
|
|
868
|
+
}
|
|
869
|
+
if (domainConfig.rateLimits.inbound.connectionsPerIp) {
|
|
870
|
+
rateLimitConfig.maxConnectionsPerIP = domainConfig.rateLimits.inbound.connectionsPerIp;
|
|
871
|
+
}
|
|
872
|
+
if (domainConfig.rateLimits.inbound.recipientsPerMessage) {
|
|
873
|
+
rateLimitConfig.maxRecipientsPerMessage = domainConfig.rateLimits.inbound.recipientsPerMessage;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (Object.keys(rateLimitConfig).length > 0) {
|
|
878
|
+
this.rateLimiter.applyDomainLimits(domain, rateLimitConfig);
|
|
879
|
+
logger.log('info', `Applied rate limits for domain ${domain}:`, rateLimitConfig);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Generate SmartProxy routes for email ports
|
|
887
|
+
*/
|
|
888
|
+
public generateProxyRoutes(portMapping?: Record<number, number>): any[] {
|
|
889
|
+
const routes: any[] = [];
|
|
890
|
+
const defaultPortMapping = {
|
|
891
|
+
25: 10025,
|
|
892
|
+
587: 10587,
|
|
893
|
+
465: 10465
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const actualPortMapping = portMapping || defaultPortMapping;
|
|
897
|
+
|
|
898
|
+
for (const externalPort of this.options.ports) {
|
|
899
|
+
const internalPort = actualPortMapping[externalPort] || externalPort + 10000;
|
|
900
|
+
|
|
901
|
+
let routeName = 'email-route';
|
|
902
|
+
let tlsMode = 'passthrough';
|
|
903
|
+
|
|
904
|
+
switch (externalPort) {
|
|
905
|
+
case 25:
|
|
906
|
+
routeName = 'smtp-route';
|
|
907
|
+
tlsMode = 'passthrough';
|
|
908
|
+
break;
|
|
909
|
+
case 587:
|
|
910
|
+
routeName = 'submission-route';
|
|
911
|
+
tlsMode = 'passthrough';
|
|
912
|
+
break;
|
|
913
|
+
case 465:
|
|
914
|
+
routeName = 'smtps-route';
|
|
915
|
+
tlsMode = 'terminate';
|
|
916
|
+
break;
|
|
917
|
+
default:
|
|
918
|
+
routeName = `email-port-${externalPort}-route`;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
routes.push({
|
|
922
|
+
name: routeName,
|
|
923
|
+
match: {
|
|
924
|
+
ports: [externalPort]
|
|
925
|
+
},
|
|
926
|
+
action: {
|
|
927
|
+
type: 'forward',
|
|
928
|
+
target: {
|
|
929
|
+
host: 'localhost',
|
|
930
|
+
port: internalPort
|
|
931
|
+
},
|
|
932
|
+
tls: {
|
|
933
|
+
mode: tlsMode
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
return routes;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Update server configuration
|
|
944
|
+
*/
|
|
945
|
+
public updateOptions(options: Partial<IUnifiedEmailServerOptions>): void {
|
|
946
|
+
const portsChanged = options.ports &&
|
|
947
|
+
(!this.options.ports ||
|
|
948
|
+
JSON.stringify(options.ports) !== JSON.stringify(this.options.ports));
|
|
949
|
+
|
|
950
|
+
if (portsChanged) {
|
|
951
|
+
this.stop().then(() => {
|
|
952
|
+
this.options = { ...this.options, ...options };
|
|
953
|
+
this.start();
|
|
954
|
+
});
|
|
955
|
+
} else {
|
|
956
|
+
this.options = { ...this.options, ...options };
|
|
957
|
+
|
|
958
|
+
if (options.domains) {
|
|
959
|
+
this.domainRegistry = new DomainRegistry(options.domains, options.defaults || this.options.defaults);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (options.routes) {
|
|
963
|
+
this.emailRouter.updateRoutes(options.routes);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/**
|
|
969
|
+
* Update email routes
|
|
970
|
+
*/
|
|
971
|
+
public updateEmailRoutes(routes: IEmailRoute[]): void {
|
|
972
|
+
this.options.routes = routes;
|
|
973
|
+
this.emailRouter.updateRoutes(routes);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
/**
|
|
977
|
+
* Get server statistics
|
|
978
|
+
*/
|
|
979
|
+
public getStats(): IServerStats {
|
|
980
|
+
return { ...this.stats };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
/**
|
|
984
|
+
* Get domain registry
|
|
985
|
+
*/
|
|
986
|
+
public getDomainRegistry(): DomainRegistry {
|
|
987
|
+
return this.domainRegistry;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
/**
|
|
991
|
+
* Send an email through the delivery system
|
|
992
|
+
*/
|
|
993
|
+
public async sendEmail(
|
|
994
|
+
email: Email,
|
|
995
|
+
mode: EmailProcessingMode = 'mta',
|
|
996
|
+
route?: IEmailRoute,
|
|
997
|
+
options?: {
|
|
998
|
+
skipSuppressionCheck?: boolean;
|
|
999
|
+
ipAddress?: string;
|
|
1000
|
+
isTransactional?: boolean;
|
|
1001
|
+
}
|
|
1002
|
+
): Promise<string> {
|
|
1003
|
+
logger.log('info', `Sending email: ${email.subject} to ${email.to.join(', ')}`);
|
|
1004
|
+
|
|
1005
|
+
try {
|
|
1006
|
+
if (!email.from) {
|
|
1007
|
+
throw new Error('Email must have a sender address');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
if (!email.to || email.to.length === 0) {
|
|
1011
|
+
throw new Error('Email must have at least one recipient');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Check if any recipients are on the suppression list
|
|
1015
|
+
if (!options?.skipSuppressionCheck) {
|
|
1016
|
+
const suppressedRecipients = email.to.filter(recipient => this.isEmailSuppressed(recipient));
|
|
1017
|
+
|
|
1018
|
+
if (suppressedRecipients.length > 0) {
|
|
1019
|
+
const originalCount = email.to.length;
|
|
1020
|
+
const suppressed = suppressedRecipients.map(recipient => {
|
|
1021
|
+
const info = this.getSuppressionInfo(recipient);
|
|
1022
|
+
return {
|
|
1023
|
+
email: recipient,
|
|
1024
|
+
reason: info?.reason || 'Unknown',
|
|
1025
|
+
until: info?.expiresAt ? new Date(info.expiresAt).toISOString() : 'permanent'
|
|
1026
|
+
};
|
|
1027
|
+
});
|
|
1028
|
+
|
|
1029
|
+
logger.log('warn', `Filtering out ${suppressedRecipients.length} suppressed recipient(s)`, { suppressed });
|
|
1030
|
+
|
|
1031
|
+
if (suppressedRecipients.length === originalCount) {
|
|
1032
|
+
throw new Error('All recipients are on the suppression list');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
email.to = email.to.filter(recipient => !this.isEmailSuppressed(recipient));
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// Sign with DKIM if configured
|
|
1040
|
+
if (mode === 'mta' && route?.action.options?.mtaOptions?.dkimSign) {
|
|
1041
|
+
const domain = email.from.split('@')[1];
|
|
1042
|
+
await this.dkimManager.handleDkimSigning(email, domain, route.action.options.mtaOptions.dkimOptions?.keySelector || 'mta');
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const id = plugins.uuid.v4();
|
|
1046
|
+
await this.deliveryQueue.enqueue(email, mode, route);
|
|
1047
|
+
|
|
1048
|
+
logger.log('info', `Email queued with ID: ${id}`);
|
|
1049
|
+
return id;
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
logger.log('error', `Failed to send email: ${error.message}`);
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// -----------------------------------------------------------------------
|
|
1057
|
+
// Bounce/suppression methods
|
|
1058
|
+
// -----------------------------------------------------------------------
|
|
1059
|
+
|
|
1060
|
+
public async processBounceNotification(bounceEmail: Email): Promise<boolean> {
|
|
1061
|
+
logger.log('info', 'Processing potential bounce notification email');
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
const bounceRecord = await this.bounceManager.processBounceEmail(bounceEmail);
|
|
1065
|
+
|
|
1066
|
+
if (bounceRecord) {
|
|
1067
|
+
logger.log('info', `Successfully processed bounce notification for ${bounceRecord.recipient}`, {
|
|
1068
|
+
bounceType: bounceRecord.bounceType,
|
|
1069
|
+
bounceCategory: bounceRecord.bounceCategory
|
|
1070
|
+
});
|
|
1071
|
+
|
|
1072
|
+
this.emit('bounceProcessed', bounceRecord);
|
|
1073
|
+
|
|
1074
|
+
SecurityLogger.getInstance().logEvent({
|
|
1075
|
+
level: SecurityLogLevel.INFO,
|
|
1076
|
+
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1077
|
+
message: `Bounce notification processed for recipient`,
|
|
1078
|
+
domain: bounceRecord.domain,
|
|
1079
|
+
details: {
|
|
1080
|
+
recipient: bounceRecord.recipient,
|
|
1081
|
+
bounceType: bounceRecord.bounceType,
|
|
1082
|
+
bounceCategory: bounceRecord.bounceCategory
|
|
1083
|
+
},
|
|
1084
|
+
success: true
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
return true;
|
|
1088
|
+
} else {
|
|
1089
|
+
logger.log('info', 'Email not recognized as a bounce notification');
|
|
1090
|
+
return false;
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
logger.log('error', `Error processing bounce notification: ${error.message}`);
|
|
1094
|
+
|
|
1095
|
+
SecurityLogger.getInstance().logEvent({
|
|
1096
|
+
level: SecurityLogLevel.ERROR,
|
|
1097
|
+
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1098
|
+
message: 'Failed to process bounce notification',
|
|
1099
|
+
details: { error: error.message, subject: bounceEmail.subject },
|
|
1100
|
+
success: false
|
|
1101
|
+
});
|
|
1102
|
+
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
public async processSmtpFailure(
|
|
1108
|
+
recipient: string,
|
|
1109
|
+
smtpResponse: string,
|
|
1110
|
+
options: { sender?: string; originalEmailId?: string; statusCode?: string; headers?: Record<string, string> } = {}
|
|
1111
|
+
): Promise<boolean> {
|
|
1112
|
+
logger.log('info', `Processing SMTP failure for ${recipient}: ${smtpResponse}`);
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
const bounceRecord = await this.bounceManager.processSmtpFailure(recipient, smtpResponse, options);
|
|
1116
|
+
|
|
1117
|
+
logger.log('info', `Successfully processed SMTP failure for ${recipient} as ${bounceRecord.bounceCategory} bounce`, {
|
|
1118
|
+
bounceType: bounceRecord.bounceType
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
this.emit('bounceProcessed', bounceRecord);
|
|
1122
|
+
|
|
1123
|
+
SecurityLogger.getInstance().logEvent({
|
|
1124
|
+
level: SecurityLogLevel.INFO,
|
|
1125
|
+
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1126
|
+
message: `SMTP failure processed for recipient`,
|
|
1127
|
+
domain: bounceRecord.domain,
|
|
1128
|
+
details: {
|
|
1129
|
+
recipient: bounceRecord.recipient,
|
|
1130
|
+
bounceType: bounceRecord.bounceType,
|
|
1131
|
+
bounceCategory: bounceRecord.bounceCategory,
|
|
1132
|
+
smtpResponse
|
|
1133
|
+
},
|
|
1134
|
+
success: true
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
return true;
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
logger.log('error', `Error processing SMTP failure: ${error.message}`);
|
|
1140
|
+
|
|
1141
|
+
SecurityLogger.getInstance().logEvent({
|
|
1142
|
+
level: SecurityLogLevel.ERROR,
|
|
1143
|
+
type: SecurityEventType.EMAIL_VALIDATION,
|
|
1144
|
+
message: 'Failed to process SMTP failure',
|
|
1145
|
+
details: { recipient, smtpResponse, error: error.message },
|
|
1146
|
+
success: false
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
return false;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
public isEmailSuppressed(email: string): boolean {
|
|
1154
|
+
return this.bounceManager.isEmailSuppressed(email);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
public getSuppressionInfo(email: string): { reason: string; timestamp: number; expiresAt?: number } | null {
|
|
1158
|
+
return this.bounceManager.getSuppressionInfo(email);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
public getBounceHistory(email: string): { lastBounce: number; count: number; type: BounceType; category: BounceCategory } | null {
|
|
1162
|
+
return this.bounceManager.getBounceInfo(email);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
public getSuppressionList(): string[] {
|
|
1166
|
+
return this.bounceManager.getSuppressionList();
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
public getHardBouncedAddresses(): string[] {
|
|
1170
|
+
return this.bounceManager.getHardBouncedAddresses();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
public addToSuppressionList(email: string, reason: string, expiresAt?: number): void {
|
|
1174
|
+
this.bounceManager.addToSuppressionList(email, reason, expiresAt);
|
|
1175
|
+
logger.log('info', `Added ${email} to suppression list: ${reason}`);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
public removeFromSuppressionList(email: string): void {
|
|
1179
|
+
this.bounceManager.removeFromSuppressionList(email);
|
|
1180
|
+
logger.log('info', `Removed ${email} from suppression list`);
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
public recordBounce(domain: string, receivingDomain: string, bounceType: 'hard' | 'soft', reason: string): void {
|
|
1184
|
+
const bounceRecord = {
|
|
1185
|
+
id: `bounce_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
|
1186
|
+
recipient: `user@${receivingDomain}`,
|
|
1187
|
+
sender: `user@${domain}`,
|
|
1188
|
+
domain: domain,
|
|
1189
|
+
bounceType: bounceType === 'hard' ? BounceType.INVALID_RECIPIENT : BounceType.TEMPORARY_FAILURE,
|
|
1190
|
+
bounceCategory: bounceType === 'hard' ? BounceCategory.HARD : BounceCategory.SOFT,
|
|
1191
|
+
timestamp: Date.now(),
|
|
1192
|
+
smtpResponse: reason,
|
|
1193
|
+
diagnosticCode: reason,
|
|
1194
|
+
statusCode: bounceType === 'hard' ? '550' : '450',
|
|
1195
|
+
processed: false
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
this.bounceManager.processBounce(bounceRecord);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* Get the rate limiter instance
|
|
1203
|
+
*/
|
|
1204
|
+
public getRateLimiter(): UnifiedRateLimiter {
|
|
1205
|
+
return this.rateLimiter;
|
|
1206
|
+
}
|
|
1207
|
+
}
|