update
This commit is contained in:
parent
073c8378c7
commit
cfea44742a
124
readme.hints.md
124
readme.hints.md
@ -452,4 +452,126 @@ External Port → SmartProxy → Internal Port → UnifiedEmailServer → Proces
|
||||
- Use single SmtpClient instance for all outbound mail
|
||||
- Simplify DcRouter to just manage high-level services
|
||||
- Add connection pooling for better performance
|
||||
- See readme.plan.md for detailed implementation plan
|
||||
- See readme.plan.md for detailed implementation plan
|
||||
|
||||
## SMTP Client Management (2025-05-27)
|
||||
|
||||
### Centralized SMTP Client in UnifiedEmailServer
|
||||
- SMTP clients are now managed centrally in UnifiedEmailServer
|
||||
- Uses connection pooling for efficiency (one pool per destination host:port)
|
||||
- Classes using UnifiedEmailServer get SMTP clients via `getSmtpClient(host, port)`
|
||||
|
||||
### Implementation Details
|
||||
```typescript
|
||||
// In UnifiedEmailServer
|
||||
private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
|
||||
|
||||
public getSmtpClient(host: string, port: number = 25): SmtpClient {
|
||||
const clientKey = `${host}:${port}`;
|
||||
let client = this.smtpClients.get(clientKey);
|
||||
|
||||
if (!client) {
|
||||
client = createPooledSmtpClient({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465,
|
||||
connectionTimeout: 30000,
|
||||
socketTimeout: 120000,
|
||||
maxConnections: 10,
|
||||
maxMessages: 1000,
|
||||
pool: true
|
||||
});
|
||||
this.smtpClients.set(clientKey, client);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
```
|
||||
|
||||
### Usage Pattern
|
||||
- EmailSendJob and DeliverySystem now use `this.emailServerRef.getSmtpClient(host, port)`
|
||||
- Connection pooling happens automatically
|
||||
- Connections are reused across multiple send jobs
|
||||
- All SMTP clients are closed when UnifiedEmailServer stops
|
||||
|
||||
### Dependency Injection Pattern
|
||||
- Classes that need UnifiedEmailServer functionality receive it as constructor argument
|
||||
- This provides access to SMTP clients, DKIM signing, and other shared functionality
|
||||
- Example: `new EmailSendJob(emailServerRef, email, options)`
|
||||
|
||||
## Email Class Design Pattern (2025-05-27)
|
||||
|
||||
### Three-Interface Pattern for Email
|
||||
The Email system uses three distinct interfaces for clarity and type safety:
|
||||
|
||||
1. **IEmailOptions** - The flexible input interface:
|
||||
```typescript
|
||||
interface IEmailOptions {
|
||||
to: string | string[]; // Flexible: single or array
|
||||
cc?: string | string[]; // Optional
|
||||
attachments?: IAttachment[]; // Optional
|
||||
skipAdvancedValidation?: boolean; // Constructor-only option
|
||||
}
|
||||
```
|
||||
- Used as constructor parameter
|
||||
- Allows flexible input formats
|
||||
- Has constructor-only options (like skipAdvancedValidation)
|
||||
|
||||
2. **INormalizedEmail** - The normalized runtime interface:
|
||||
```typescript
|
||||
interface INormalizedEmail {
|
||||
to: string[]; // Always an array
|
||||
cc: string[]; // Always an array (empty if not provided)
|
||||
attachments: IAttachment[]; // Always an array (empty if not provided)
|
||||
mightBeSpam: boolean; // Always has a value (defaults to false)
|
||||
}
|
||||
```
|
||||
- Represents the guaranteed internal structure
|
||||
- No optional arrays - everything has a default
|
||||
- Email class implements this interface
|
||||
|
||||
3. **Email class** - The implementation:
|
||||
```typescript
|
||||
export class Email implements INormalizedEmail {
|
||||
// All INormalizedEmail properties
|
||||
to: string[];
|
||||
cc: string[];
|
||||
// ... etc
|
||||
|
||||
// Additional runtime properties
|
||||
private messageId: string;
|
||||
private envelopeFrom: string;
|
||||
}
|
||||
```
|
||||
- Implements INormalizedEmail
|
||||
- Adds behavior methods and computed properties
|
||||
- Handles validation and normalization
|
||||
|
||||
### Benefits of This Pattern:
|
||||
- **Type Safety**: Email class explicitly implements INormalizedEmail
|
||||
- **Clear Contracts**: Input vs. runtime structure is explicit
|
||||
- **Flexibility**: IEmailOptions allows various input formats
|
||||
- **Consistency**: INormalizedEmail guarantees structure
|
||||
- **Validation**: Constructor validates and normalizes
|
||||
|
||||
### Usage:
|
||||
```typescript
|
||||
// Input with flexible options
|
||||
const options: IEmailOptions = {
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com', // Single string
|
||||
subject: 'Hello',
|
||||
text: 'World'
|
||||
};
|
||||
|
||||
// Creates normalized Email instance
|
||||
const email = new Email(options);
|
||||
|
||||
// email.to is guaranteed to be string[]
|
||||
email.to.forEach(recipient => {
|
||||
// No need to check if it's an array
|
||||
});
|
||||
|
||||
// Convert back to options format
|
||||
const optionsAgain = email.toEmailOptions();
|
||||
```
|
@ -25,7 +25,16 @@ export interface IEmailOptions {
|
||||
variables?: Record<string, any>; // Template variables for placeholder replacement
|
||||
}
|
||||
|
||||
/**
|
||||
* Email class represents a complete email message.
|
||||
*
|
||||
* This class takes IEmailOptions in the constructor and normalizes the data:
|
||||
* - 'to', 'cc', 'bcc' are always converted to arrays
|
||||
* - Optional properties get default values
|
||||
* - Additional properties like messageId and envelopeFrom are generated
|
||||
*/
|
||||
export class Email {
|
||||
// INormalizedEmail properties
|
||||
from: string;
|
||||
to: string[];
|
||||
cc: string[];
|
||||
@ -38,6 +47,8 @@ export class Email {
|
||||
mightBeSpam: boolean;
|
||||
priority: 'high' | 'normal' | 'low';
|
||||
variables: Record<string, any>;
|
||||
|
||||
// Additional Email-specific properties
|
||||
private envelopeFrom: string;
|
||||
private messageId: string;
|
||||
|
||||
@ -637,6 +648,57 @@ export class Email {
|
||||
return this.messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the Email instance back to IEmailOptions format.
|
||||
* Useful for serialization or passing to APIs that expect IEmailOptions.
|
||||
* Note: This loses some Email-specific properties like messageId and envelopeFrom.
|
||||
*
|
||||
* @returns IEmailOptions representation of this email
|
||||
*/
|
||||
public toEmailOptions(): IEmailOptions {
|
||||
const options: IEmailOptions = {
|
||||
from: this.from,
|
||||
to: this.to.length === 1 ? this.to[0] : this.to,
|
||||
subject: this.subject,
|
||||
text: this.text
|
||||
};
|
||||
|
||||
// Add optional properties only if they have values
|
||||
if (this.cc && this.cc.length > 0) {
|
||||
options.cc = this.cc.length === 1 ? this.cc[0] : this.cc;
|
||||
}
|
||||
|
||||
if (this.bcc && this.bcc.length > 0) {
|
||||
options.bcc = this.bcc.length === 1 ? this.bcc[0] : this.bcc;
|
||||
}
|
||||
|
||||
if (this.html) {
|
||||
options.html = this.html;
|
||||
}
|
||||
|
||||
if (this.attachments && this.attachments.length > 0) {
|
||||
options.attachments = this.attachments;
|
||||
}
|
||||
|
||||
if (this.headers && Object.keys(this.headers).length > 0) {
|
||||
options.headers = this.headers;
|
||||
}
|
||||
|
||||
if (this.mightBeSpam) {
|
||||
options.mightBeSpam = this.mightBeSpam;
|
||||
}
|
||||
|
||||
if (this.priority !== 'normal') {
|
||||
options.priority = this.priority;
|
||||
}
|
||||
|
||||
if (this.variables && Object.keys(this.variables).length > 0) {
|
||||
options.variables = this.variables;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a custom message ID
|
||||
* @param id The message ID to set
|
||||
|
@ -455,7 +455,7 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
return this.handleForwardDeliveryLegacy(item);
|
||||
}
|
||||
|
||||
// Get or create SMTP client for the target server
|
||||
// Get SMTP client from UnifiedEmailServer
|
||||
const smtpClient = this.emailServer.getSmtpClient(targetServer, targetPort);
|
||||
|
||||
// Send the email using SmtpClient
|
||||
|
@ -22,16 +22,15 @@ import type {
|
||||
} from './classes.email.config.js';
|
||||
import { Email } from '../core/classes.email.js';
|
||||
import { BounceManager, BounceType, BounceCategory } from '../core/classes.bouncemanager.js';
|
||||
import * as net from 'node:net';
|
||||
import * as tls from 'node:tls';
|
||||
import * as stream from 'node:stream';
|
||||
import { createSmtpServer } from '../delivery/smtpserver/index.js';
|
||||
import { createPooledSmtpClient } from '../delivery/smtpclient/create-client.js';
|
||||
import type { SmtpClient } from '../delivery/smtpclient/smtp-client.js';
|
||||
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
|
||||
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
|
||||
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from '../delivery/classes.unified.rate.limiter.js';
|
||||
import { SmtpState } from '../delivery/interfaces.js';
|
||||
import type { EmailProcessingMode, ISmtpSession as IBaseSmtpSession } from '../delivery/interfaces.js';
|
||||
import { smtpClientMod } from '../delivery/index.js';
|
||||
import type { DcRouter } from '../../classes.dcrouter.js';
|
||||
|
||||
/**
|
||||
@ -180,6 +179,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
public deliverySystem: MultiModeDeliverySystem;
|
||||
private rateLimiter: UnifiedRateLimiter;
|
||||
private dkimKeys: Map<string, string> = new Map(); // domain -> private key
|
||||
private smtpClients: Map<string, SmtpClient> = new Map(); // host:port -> client
|
||||
|
||||
constructor(dcRouter: DcRouter, options: IUnifiedEmailServerOptions) {
|
||||
super();
|
||||
@ -303,6 +303,37 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
// We'll create the SMTP servers during the start() method
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create an SMTP client for the given host and port
|
||||
* Uses connection pooling for efficiency
|
||||
*/
|
||||
public getSmtpClient(host: string, port: number = 25): SmtpClient {
|
||||
const clientKey = `${host}:${port}`;
|
||||
|
||||
// Check if we already have a client for this destination
|
||||
let client = this.smtpClients.get(clientKey);
|
||||
|
||||
if (!client) {
|
||||
// Create a new pooled SMTP client
|
||||
client = createPooledSmtpClient({
|
||||
host,
|
||||
port,
|
||||
secure: port === 465,
|
||||
connectionTimeout: this.options.outbound?.connectionTimeout || 30000,
|
||||
socketTimeout: this.options.outbound?.socketTimeout || 120000,
|
||||
maxConnections: this.options.outbound?.maxConnections || 10,
|
||||
maxMessages: 1000, // Messages per connection before reconnect
|
||||
pool: true,
|
||||
debug: false
|
||||
});
|
||||
|
||||
this.smtpClients.set(clientKey, client);
|
||||
logger.log('info', `Created new SMTP client pool for ${clientKey}`);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the unified email server
|
||||
*/
|
||||
@ -469,6 +500,17 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
logger.log('info', 'Email delivery queue shut down');
|
||||
}
|
||||
|
||||
// Close all SMTP client connections
|
||||
for (const [clientKey, client] of this.smtpClients) {
|
||||
try {
|
||||
await client.close();
|
||||
logger.log('info', `Closed SMTP client pool for ${clientKey}`);
|
||||
} catch (error) {
|
||||
logger.log('warn', `Error closing SMTP client for ${clientKey}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
this.smtpClients.clear();
|
||||
|
||||
logger.log('info', 'UnifiedEmailServer stopped successfully');
|
||||
this.emit('stopped');
|
||||
} catch (error) {
|
||||
@ -480,89 +522,6 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Handle incoming email data (stub implementation)
|
||||
*/
|
||||
private onData(stream: stream.Readable, session: IExtendedSmtpSession, callback: (err?: Error) => void): void {
|
||||
logger.log('info', `Processing email data for session ${session.id}`);
|
||||
|
||||
const startTime = Date.now();
|
||||
const chunks: Buffer[] = [];
|
||||
|
||||
stream.on('data', (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
stream.on('end', async () => {
|
||||
try {
|
||||
const data = Buffer.concat(chunks);
|
||||
const mode = session.processingMode || this.options.defaultMode;
|
||||
|
||||
// Determine processing mode based on matched rule
|
||||
const processedEmail = await this.processEmailByMode(data, session, mode);
|
||||
|
||||
// Update statistics
|
||||
this.stats.messages.processed++;
|
||||
this.stats.messages.delivered++;
|
||||
|
||||
// Calculate processing time
|
||||
const processingTime = Date.now() - startTime;
|
||||
this.processingTimes.push(processingTime);
|
||||
this.updateProcessingTimeStats();
|
||||
|
||||
// Emit event for delivery queue
|
||||
this.emit('emailProcessed', processedEmail, mode, session.matchedRule);
|
||||
|
||||
logger.log('info', `Email processed successfully in ${processingTime}ms, mode: ${mode}`);
|
||||
callback();
|
||||
} catch (error) {
|
||||
logger.log('error', `Error processing email: ${error.message}`);
|
||||
|
||||
// Update statistics
|
||||
this.stats.messages.processed++;
|
||||
this.stats.messages.failed++;
|
||||
|
||||
// Calculate processing time for failed attempts too
|
||||
const processingTime = Date.now() - startTime;
|
||||
this.processingTimes.push(processingTime);
|
||||
this.updateProcessingTimeStats();
|
||||
|
||||
SecurityLogger.getInstance().logEvent({
|
||||
level: SecurityLogLevel.ERROR,
|
||||
type: SecurityEventType.EMAIL_PROCESSING,
|
||||
message: 'Email processing failed',
|
||||
ipAddress: session.remoteAddress,
|
||||
details: {
|
||||
error: error.message,
|
||||
sessionId: session.id,
|
||||
mode: session.processingMode,
|
||||
processingTime
|
||||
},
|
||||
success: false
|
||||
});
|
||||
|
||||
callback(error);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
logger.log('error', `Stream error: ${err.message}`);
|
||||
|
||||
SecurityLogger.getInstance().logEvent({
|
||||
level: SecurityLogLevel.ERROR,
|
||||
type: SecurityEventType.EMAIL_PROCESSING,
|
||||
message: 'Email stream error',
|
||||
ipAddress: session.remoteAddress,
|
||||
details: {
|
||||
error: err.message,
|
||||
sessionId: session.id
|
||||
},
|
||||
success: false
|
||||
});
|
||||
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update processing time statistics
|
||||
@ -636,8 +595,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
// Process based on mode
|
||||
switch (mode) {
|
||||
case 'forward':
|
||||
await this.handleForwardMode(email, session);
|
||||
break;
|
||||
throw new Error('Forward mode is not implemented');
|
||||
|
||||
case 'mta':
|
||||
await this.handleMtaMode(email, session);
|
||||
@ -655,99 +613,6 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
return email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle email in forward mode (SMTP proxy)
|
||||
*/
|
||||
private async handleForwardMode(email: Email, session: IExtendedSmtpSession): Promise<void> {
|
||||
logger.log('info', `Handling email in forward mode for session ${session.id}`);
|
||||
|
||||
// Get target server information
|
||||
const rule = session.matchedRule;
|
||||
const targetServer = rule?.target?.server || this.options.defaultServer;
|
||||
const targetPort = rule?.target?.port || this.options.defaultPort || 25;
|
||||
const useTls = rule?.target?.useTls ?? this.options.defaultTls ?? false;
|
||||
|
||||
if (!targetServer) {
|
||||
throw new Error('No target server configured for forward mode');
|
||||
}
|
||||
|
||||
logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`);
|
||||
|
||||
try {
|
||||
// Create a simple SMTP client connection to the target server
|
||||
const client = new net.Socket();
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
// Connect to the target server
|
||||
client.connect({
|
||||
host: targetServer,
|
||||
port: targetPort
|
||||
});
|
||||
|
||||
client.on('data', (data) => {
|
||||
const response = data.toString().trim();
|
||||
logger.log('debug', `SMTP response: ${response}`);
|
||||
|
||||
// Handle SMTP response codes
|
||||
if (response.startsWith('2')) {
|
||||
// Success response
|
||||
resolve();
|
||||
} else if (response.startsWith('5')) {
|
||||
// Permanent error
|
||||
reject(new Error(`SMTP error: ${response}`));
|
||||
}
|
||||
});
|
||||
|
||||
client.on('error', (err) => {
|
||||
logger.log('error', `SMTP client error: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// SMTP client commands would go here in a full implementation
|
||||
// For now, just finish the connection
|
||||
client.end();
|
||||
resolve();
|
||||
});
|
||||
|
||||
logger.log('info', `Email forwarded successfully to ${targetServer}:${targetPort}`);
|
||||
|
||||
SecurityLogger.getInstance().logEvent({
|
||||
level: SecurityLogLevel.INFO,
|
||||
type: SecurityEventType.EMAIL_FORWARDING,
|
||||
message: 'Email forwarded',
|
||||
ipAddress: session.remoteAddress,
|
||||
details: {
|
||||
sessionId: session.id,
|
||||
targetServer,
|
||||
targetPort,
|
||||
useTls,
|
||||
ruleName: rule?.pattern || 'default',
|
||||
subject: email.subject
|
||||
},
|
||||
success: true
|
||||
});
|
||||
} catch (error) {
|
||||
logger.log('error', `Failed to forward email: ${error.message}`);
|
||||
|
||||
SecurityLogger.getInstance().logEvent({
|
||||
level: SecurityLogLevel.ERROR,
|
||||
type: SecurityEventType.EMAIL_FORWARDING,
|
||||
message: 'Email forwarding failed',
|
||||
ipAddress: session.remoteAddress,
|
||||
details: {
|
||||
sessionId: session.id,
|
||||
targetServer,
|
||||
targetPort,
|
||||
useTls,
|
||||
ruleName: rule?.pattern || 'default',
|
||||
error: error.message
|
||||
},
|
||||
success: false
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle email in MTA mode (programmatic processing)
|
||||
@ -948,24 +813,7 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle server errors
|
||||
*/
|
||||
private onError(err: Error): void {
|
||||
logger.log('error', `Server error: ${err.message}`);
|
||||
this.emit('error', err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle server close
|
||||
*/
|
||||
private onClose(): void {
|
||||
logger.log('info', 'Server closed');
|
||||
this.emit('close');
|
||||
|
||||
// Update statistics
|
||||
this.stats.connections.current = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up automatic DKIM configuration with DNS server
|
||||
@ -1025,25 +873,6 @@ export class UnifiedEmailServer extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get SMTP client for a specific destination
|
||||
*/
|
||||
public getSmtpClient(host: string, port: number): smtpClientMod.SmtpClient {
|
||||
const key = `${host}:${port}`;
|
||||
|
||||
if (!this.smtpClients.has(key)) {
|
||||
// Create a new client for this destination
|
||||
const client = smtpClientMod.createSmtpClient({
|
||||
...this.smtpClientConfig,
|
||||
host,
|
||||
port
|
||||
});
|
||||
|
||||
this.smtpClients.set(key, client);
|
||||
}
|
||||
|
||||
return this.smtpClients.get(key)!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate SmartProxy routes for email ports
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user