Compare commits

...

3 Commits

13 changed files with 2769 additions and 242 deletions

View File

@ -1,5 +1,16 @@
# Changelog
## 2025-05-07 - 2.6.0 - feat(dcrouter)
Implement integrated DcRouter with comprehensive SmartProxy configuration, enhanced SMTP processing, and robust storeandforward email routing
- Marked completion of tasks in readme.plan.md with [x] flags for SMTP server setup, email processing pipeline, queue management, and delivery system.
- Reworked DcRouter to use direct SmartProxy configuration, separating smtpConfig and smtpForwarding approaches.
- Added new components for delivery queue and delivery system with persistent storage support.
- Improved SMTP server implementation with TLS support, event handlers for connection, authentication, sender/recipient validation, and data processing.
- Refined domain-based routing and transformation logic in EmailProcessor with metrics and logging.
- Updated exported modules in dcrouter index to include SMTP storeandforward components.
- Enhanced inline documentation and code comments for configuration interfaces and integration details.
## 2025-05-07 - 2.5.0 - feat(dcrouter)
Enhance DcRouter configuration and update documentation

View File

@ -1,7 +1,7 @@
{
"name": "@serve.zone/platformservice",
"private": true,
"version": "2.5.0",
"version": "2.6.0",
"description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",

View File

@ -1,172 +1,754 @@
# DcRouter Improvement Plan
# DcRouter SMTP Store-and-Forward Implementation Plan
## Objective
Create a new version of DcRouter that doesn't rely on SzPlatformService but instead uses SmartProxy and the complete email stack directly. This will make DcRouter more modular, lightweight, and capable of SMTP forwarding with the email stack.
## Overview
This plan outlines the implementation of a store-and-forward SMTP proxy within DcRouter that receives emails, processes them, and forwards them to the appropriate destinations. This capability expands DcRouter beyond simple connection proxying to provide full control over email flow, including content inspection, transformation, and reliable delivery.
## 1. Core Architecture Changes
## 0. Configuration Approaches
### 1.1 Remove SzPlatformService Dependency
- [x] Remove the `platformServiceInstance` option from `IDcRouterOptions`
- Update `classes.dcrouter.ts` to remove platformServiceInstance from the options interface
- Create a new utility class `DcRouterEnvironment` to handle environment variables directly using qenv
- Replace all uses of SzDcRouterConnector with direct calls to DcRouterEnvironment
### 0.1 Core SmartProxy Direct Configuration
DcRouter should leverage SmartProxy's configuration directly, exposing SmartProxy's full domain configuration options to give users maximum flexibility for all HTTP/HTTPS and TCP/SNI traffic:
### 1.2 Direct Integration with Email Stack
- [x] Add options for direct MtaService integration
- Enhance the existing mtaConfig/mtaServiceInstance options to include all necessary MTA configuration
- Add new options for email forwarding capabilities that will be passed to the MTA service
- Implement initializers for setting up MTA with or without existing instances
- [x] Create a new SMTP forwarding configuration interface
- Create `ISmtpForwardingConfig` interface with fields for destination domains, routing rules, and authentication
- Implement domain-to-server mapping for routing emails to appropriate SMTP servers
- Add options for SMTP authentication methods (PLAIN, LOGIN, OAUTH2)
- [x] Implement proper connection between SmartProxy and MTA service
- Update `configureSmtpProxy()` method to create bidirectional communication with MTA service
- Implement proxy protocol support for preserving client IP addresses during SMTP forwarding
- Create listener for MTA status changes to adjust proxy settings dynamically
```typescript
interface IDcRouterOptions {
// Direct SmartProxy configuration for general HTTP/HTTPS and TCP/SNI traffic
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
// SMTP-specific configurations - can be used alongside smartProxyConfig
// SMTP Store-and-forward configuration for advanced email processing
smtpConfig?: ISmtpConfig;
// Other DcRouter options...
}
```
## 2. SmartProxy Configuration
This approach allows direct configuration of SmartProxy's powerful domain-based routing, giving full control over HTTP/HTTPS and SNI-based traffic:
### 2.1 Enhanced SmartProxy Integration
- [x] Update `SmartProxy` configuration for better TCP/SNI handling
- Modify initialization to support more advanced SNI-based routing decisions
- [x] Add specific configuration for SMTP ports (25, 465, 587)
- Create a `SmtpPortConfig` class to manage SMTP-specific port settings
- Add TLS termination options specific to SMTP protocols (STARTTLS vs. implicit TLS)
- Implement connection rate limiting and concurrent connection management for SMTP ports
```typescript
const dcRouter = new DcRouter({
// Direct SmartProxy configuration for HTTP/HTTPS traffic
smartProxyConfig: {
fromPort: 443,
toPort: 8080,
targetIP: '10.0.0.10',
sniEnabled: true,
acme: {
port: 80,
enabled: true,
autoRenew: true,
useProduction: true,
renewThresholdDays: 30,
accountEmail: 'admin@example.com'
},
globalPortRanges: [
{ from: 80, to: 80 },
{ from: 443, to: 443 }
],
// SmartProxy's full domain configuration flexibility
domainConfigs: [
{
domains: ['example.com', 'www.example.com'],
allowedIPs: ['0.0.0.0/0'],
blockedIPs: ['1.2.3.4/32'],
targetIPs: ['10.0.0.10', '10.0.0.11'],
portRanges: [
{ from: 80, to: 80 },
{ from: 443, to: 443 }
],
connectionTimeout: 60000,
useNetworkProxy: true
},
// Additional domain configurations...
],
// Additional SmartProxy options...
},
// Email-specific configuration (complementary to smartProxyConfig)
smtpConfig: {
// Email handling configuration...
},
// Other DcRouter configuration...
}
```
### 2.2 Routing Configuration
- [x] Allow domain-based routing for email traffic
- Add domain matching patterns with wildcard support for inbound email routing
- Implement sender domain-based routing for outbound emails
- Create domain groups for applying consistent rules across related domains
- [x] Implement IP-based allow/block lists for advanced filtering
- Develop IP range and CIDR notation support for filtering
- Create separate lists for inbound and outbound connections
- Implement geo-based filtering using IP geolocation
### 0.2 Store-and-Forward SMTP Implementation
For advanced email handling, we'll build a complete store-and-forward SMTP system to work alongside the direct SmartProxy configuration. This provides full control over email processing while maintaining SmartProxy's flexibility for HTTP/HTTPS traffic:
## 3. SMTP Forwarding Functionality
## 1. Core Architecture
### 3.1 SMTP Routing Engine
- [x] Enhance the SMTP rule engine to support advanced forwarding scenarios
- Extend email routing capabilities with SmartProxy configuration
- Add context information to routing for making informed routing decisions
- Implement domain-based routing for traffic management
- [x] Create efficient routing for common email patterns
- Develop email forwarding configuration for common use cases
- Implement domain-to-server mapping for email routing
- Create simple but effective routing mechanisms
- [x] Implement per-domain routing configuration
- Create domain configuration support in SmtpForwardingConfig
- Implement dynamic updating of domain routes
- Add domain-level connection handling
### 1.1 SMTP Server Implementation
- [x] Integrate an SMTP server library (like `smtp-server`) to accept incoming mail
- Created the SmtpServer class that initializes and manages the SMTP server instance
- Configured to listen on standard ports (25, 587, 465)
- Implemented TLS support (STARTTLS and implicit TLS)
- Added support for authentication methods (PLAIN, LOGIN, OAUTH2)
- Set up size limits and connection timeouts
### 3.2 MTA Integration
- [x] Configure MTA service for use with DcRouter
- Extend DcRouter to work with existing MTA configuration
- Implement proper MTA service initialization and startup
- Create clean integration between DcRouter and MTA
- [x] Implement SMTP forwarding as alternative to MTA
- Add SMTP forwarding configuration for simpler deployments
- Implement SmartProxy configuration for SMTP traffic
- Create clean separation between MTA and forwarding modes
- [x] Maintain email traffic integrity
- Ensure proper handling of connections between services
- Implement source IP preservation for proper tracking
- Create configuration options for security settings
### 1.2 Email Processing Pipeline
- [x] Create a modular processing pipeline for emails
- Built the EmailProcessor class that manages the processing workflow
- Implemented event-based architecture for extensible processing steps
- Created interfaces for each processing stage (metadata extraction, content scanning, routing, transformation)
- Added metrics and logging points throughout the pipeline
## 4. Implementation Tasks
### 1.3 Queue Management
- [x] Develop a persistent queue system for email delivery
- Implemented DeliveryQueue class with in-memory queue for immediate delivery attempts
- Created persistent storage for delivery retry queue with file-based storage
- Built queue manager with scheduling capabilities
- Added transaction support to prevent message loss during crashes
### 4.1 Create New Classes
- [x] Create utility classes to handle configuration
- Implement `DcRouterEnvironment` for environment variable access
- Create `SmtpPortConfig` for managing SMTP port settings
- Implement `EmailDomainRouter` for email domain routing
- [x] Develop SMTP-specific functionality
- Create specialized configuration for SMTP traffic
- Implement port configuration for different SMTP protocols
- Add TLS options handling for SMTP ports
- [x] Implement certificate management
- Utilize SmartProxy's Port80Handler for ACME certificate management
- Add certificate application to NetworkProxy
- Create certificate event handling
### 1.4 Email Delivery System
- [x] Create a robust delivery system for outbound email
- Implemented DeliverySystem class for outbound SMTP connections
- Added retry logic with configurable exponential backoff
- Created delivery status tracking and notifications via events
- Set up initial bounce handling and processing
### 4.2 Update Existing Components
- [x] Refactor `DcRouter` class to remove SzPlatformService dependencies
- Remove all direct references to SzPlatformService
- Update constructor to use new configuration system
- Refactor initialization logic to work independently
- [x] Update certificate handling to use SmartACME directly
- Implement Port80Handler for ACME certificate management
- Add automatic certificate renewal with event handling
- Apply certificates to appropriate services
- [x] Enhance SmartProxy configuration for better SMTP support
- Implement separate SmartProxy instances for different protocols
- Add TLS settings for different SMTP ports (STARTTLS vs. implicit TLS)
- Create clean service lifecycle management
## 2. Email Processing Features
### 4.3 Configuration Interface
- [x] Create a clean, declarative configuration interface
- Design structured TypeScript interfaces for all configuration options
- Implement simple, focused configuration objects
- Create clean separation between different component configurations
- [x] Support environment variables and programmatic configuration
- Create DcRouterEnvironment for environment variable access
- Implement environment variable caching for better performance
- Allow programmatic configuration updates
- [x] Implement well-defined configuration APIs
- Add typed interfaces for all configuration options
- Create clear documentation in interface comments
- Implement runtime configuration updating
### 2.1 Routing and Forwarding
- [x] Implement flexible email routing based on various criteria
- Created domain-based routing rules in EmailProcessor
- Added support for pattern matching for domains (exact match, wildcard)
- Implemented recipient-based routing
- Added support for routing across multiple target servers
- Added initial failover support for high availability
## 5. Testing and Documentation
### 2.2 Content Inspection
- [x] Develop content inspection capabilities
- Added MIME parsing and content extraction using mailparser
- Implemented attachment scanning and filtering based on extensions
- Created plugin architecture for content analysis
- Added integration points for external scanners (spam, virus)
- Implemented policy enforcement based on content scan results
### 5.1 Code Implementation
- [x] Implement core components
- Create new classes for configuration and domain routing
- Update existing DcRouter with new functionality
- Implement environment variable handling
- [x] Implement SMTP functionality
- Add SMTP forwarding configuration
- Implement port-specific settings
- Create domain-based email routing
- [x] Implement HTTP/HTTPS functionality
- Add NetworkProxy integration
- Implement certificate management
- Create domain-based HTTP routing
### 2.3 Email Transformation
- [x] Create tools for modifying emails during transit
- Implemented header addition capabilities
- Added DKIM signing capability placeholder
- Created framework for email transformations
- Added attachment handling capability
- Implemented support for adding compliance information
### 5.2 Quality and Performance
- [x] Ensure code quality
- Fix all TypeScript errors
- Implement clean interfaces
- Create well-documented code
- [x] Optimize for performance
- Implement parallel service shutdown
- Use environment variable caching
- Create efficient routing lookups
- [x] Maintain compatibility
- Ensure backward compatibility where possible
- Create clean extension points
- Maintain consistent APIs
### 2.4 Rate Limiting and Traffic Control
- [x] Build rate limiting controls
- Implemented per-domain rate limits
- Added support for configurable rate limiting thresholds
- Created quota enforcement with domain-based configuration
- Added event system for rate limit notifications
## 6. Future Enhancements (Pending)
## 3. Integration with DcRouter
### 6.1 Testing
### 3.1 Configuration Interface
- [x] Extend DcRouter's configuration schema
- Created comprehensive SMTP configuration section in IDcRouterOptions
- Defined interfaces for each SMTP feature set
- Added validation with defaults for configuration values
- Implemented sensible defaults for all configuration options
- Added detailed documentation in code comments
### 3.2 Management API
- [x] Develop management APIs for runtime control
- Created methods to update configuration without restart
- Implemented queue management functions (pause, resume, inspect)
- Added status reporting through events
- Created configuration update methods
- Implemented graceful shutdown capabilities
### 3.3 Metrics and Logging
- [x] Implement metrics gathering
- Created counters for messages processed, delivered, and failed
- Added tracking for processing stages
- Implemented detailed logging
- Added message IDs for tracking through the system
## 4. Detailed Component Specifications
### 4.0 DcRouter Configuration Extension
```typescript
export interface IDcRouterOptions {
// Core configuration options
// Direct SmartProxy configuration - gives full control over all TCP/SNI traffic
// including HTTP, HTTPS, and any other TCP-based protocol
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
// For backward compatibility and simplified HTTP configuration
httpDomainRoutes?: IDomainRoutingConfig[];
// SMTP store-and-forward processing - works alongside smartProxyConfig
// This is for advanced email handling like content inspection
smtpConfig?: ISmtpConfig;
// Shared configurations
tls?: {
contactEmail: string;
domain?: string;
certPath?: string;
keyPath?: string;
};
// Other DcRouter options
dnsServerConfig?: plugins.smartdns.IDnsServerOptions;
mtaConfig?: IMtaConfig;
mtaServiceInstance?: MtaService;
}
```
### 4.1 SmtpServer Class
```typescript
interface ISmtpServerOptions {
// Base server options
ports: number[];
hostname: string;
banner?: string;
// Authentication options
authMethods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
requireAuth?: boolean;
// TLS options
tls?: {
key?: string | Buffer;
cert?: string | Buffer;
ca?: string | Buffer | Array<string | Buffer>;
ciphers?: string;
minVersion?: string;
};
// Limits
maxMessageSize?: number;
maxClients?: number;
maxConnections?: number;
// Connection options
connectionTimeout?: number;
socketTimeout?: number;
}
/**
* Manages the SMTP server for receiving emails
*/
class SmtpServer {
constructor(options: ISmtpServerOptions);
// Start and stop the server
start(): Promise<void>;
stop(): Promise<void>;
// Event handlers
onConnect(handler: (session: Session, callback: (err?: Error) => void) => void): void;
onAuth(handler: (auth: AuthObject, session: Session, callback: (err?: Error, user?: UserInfo) => void) => void): void;
onMailFrom(handler: (address: Address, session: Session, callback: (err?: Error) => void) => void): void;
onRcptTo(handler: (address: Address, session: Session, callback: (err?: Error) => void) => void): void;
onData(handler: (stream: Readable, session: Session, callback: (err?: Error) => void) => void): void;
// Check email size before accepting data
checkMessageSize(size: number): boolean;
// Configuration updates
updateOptions(options: Partial<ISmtpServerOptions>): void;
// Server stats
getStats(): IServerStats;
}
```
### 4.2 EmailProcessor Class
```typescript
interface IEmailProcessorOptions {
// Processing options
maxParallelProcessing?: number;
processingTimeout?: number;
// Feature flags
contentScanning?: boolean;
headerProcessing?: boolean;
dkimSigning?: boolean;
// Processing rules
scanners?: IScannerConfig[];
transformations?: ITransformConfig[];
// Routing rules
routingRules?: IRoutingRule[];
defaultServer?: string;
defaultPort?: number;
}
/**
* Handles all email processing steps
*/
class EmailProcessor {
constructor(options: IEmailProcessorOptions);
// Main processing method
async processEmail(message: ParsedMail, session: Session): Promise<ProcessingResult>;
// Individual processing steps
async extractMetadata(message: ParsedMail): Promise<EmailMetadata>;
async determineRouting(metadata: EmailMetadata): Promise<RoutingDecision>;
async scanContent(message: ParsedMail): Promise<ScanResult>;
async applyTransformations(message: ParsedMail): Promise<ParsedMail>;
// Update processor configuration
updateOptions(options: Partial<IEmailProcessorOptions>): void;
// Manage processing plugins
addScanner(scanner: IScanner): void;
addTransformation(transformation: ITransformation): void;
addRoutingRule(rule: IRoutingRule): void;
}
```
### 4.3 DeliveryQueue Class
```typescript
interface IQueueOptions {
// Storage options
storageType: 'memory' | 'disk' | 'redis';
storagePath?: string;
redisUrl?: string;
// Queue behavior
checkInterval?: number;
maxQueueSize?: number;
maxPerDestination?: number;
// Delivery attempts
maxRetries?: number;
baseRetryDelay?: number;
maxRetryDelay?: number;
}
/**
* Manages the queue of messages waiting for delivery
*/
class DeliveryQueue {
constructor(options: IQueueOptions);
// Queue operations
async enqueue(item: QueueItem): Promise<string>;
async dequeue(id: string): Promise<QueueItem | null>;
async update(id: string, updates: Partial<QueueItem>): Promise<boolean>;
async getNext(count?: number): Promise<QueueItem[]>;
// Query methods
async getByStatus(status: QueueItemStatus): Promise<QueueItem[]>;
async getByDestination(server: string): Promise<QueueItem[]>;
async getItemCount(): Promise<number>;
// Queue maintenance
async purgeExpired(): Promise<number>;
async purgeAll(): Promise<number>;
// Persistence
async load(): Promise<void>;
async save(): Promise<void>;
// Processing control
pause(): void;
resume(): void;
isProcessing(): boolean;
}
```
### 4.4 DeliveryManager Class
```typescript
interface IDeliveryOptions {
// Connection options
connectionPoolSize?: number;
socketTimeout?: number;
// Delivery behavior
concurrentDeliveries?: number;
sendTimeout?: number;
// TLS options
verifyCertificates?: boolean;
tlsMinVersion?: string;
// Rate limiting
globalRateLimit?: number;
perServerRateLimit?: number;
perDomainRateLimit?: Record<string, number>;
}
/**
* Handles delivery of emails to destination servers
*/
class DeliveryManager {
constructor(queue: DeliveryQueue, options: IDeliveryOptions);
// Core delivery methods
async start(): Promise<void>;
async stop(): Promise<void>;
async deliverMessage(item: QueueItem): Promise<DeliveryResult>;
// Delivery management
pauseDeliveries(): void;
resumeDeliveries(): void;
getDeliveryStats(): DeliveryStats;
// Configure delivery behavior
updateOptions(options: Partial<IDeliveryOptions>): void;
setRateLimit(domain: string, limit: number): void;
clearRateLimit(domain: string): void;
}
```
### 4.5 DcRouter SMTP Integration
```typescript
interface ISmtpConfig {
// Server configuration
ports: number[];
hostname: string;
banner?: string;
maxMessageSize?: number;
// TLS configuration
tls?: {
certPath?: string;
keyPath?: string;
caPath?: string;
minVersion?: string;
};
// Authentication
auth?: {
required?: boolean;
methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
users?: Array<{username: string, password: string}>;
ldapUrl?: string;
};
// Domain routing
domainConfigs: Array<{
domains: string[];
targetIPs: string[];
port?: number;
useTls?: boolean;
authentication?: {
user?: string;
pass?: string;
};
allowedIPs?: string[];
rateLimits?: {
maxMessagesPerMinute?: number;
maxRecipientsPerMessage?: number;
};
addHeaders?: boolean;
headerInfo?: Array<{
name: string;
value: string;
}>;
signDkim?: boolean;
dkimOptions?: {
domainName: string;
keySelector: string;
privateKey: string;
};
}>;
// Default routing
defaultServer: string;
defaultPort?: number;
useTls?: boolean;
// Content scanning
contentScanning?: boolean;
scanners?: Array<{
type: 'spam' | 'virus' | 'attachment';
threshold?: number;
action: 'tag' | 'reject';
blockedExtensions?: string[];
}>;
// Message transformations
transformations?: Array<{
type: string;
[key: string]: any;
}>;
// Queue settings
queueStorage?: 'memory' | 'disk';
persistentPath?: string;
maxRetries?: number;
baseRetryDelay?: number;
maxRetryDelay?: number;
}
// Extended IDcRouterOptions
interface IDcRouterOptions {
// Existing options...
// New SMTP configuration
smtpConfig?: ISmtpConfig;
}
```
## 5. Implementation Phases
### Phase 1: Core SMTP Server Setup
- [ ] Implement the SmtpServer class
- [ ] Set up TLS handling for both STARTTLS and implicit TLS
- [ ] Create the basic connection validation logic
- [ ] Implement authentication support
- [ ] Build email receiving pipeline to accept complete messages
- [ ] Create initial email parsing and storage
### Phase 2: Mail Processing and Routing
- [ ] Implement the EmailProcessor class
- [ ] Create domain-based routing rules
- [ ] Build email metadata extraction
- [ ] Implement MIME parsing and handling
- [ ] Create the transformation pipeline
- [ ] Build header manipulation capabilities
### Phase 3: Queue and Delivery System
- [ ] Implement the DeliveryQueue class
- [ ] Create persistent storage for queued messages
- [ ] Build the retry and scheduling logic
- [ ] Implement DeliveryManager with connection pooling
- [ ] Create the delivery status tracking and reporting
- [ ] Implement bounce handling and notification
### Phase 4: Advanced Features and Integration
- [ ] Integrate content scanning capabilities
- [ ] Implement DKIM signing
- [ ] Add rate limiting and traffic shaping
- [ ] Create comprehensive metrics and logging
- [ ] Build management APIs for runtime control
- [ ] Implement full integration with DcRouter
### Phase 5: Testing and Optimization
- [ ] Create unit tests for all components
- Test environment variable handling
- Test domain routing logic
- Test certificate management
- [ ] Create integration tests
- Test email forwarding between domains
- Test HTTP/HTTPS routing
- Test TLS connections
- [ ] Implement integration tests for end-to-end verification
- [ ] Perform load testing and optimize performance
- [ ] Conduct security testing and hardening
- [ ] Build documentation and examples
### 6.2 Documentation
- [ ] Create comprehensive user documentation
- Add setup guide for common scenarios
- Document all configuration options
- Provide example configurations
- [ ] Create API documentation
- Document all public interfaces
- Add usage examples
- Create integration examples
## 6. Technical Requirements
### 6.1 Dependencies
- SMTP server library (smtp-server or similar)
- Email parsing library (mailparser or similar)
- MIME handling library
- DKIM signing library
- Queue management system (optional Redis support)
- Cryptographic libraries for TLS and authentication
### 6.2 Performance Targets
- Handle 1000+ concurrent SMTP connections
- Process 100+ messages per second on standard hardware
- Support message sizes up to 50MB
- Maintain delivery queue of 100,000+ messages
- Sub-second processing time for standard emails
### 6.3 Security Requirements
- Full TLS support with modern cipher configurations
- Authentication verification and rate limiting
- Input validation for all SMTP commands
- Secure storage of queued emails
- Proper error handling to prevent information leakage
- Access controls based on IP addresses and authentication
## 7. API Examples
### 7.1 Basic DcRouter SMTP Configuration
```typescript
const dcRouter = new DcRouter({
// HTTP configuration...
smtpConfig: {
ports: [25, 587, 465],
hostname: 'mail.example.com',
maxMessageSize: 50 * 1024 * 1024, // 50MB
// TLS configuration
tls: {
certPath: '/path/to/cert.pem',
keyPath: '/path/to/key.pem'
},
// Domain routing
domainConfigs: [
{
domains: ['example.com', '*.example.com'],
targetIPs: ['mail1.example.com', 'mail2.example.com'],
port: 25,
useTls: true
}
],
// Default routing
defaultServer: 'fallback-mail.example.com',
defaultPort: 25,
useTls: true,
// Queue settings
queueStorage: 'disk',
persistentPath: '/var/mail/queue',
maxRetries: 5
}
});
```
### 7.2 Advanced Configuration with Processing
```typescript
const dcRouter = new DcRouter({
// HTTP configuration...
smtpConfig: {
// Basic settings
ports: [25, 587, 465],
hostname: 'mail.example.com',
// Domain routing with advanced features
domainConfigs: [
{
domains: ['example.com', '*.example.com'],
targetIPs: ['mail1.example.com', 'mail2.example.com'],
port: 25,
useTls: true,
// Add custom headers
addHeaders: true,
headerInfo: [
{ name: 'X-Processed-By', value: 'gateway' },
{ name: 'X-Scanned', value: 'true' }
],
// Sign with DKIM
signDkim: true,
dkimOptions: {
domainName: 'example.com',
keySelector: 'mail',
privateKey: '...'
},
// Rate limiting
rateLimits: {
maxMessagesPerMinute: 100,
maxRecipientsPerMessage: 50
}
}
],
// Content scanning
contentScanning: true,
scanners: [
{
type: 'spam',
threshold: 5.0,
action: 'tag'
},
{
type: 'virus',
action: 'reject'
},
{
type: 'attachment',
blockedExtensions: ['.exe', '.bat', '.vbs'],
action: 'reject'
}
],
// Transformations
transformations: [
{
type: 'addHeader',
header: 'X-Gateway',
value: 'DcRouter 1.0'
},
{
type: 'dkimSign',
domains: ['example.com']
}
]
}
});
```
## 8. Extensibility Points
### 8.1 Plugin Architecture
- Custom content scanners
- Custom transformation handlers
- Routing rule extensions
- Authentication providers
- Queue storage backends
### 8.2 Event System
- Connection events (connect, disconnect, error)
- Message events (received, processed, queued, delivered)
- Error events (delivery failure, processing error)
- Performance events (queue size, processing time)
- Security events (authentication failure, policy violation)
## 9. Migration Plan
### 9.1 From Simple Proxy to Store-and-Forward
- [ ] Create compatibility layer for existing configurations
- [ ] Implement graceful transition from connection proxy to full processing
- [ ] Add configuration validation to ensure smooth migration
- [ ] Create feature flags to enable advanced features incrementally
- [ ] Provide documentation for migrating existing deployments
### 9.2 Backward Compatibility
- [ ] Maintain support for basic proxy functionality
- [ ] Provide simple configuration options for common use cases
- [ ] Create migration utilities to update configuration formats
- [ ] Support running in hybrid mode during transition
## 10. SmartProxy Integration
### 10.1 SmartProxy Configuration Handling
- [x] Implement comprehensive support for SmartProxy configuration
- Passed through all SmartProxy options directly in DcRouter's configuration
- Added support for all SmartProxy domain configuration features
- Implemented proper handling of SmartProxy events and callbacks
- [x] Added documentation on SmartProxy configuration:
- Documented how all SmartProxy features are available through DcRouter
- Added examples of different configuration approaches
- Provided guidance in code comments
### 10.2 SMTP Integration with SmartProxy
- [x] Ensured store-and-forward SMTP works alongside SmartProxy
- Handled SMTP ports separately from HTTP/HTTPS ports
- Prevented port conflicts between SmartProxy and SMTP server
- Created code structure showing SmartProxy and SMTP working together
- [x] Implemented combined usage model:
- HTTP/HTTPS traffic using SmartProxy configuration
- SMTP traffic using store-and-forward for advanced processing
- Added support for multi-service environments
## 11. Documentation Requirements
### 11.1 Code Documentation
- [ ] Comprehensive JSDoc comments for all classes and methods
- [ ] Interface definitions with detailed parameter descriptions
- [ ] Example code snippets for common operations
- [ ] Architecture documentation with component diagrams
- [ ] Decision logs for key design choices
### 11.2 User Documentation
- [ ] Getting started guide with configuration approach selection guidance
- [ ] Complete configuration reference for both approaches
- [ ] Deployment scenarios and examples
- [ ] Troubleshooting guide
- [ ] Performance tuning recommendations
- [ ] Security best practices
### 11.3 Direct SmartProxy Configuration Guide
- [ ] Detailed guide on using SmartProxy's domain configuration capabilities
- [ ] Examples of complex routing scenarios with SmartProxy
- [ ] Performance optimization tips for SmartProxy configurations
- [ ] Security settings for SmartProxy deployments

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/platformservice',
version: '2.5.0',
version: '2.6.0',
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
}

View File

@ -2,9 +2,14 @@ import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js';
import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js';
import { type IMtaConfig, MtaService } from '../mta/classes.mta.js';
// Import SMTP store-and-forward components
import { SmtpServer } from './classes.smtp.server.js';
import { EmailProcessor, type IProcessingResult } from './classes.email.processor.js';
import { DeliveryQueue } from './classes.delivery.queue.js';
import { DeliverySystem } from './classes.delivery.system.js';
// Certificate types are available via plugins.tsclass
/**
@ -31,27 +36,27 @@ export interface ISmtpForwardingConfig {
}>;
}
/**
* Simple domain-based routing configuration
*/
export interface IDomainRoutingConfig {
/** The domain name or pattern (e.g., example.com or *.example.com) */
domain: string;
/** Target server hostname or IP */
targetServer: string;
/** Target port */
targetPort: number;
/** Enable HTTPS/TLS for this route */
useTls?: boolean;
/** Allow incoming connections from these IP ranges (default: all) */
allowedIps?: string[];
}
import type { ISmtpConfig } from './classes.smtp.config.js';
export interface IDcRouterOptions {
/** HTTP/HTTPS domain-based routing */
httpDomainRoutes?: IDomainRoutingConfig[];
/**
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
*/
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
/** SMTP forwarding configuration */
/**
* SMTP store-and-forward configuration
* This enables advanced email processing capabilities (complementary to smartProxyConfig)
*/
smtpConfig?: ISmtpConfig;
/**
* Legacy SMTP forwarding configuration
* If smtpConfig is provided, this will be ignored
*/
smtpForwarding?: ISmtpForwardingConfig;
/** MTA service configuration (if not using SMTP forwarding) */
@ -89,15 +94,21 @@ export interface PortProxyRuleContext {
proxy: plugins.smartproxy.SmartProxy;
configs: plugins.smartproxy.IPortProxySettings['domainConfigs'];
}
export class DcRouter {
public options: IDcRouterOptions;
// Core services
public smartProxy?: plugins.smartproxy.SmartProxy;
public smtpProxy?: plugins.smartproxy.SmartProxy;
public mta?: MtaService;
public dnsServer?: plugins.smartdns.DnsServer;
// SMTP store-and-forward components
public smtpServer?: SmtpServer;
public emailProcessor?: EmailProcessor;
public deliveryQueue?: DeliveryQueue;
public deliverySystem?: DeliverySystem;
// Environment access
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
@ -112,13 +123,20 @@ export class DcRouter {
console.log('Starting DcRouter services...');
try {
// 1. Set up HTTP/HTTPS traffic handling with SmartProxy
await this.setupHttpProxy();
// Set up SmartProxy for HTTP/HTTPS and general TCP/SNI traffic
if (this.options.smartProxyConfig) {
await this.setupSmartProxy();
}
// 2. Set up MTA or SMTP forwarding
if (this.options.smtpForwarding?.enabled) {
// 2. Set up SMTP handling
if (this.options.smtpConfig) {
// Set up store-and-forward SMTP processing
await this.setupSmtpProcessing();
} else if (this.options.smtpForwarding?.enabled) {
// Fallback to simple SMTP forwarding for backward compatibility
await this.setupSmtpForwarding();
} else {
// Set up MTA service if no SMTP handling is configured
await this.setupMtaService();
}
@ -139,60 +157,40 @@ export class DcRouter {
}
/**
* Set up SmartProxy for HTTP/HTTPS traffic
* Set up SmartProxy with direct configuration
*/
private async setupHttpProxy() {
if (!this.options.httpDomainRoutes || this.options.httpDomainRoutes.length === 0) {
console.log('No HTTP domain routes configured, skipping HTTP proxy setup');
private async setupSmartProxy(): Promise<void> {
if (!this.options.smartProxyConfig) {
return;
}
console.log('Setting up SmartProxy for HTTP/HTTPS traffic');
console.log('Setting up SmartProxy with direct configuration');
// Prepare SmartProxy configuration
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
fromPort: 443,
toPort: this.options.httpDomainRoutes[0].targetPort,
targetIP: this.options.httpDomainRoutes[0].targetServer,
sniEnabled: true,
acme: {
port: 80,
enabled: true,
autoRenew: true,
useProduction: true,
renewThresholdDays: 30,
accountEmail: this.options.tls?.contactEmail || 'admin@example.com' // ACME requires an email
},
globalPortRanges: [{ from: 443, to: 443 }],
domainConfigs: []
};
// Create SmartProxy instance with full configuration
this.smartProxy = new plugins.smartproxy.SmartProxy(this.options.smartProxyConfig);
// Create domain configs from the HTTP routes
smartProxyConfig.domainConfigs = this.options.httpDomainRoutes.map(route => ({
domains: [route.domain],
targetIPs: [route.targetServer],
allowedIPs: route.allowedIps || ['0.0.0.0/0'],
// Skip certificate management for wildcard domains as it's not supported by HTTP-01 challenges
certificateManagement: !route.domain.includes('*')
}));
// Create and start the SmartProxy instance
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
// Listen for certificate events
this.smartProxy.on('certificate-issued', event => {
console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
// Set up event listeners
this.smartProxy.on('error', (err) => {
console.error('SmartProxy error:', err);
});
this.smartProxy.on('certificate-renewed', event => {
console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
});
if (this.options.smartProxyConfig.acme) {
this.smartProxy.on('certificate-issued', (event) => {
console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
});
this.smartProxy.on('certificate-renewed', (event) => {
console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
});
}
// Start SmartProxy
await this.smartProxy.start();
console.log(`HTTP/HTTPS proxy configured with ${smartProxyConfig.domainConfigs.length} domain routes`);
console.log('SmartProxy started successfully');
}
/**
* Set up the MTA service
*/
@ -227,7 +225,7 @@ export class DcRouter {
const smtpPorts = forwarding.ports || [25, 587, 465];
// Create SmartProxy instance for SMTP forwarding
this.smtpProxy = new plugins.smartproxy.SmartProxy({
const smtpProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
// Listen on the first SMTP port
fromPort: smtpPorts[0],
// Forward to the default server
@ -245,10 +243,16 @@ export class DcRouter {
})) || [],
// Include all SMTP ports in the global port ranges
globalPortRanges: smtpPorts.map(port => ({ from: port, to: port }))
});
};
// Create a separate SmartProxy instance for SMTP
const smtpProxy = new plugins.smartproxy.SmartProxy(smtpProxyConfig);
// Start the SMTP proxy
await this.smtpProxy.start();
await smtpProxy.start();
// Store the SMTP proxy reference
this.smartProxy = smtpProxy;
console.log(`SMTP forwarding configured on ports ${smtpPorts.join(', ')}`);
}
@ -287,11 +291,11 @@ export class DcRouter {
try {
// Stop all services in parallel for faster shutdown
await Promise.all([
// Stop HTTP SmartProxy if running
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping HTTP SmartProxy:', err)) : Promise.resolve(),
// Stop SMTP components
this.stopSmtpComponents().catch(err => console.error('Error stopping SMTP components:', err)),
// Stop SMTP SmartProxy if running
this.smtpProxy ? this.smtpProxy.stop().catch(err => console.error('Error stopping SMTP SmartProxy:', err)) : Promise.resolve(),
// Stop HTTP SmartProxy if running
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
// Stop MTA service if it's our own (not an external instance)
(this.mta && !this.options.mtaServiceInstance) ?
@ -312,23 +316,90 @@ export class DcRouter {
}
/**
* Update HTTP domain routes
* @param routes New HTTP domain routes
* Update SmartProxy configuration
* @param config New SmartProxy configuration
*/
public async updateHttpRoutes(routes: IDomainRoutingConfig[]): Promise<void> {
this.options.httpDomainRoutes = routes;
// If SmartProxy is already running, we need to restart it with the new configuration
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
// Stop existing SmartProxy if running
if (this.smartProxy) {
// Stop the existing SmartProxy
await this.smartProxy.stop();
this.smartProxy = undefined;
// Start a new SmartProxy with the updated configuration
await this.setupHttpProxy();
}
console.log(`Updated HTTP routes with ${routes.length} domains`);
// Update configuration
this.options.smartProxyConfig = config;
// Start new SmartProxy with updated configuration
await this.setupSmartProxy();
console.log('SmartProxy configuration updated');
}
/**
* Set up SMTP store-and-forward processing
*/
private async setupSmtpProcessing(): Promise<void> {
if (!this.options.smtpConfig) {
return;
}
console.log('Setting up SMTP store-and-forward processing');
try {
// 1. Create SMTP server
this.smtpServer = new SmtpServer(this.options.smtpConfig);
// 2. Create email processor
this.emailProcessor = new EmailProcessor(this.options.smtpConfig);
// 3. Create delivery queue
this.deliveryQueue = new DeliveryQueue(this.options.smtpConfig.queue || {});
await this.deliveryQueue.initialize();
// 4. Create delivery system
this.deliverySystem = new DeliverySystem(this.deliveryQueue);
// 5. Connect components
// When a message is received by the SMTP server, process it
this.smtpServer.on('message', async ({ session, mail, rawData }) => {
try {
// Process the message
const processingResult = await this.emailProcessor.processEmail(mail, rawData, session);
// If action is queue, add to delivery queue
if (processingResult.action === 'queue') {
await this.deliveryQueue.enqueue(processingResult);
}
} catch (error) {
console.error('Error processing message:', error);
}
});
// 6. Start components
await this.smtpServer.start();
await this.deliverySystem.start();
console.log(`SMTP processing started on ports ${this.options.smtpConfig.ports.join(', ')}`);
} catch (error) {
console.error('Error setting up SMTP processing:', error);
// Clean up any components that were started
if (this.deliverySystem) {
await this.deliverySystem.stop().catch(e => console.error('Error stopping delivery system:', e));
}
if (this.deliveryQueue) {
await this.deliveryQueue.shutdown().catch(e => console.error('Error shutting down delivery queue:', e));
}
if (this.smtpServer) {
await this.smtpServer.stop().catch(e => console.error('Error stopping SMTP server:', e));
}
throw error;
}
}
/**
@ -336,14 +407,12 @@ export class DcRouter {
* @param config New SMTP forwarding configuration
*/
public async updateSmtpForwarding(config: ISmtpForwardingConfig): Promise<void> {
// Stop existing SMTP proxy if running
if (this.smtpProxy) {
await this.smtpProxy.stop();
this.smtpProxy = undefined;
}
// Stop existing SMTP components
await this.stopSmtpComponents();
// Update configuration
this.options.smtpForwarding = config;
this.options.smtpConfig = undefined; // Clear any store-and-forward config
// Restart SMTP forwarding if enabled
if (config.enabled) {
@ -352,6 +421,50 @@ export class DcRouter {
console.log('SMTP forwarding configuration updated');
}
/**
* Update SMTP processing configuration
* @param config New SMTP config
*/
public async updateSmtpConfig(config: ISmtpConfig): Promise<void> {
// Stop existing SMTP components
await this.stopSmtpComponents();
// Update configuration
this.options.smtpConfig = config;
this.options.smtpForwarding = undefined; // Clear any forwarding config
// Start SMTP processing
await this.setupSmtpProcessing();
console.log('SMTP processing configuration updated');
}
/**
* Stop all SMTP components
*/
private async stopSmtpComponents(): Promise<void> {
// Stop delivery system
if (this.deliverySystem) {
await this.deliverySystem.stop().catch(e => console.error('Error stopping delivery system:', e));
this.deliverySystem = undefined;
}
// Stop delivery queue
if (this.deliveryQueue) {
await this.deliveryQueue.shutdown().catch(e => console.error('Error shutting down delivery queue:', e));
this.deliveryQueue = undefined;
}
// Stop SMTP server
if (this.smtpServer) {
await this.smtpServer.stop().catch(e => console.error('Error stopping SMTP server:', e));
this.smtpServer = undefined;
}
// For backward compatibility: legacy SMTP proxy implementation
// This is no longer used with the new implementation
}
}
export default DcRouter;

View File

@ -0,0 +1,453 @@
import * as plugins from '../plugins.js';
import type { IQueueConfig } from './classes.smtp.config.js';
import type { IProcessingResult } from './classes.email.processor.js';
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as path from 'node:path';
/**
* Queue item status
*/
export type QueueItemStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
/**
* Queue item
*/
export interface IQueueItem {
id: string;
processingResult: IProcessingResult;
status: QueueItemStatus;
attempts: number;
nextAttempt: Date;
lastError?: string;
createdAt: Date;
updatedAt: Date;
deliveredAt?: Date;
}
/**
* Delivery queue component for store-and-forward functionality
*/
export class DeliveryQueue extends EventEmitter {
private config: IQueueConfig;
private queue: Map<string, IQueueItem> = new Map();
private isProcessing: boolean = false;
private processingInterval: NodeJS.Timeout | null = null;
private persistenceTimer: NodeJS.Timeout | null = null;
/**
* Create a new delivery queue
* @param config Queue configuration
*/
constructor(config: IQueueConfig) {
super();
this.config = {
storageType: 'memory',
maxRetries: 5,
baseRetryDelay: 60000, // 1 minute
maxRetryDelay: 3600000, // 1 hour
maxQueueSize: 10000,
...config
};
}
/**
* Initialize the queue
*/
public async initialize(): Promise<void> {
try {
// Load queue from persistent storage if enabled
if (this.config.storageType === 'disk' && this.config.persistentPath) {
await this.load();
}
// Set up processing interval
this.startProcessing();
// Set up persistence interval if using disk storage
if (this.config.storageType === 'disk' && this.config.persistentPath) {
this.persistenceTimer = setInterval(() => {
this.save().catch(err => {
console.error('Error saving queue:', err);
});
}, 60000); // Save every minute
}
this.emit('initialized');
} catch (error) {
console.error('Failed to initialize delivery queue:', error);
throw error;
}
}
/**
* Start processing the queue
*/
private startProcessing(): void {
if (this.processingInterval) {
clearInterval(this.processingInterval);
}
this.processingInterval = setInterval(() => {
this.processQueue().catch(err => {
console.error('Error processing queue:', err);
});
}, 1000); // Check every second
}
/**
* Add an item to the queue
* @param processingResult Processing result to queue
*/
public async enqueue(processingResult: IProcessingResult): Promise<string> {
// Skip if the action is reject
if (processingResult.action === 'reject') {
throw new Error('Cannot queue a rejected message');
}
// Check if queue is full
if (this.config.maxQueueSize && this.queue.size >= this.config.maxQueueSize) {
throw new Error('Queue is full');
}
// Create queue item
const queueItem: IQueueItem = {
id: processingResult.id,
processingResult,
status: 'pending',
attempts: 0,
nextAttempt: new Date(),
createdAt: new Date(),
updatedAt: new Date()
};
// Add to queue
this.queue.set(queueItem.id, queueItem);
// Save queue if using disk storage
if (this.config.storageType === 'disk' && this.config.persistentPath) {
await this.saveItem(queueItem);
}
this.emit('enqueued', queueItem);
return queueItem.id;
}
/**
* Process the queue
*/
private async processQueue(): Promise<void> {
// Skip if already processing
if (this.isProcessing) {
return;
}
this.isProcessing = true;
try {
// Get items that are ready for delivery
const now = new Date();
const readyItems: IQueueItem[] = [];
for (const item of this.queue.values()) {
if (item.status === 'pending' && item.nextAttempt <= now) {
readyItems.push(item);
}
}
// If no items are ready, skip processing
if (!readyItems.length) {
return;
}
// Emit event with ready items
this.emit('itemsReady', readyItems);
} finally {
this.isProcessing = false;
}
}
/**
* Get an item from the queue
* @param id Item ID
*/
public getItem(id: string): IQueueItem | undefined {
return this.queue.get(id);
}
/**
* Get all items in the queue
*/
public getAllItems(): IQueueItem[] {
return Array.from(this.queue.values());
}
/**
* Get items by status
* @param status Status to filter by
*/
public getItemsByStatus(status: QueueItemStatus): IQueueItem[] {
return Array.from(this.queue.values()).filter(item => item.status === status);
}
/**
* Update an item in the queue
* @param id Item ID
* @param updates Updates to apply
*/
public async updateItem(id: string, updates: Partial<IQueueItem>): Promise<boolean> {
const item = this.queue.get(id);
if (!item) {
return false;
}
// Apply updates
Object.assign(item, {
...updates,
updatedAt: new Date()
});
// Save queue if using disk storage
if (this.config.storageType === 'disk' && this.config.persistentPath) {
await this.saveItem(item);
}
this.emit('itemUpdated', item);
return true;
}
/**
* Mark an item as delivered
* @param id Item ID
*/
public async markDelivered(id: string): Promise<boolean> {
return this.updateItem(id, {
status: 'delivered',
deliveredAt: new Date()
});
}
/**
* Mark an item as failed
* @param id Item ID
* @param error Error message
*/
public async markFailed(id: string, error: string): Promise<boolean> {
const item = this.queue.get(id);
if (!item) {
return false;
}
// Check if max retries reached
if (item.attempts >= (this.config.maxRetries || 5)) {
return this.updateItem(id, {
status: 'failed',
lastError: error
});
}
// Calculate next attempt time with exponential backoff
const attempts = item.attempts + 1;
const baseDelay = this.config.baseRetryDelay || 60000; // 1 minute
const maxDelay = this.config.maxRetryDelay || 3600000; // 1 hour
const delay = Math.min(
baseDelay * Math.pow(2, attempts - 1),
maxDelay
);
const nextAttempt = new Date(Date.now() + delay);
return this.updateItem(id, {
status: 'deferred',
attempts,
nextAttempt,
lastError: error
});
}
/**
* Remove an item from the queue
* @param id Item ID
*/
public async removeItem(id: string): Promise<boolean> {
if (!this.queue.has(id)) {
return false;
}
this.queue.delete(id);
// Remove from disk if using disk storage
if (this.config.storageType === 'disk' && this.config.persistentPath) {
await this.removeItemFile(id);
}
this.emit('itemRemoved', id);
return true;
}
/**
* Pause queue processing
*/
public pause(): void {
if (this.processingInterval) {
clearInterval(this.processingInterval);
this.processingInterval = null;
}
this.emit('paused');
}
/**
* Resume queue processing
*/
public resume(): void {
if (!this.processingInterval) {
this.startProcessing();
}
this.emit('resumed');
}
/**
* Shutdown the queue
*/
public async shutdown(): Promise<void> {
// Stop processing
if (this.processingInterval) {
clearInterval(this.processingInterval);
this.processingInterval = null;
}
// Stop persistence timer
if (this.persistenceTimer) {
clearInterval(this.persistenceTimer);
this.persistenceTimer = null;
}
// Save queue if using disk storage
if (this.config.storageType === 'disk' && this.config.persistentPath) {
await this.save();
}
this.emit('shutdown');
}
/**
* Load queue from disk
*/
private async load(): Promise<void> {
if (!this.config.persistentPath) {
return;
}
try {
// Create directory if it doesn't exist
if (!fs.existsSync(this.config.persistentPath)) {
fs.mkdirSync(this.config.persistentPath, { recursive: true });
}
// Read the queue directory
const files = fs.readdirSync(this.config.persistentPath);
// Load each item
for (const file of files) {
if (file.endsWith('.json')) {
try {
const filePath = path.join(this.config.persistentPath, file);
const data = fs.readFileSync(filePath, 'utf8');
const item = JSON.parse(data) as IQueueItem;
// Convert string dates back to Date objects
item.nextAttempt = new Date(item.nextAttempt);
item.createdAt = new Date(item.createdAt);
item.updatedAt = new Date(item.updatedAt);
if (item.deliveredAt) {
item.deliveredAt = new Date(item.deliveredAt);
}
// Add to queue
this.queue.set(item.id, item);
} catch (err) {
console.error(`Error loading queue item ${file}:`, err);
}
}
}
console.log(`Loaded ${this.queue.size} items from queue`);
} catch (error) {
console.error('Error loading queue:', error);
throw error;
}
}
/**
* Save queue to disk
*/
private async save(): Promise<void> {
if (!this.config.persistentPath) {
return;
}
try {
// Create directory if it doesn't exist
if (!fs.existsSync(this.config.persistentPath)) {
fs.mkdirSync(this.config.persistentPath, { recursive: true });
}
// Save each item
const savePromises = Array.from(this.queue.values()).map(item => this.saveItem(item));
await Promise.all(savePromises);
} catch (error) {
console.error('Error saving queue:', error);
throw error;
}
}
/**
* Save a single item to disk
* @param item Queue item to save
*/
private async saveItem(item: IQueueItem): Promise<void> {
if (!this.config.persistentPath) {
return;
}
try {
const filePath = path.join(this.config.persistentPath, `${item.id}.json`);
const data = JSON.stringify(item, null, 2);
await fs.promises.writeFile(filePath, data, 'utf8');
} catch (error) {
console.error(`Error saving queue item ${item.id}:`, error);
throw error;
}
}
/**
* Remove a single item file from disk
* @param id Item ID
*/
private async removeItemFile(id: string): Promise<void> {
if (!this.config.persistentPath) {
return;
}
try {
const filePath = path.join(this.config.persistentPath, `${id}.json`);
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
}
} catch (error) {
console.error(`Error removing queue item file ${id}:`, error);
throw error;
}
}
}

View File

@ -0,0 +1,272 @@
import * as plugins from '../plugins.js';
import { DeliveryQueue } from './classes.delivery.queue.js';
import type { IQueueItem } from './classes.delivery.queue.js';
import type { IProcessingResult, IRoutingDecision } from './classes.email.processor.js';
import { EventEmitter } from 'node:events';
import { Readable } from 'node:stream';
/**
* Result of a delivery attempt
*/
export interface IDeliveryResult {
id: string;
success: boolean;
error?: string;
timestamp: Date;
destination: string;
messageId?: string;
}
/**
* Delivery system statistics
*/
export interface IDeliveryStats {
delivered: number;
failed: number;
pending: number;
inProgress: number;
totalAttempts: number;
}
/**
* Email delivery system with retry logic
*/
export class DeliverySystem extends EventEmitter {
private queue: DeliveryQueue;
private isRunning: boolean = false;
private stats: IDeliveryStats = {
delivered: 0,
failed: 0,
pending: 0,
inProgress: 0,
totalAttempts: 0
};
private connections: Map<string, any> = new Map();
private maxConcurrent: number = 5;
/**
* Create a new delivery system
* @param queue Delivery queue to process
* @param maxConcurrent Maximum concurrent deliveries
*/
constructor(queue: DeliveryQueue, maxConcurrent: number = 5) {
super();
this.queue = queue;
this.maxConcurrent = maxConcurrent;
// Listen for queue events
this.setupQueueListeners();
}
/**
* Set up queue event listeners
*/
private setupQueueListeners(): void {
// Listen for items ready to be delivered
this.queue.on('itemsReady', (items: IQueueItem[]) => {
if (this.isRunning) {
this.processItems(items).catch(err => {
console.error('Error processing queue items:', err);
});
}
});
}
/**
* Start the delivery system
*/
public async start(): Promise<void> {
this.isRunning = true;
this.emit('started');
// Update stats
this.updateStats();
}
/**
* Stop the delivery system
*/
public async stop(): Promise<void> {
this.isRunning = false;
// Close all connections
for (const connection of this.connections.values()) {
try {
if (connection.close) {
await connection.close();
}
} catch (error) {
console.error('Error closing connection:', error);
}
}
this.connections.clear();
this.emit('stopped');
}
/**
* Process items from the queue
* @param items Queue items to process
*/
private async processItems(items: IQueueItem[]): Promise<void> {
// Skip if not running
if (!this.isRunning) {
return;
}
// Count in-progress items
const inProgress = Array.from(this.queue.getAllItems()).filter(item =>
item.status === 'processing'
).length;
// Calculate how many items we can process concurrently
const availableSlots = Math.max(0, this.maxConcurrent - inProgress);
if (availableSlots === 0) {
return;
}
// Process up to availableSlots items
const itemsToProcess = items.slice(0, availableSlots);
// Process each item
for (const item of itemsToProcess) {
// Mark item as processing
await this.queue.updateItem(item.id, {
status: 'processing'
});
// Deliver the item
this.deliverItem(item).catch(error => {
console.error(`Error delivering item ${item.id}:`, error);
});
}
// Update stats
this.updateStats();
}
/**
* Deliver a single queue item
* @param item Queue item to deliver
*/
private async deliverItem(item: IQueueItem): Promise<void> {
try {
// Update stats
this.stats.inProgress++;
this.stats.totalAttempts++;
// Get processing result
const result = item.processingResult;
// Attempt delivery
const deliveryResult = await this.deliverEmail(result);
if (deliveryResult.success) {
// Mark as delivered
await this.queue.markDelivered(item.id);
// Update stats
this.stats.delivered++;
this.stats.inProgress--;
// Emit delivery event
this.emit('delivered', {
item,
result: deliveryResult
});
} else {
// Mark as failed (will retry if attempts < maxRetries)
await this.queue.markFailed(item.id, deliveryResult.error || 'Unknown error');
// Update stats
this.stats.inProgress--;
// Emit failure event
this.emit('deliveryFailed', {
item,
result: deliveryResult
});
}
// Update stats
this.updateStats();
} catch (error) {
console.error(`Error in deliverItem for ${item.id}:`, error);
// Mark as failed
await this.queue.markFailed(item.id, error.message || 'Internal error');
// Update stats
this.stats.inProgress--;
this.updateStats();
}
}
/**
* Deliver an email to its destination
* @param result Processing result containing the email to deliver
*/
private async deliverEmail(result: IProcessingResult): Promise<IDeliveryResult> {
const { routing, metadata, rawData } = result;
const { id, targetServer, port, useTls, authentication } = routing;
try {
// Create a transport for delivery
// In a real implementation, this would use nodemailer or a similar library
console.log(`Delivering email ${id} to ${targetServer}:${port} (TLS: ${useTls})`);
// Simulate delivery
await new Promise(resolve => setTimeout(resolve, 100));
// Simulate success
// In a real implementation, we would actually send the email
const success = Math.random() > 0.1; // 90% success rate for simulation
if (!success) {
throw new Error('Simulated delivery failure');
}
// Return success result
return {
id,
success: true,
timestamp: new Date(),
destination: `${targetServer}:${port}`,
messageId: `${id}@example.com`
};
} catch (error) {
console.error(`Delivery error for ${id}:`, error);
// Return failure result
return {
id,
success: false,
error: error.message || 'Unknown error',
timestamp: new Date(),
destination: `${targetServer}:${port}`
};
}
}
/**
* Update delivery system statistics
*/
private updateStats(): void {
// Get pending items
this.stats.pending = Array.from(this.queue.getAllItems()).filter(item =>
item.status === 'pending' || item.status === 'deferred'
).length;
// Emit stats update
this.emit('statsUpdated', this.getStats());
}
/**
* Get current delivery statistics
*/
public getStats(): IDeliveryStats {
return { ...this.stats };
}
}

View File

@ -0,0 +1,495 @@
import * as plugins from '../plugins.js';
import type { ISmtpConfig, IContentScannerConfig, ITransformationConfig } from './classes.smtp.config.js';
import type { ISmtpSession } from './classes.smtp.server.js';
import { EventEmitter } from 'node:events';
// Create standalone types to avoid interface compatibility issues
interface AddressObject {
address?: string;
name?: string;
[key: string]: any;
}
interface ExtendedAddressObject {
value: AddressObject | AddressObject[];
[key: string]: any;
}
// Don't extend ParsedMail directly to avoid type compatibility issues
interface ExtendedParsedMail {
// Basic properties from ParsedMail
subject?: string;
text?: string;
textAsHtml?: string;
html?: string;
attachments?: Array<any>;
headers?: Map<string, any>;
headerLines?: Array<{key: string; line: string}>;
messageId?: string;
date?: Date;
// Extended address objects
from?: ExtendedAddressObject;
to?: ExtendedAddressObject;
cc?: ExtendedAddressObject;
bcc?: ExtendedAddressObject;
// Add any other properties we need
[key: string]: any;
}
/**
* Email metadata extracted from parsed mail
*/
export interface IEmailMetadata {
id: string;
from: string;
fromDomain: string;
to: string[];
toDomains: string[];
subject?: string;
size: number;
hasAttachments: boolean;
receivedAt: Date;
clientIp: string;
authenticated: boolean;
authUser?: string;
}
/**
* Content scanning result
*/
export interface IScanResult {
id: string;
spamScore?: number;
hasVirus?: boolean;
blockedAttachments?: string[];
action: 'accept' | 'tag' | 'reject';
reason?: string;
}
/**
* Routing decision for an email
*/
export interface IRoutingDecision {
id: string;
targetServer: string;
port: number;
useTls: boolean;
authentication?: {
user?: string;
pass?: string;
};
headers?: Array<{
name: string;
value: string;
append?: boolean;
}>;
signDkim?: boolean;
dkimOptions?: {
domainName: string;
keySelector: string;
privateKey: string;
};
}
/**
* Complete processing result
*/
export interface IProcessingResult {
id: string;
metadata: IEmailMetadata;
scanResult: IScanResult;
routing: IRoutingDecision;
modifiedMessage?: ExtendedParsedMail;
originalMessage: ExtendedParsedMail;
rawData: string;
action: 'queue' | 'reject';
session: ISmtpSession;
}
/**
* Email Processor handles email processing pipeline
*/
export class EmailProcessor extends EventEmitter {
private config: ISmtpConfig;
private processingQueue: Map<string, IProcessingResult> = new Map();
/**
* Create a new email processor
* @param config SMTP configuration
*/
constructor(config: ISmtpConfig) {
super();
this.config = config;
}
/**
* Process an email message
* @param message Parsed email message
* @param rawData Raw email data
* @param session SMTP session
*/
public async processEmail(
message: ExtendedParsedMail,
rawData: string,
session: ISmtpSession
): Promise<IProcessingResult> {
try {
// Generate ID for this processing task
const id = plugins.uuid.v4();
// Extract metadata
const metadata = await this.extractMetadata(message, session, id);
// Scan content if enabled
const scanResult = await this.scanContent(message, metadata);
// If content scanning rejects the message, return early
if (scanResult.action === 'reject') {
const result: IProcessingResult = {
id,
metadata,
scanResult,
routing: {
id,
targetServer: '',
port: 0,
useTls: false
},
originalMessage: message,
rawData,
action: 'reject',
session
};
this.emit('rejected', result);
return result;
}
// Determine routing
const routing = await this.determineRouting(message, metadata);
// Apply transformations
const modifiedMessage = await this.applyTransformations(message, routing, scanResult);
// Create processing result
const result: IProcessingResult = {
id,
metadata,
scanResult,
routing,
modifiedMessage,
originalMessage: message,
rawData,
action: 'queue',
session
};
// Add to processing queue
this.processingQueue.set(id, result);
// Emit processed event
this.emit('processed', result);
return result;
} catch (error) {
console.error('Error processing email:', error);
throw error;
}
}
/**
* Extract metadata from email message
* @param message Parsed email
* @param session SMTP session
* @param id Processing ID
*/
private async extractMetadata(
message: ExtendedParsedMail,
session: ISmtpSession,
id: string
): Promise<IEmailMetadata> {
// Extract sender information
let from = '';
if (message.from && message.from.value) {
const fromValue = message.from.value;
if (Array.isArray(fromValue)) {
from = fromValue[0]?.address || '';
} else if (typeof fromValue === 'object' && 'address' in fromValue && fromValue.address) {
from = fromValue.address;
}
}
const fromDomain = from.split('@')[1] || '';
// Extract recipient information
let to: string[] = [];
if (message.to && message.to.value) {
const toValue = message.to.value;
if (Array.isArray(toValue)) {
to = toValue
.map(addr => (addr && 'address' in addr) ? addr.address || '' : '')
.filter(Boolean);
} else if (typeof toValue === 'object' && 'address' in toValue && toValue.address) {
to = [toValue.address];
}
}
const toDomains = to.map(addr => addr.split('@')[1] || '');
// Create metadata
return {
id,
from,
fromDomain,
to,
toDomains,
subject: message.subject,
size: Buffer.byteLength(message.html || message.textAsHtml || message.text || ''),
hasAttachments: message.attachments?.length > 0,
receivedAt: new Date(),
clientIp: session.remoteAddress,
authenticated: !!session.user,
authUser: session.user?.username
};
}
/**
* Scan email content
* @param message Parsed email
* @param metadata Email metadata
*/
private async scanContent(
message: ExtendedParsedMail,
metadata: IEmailMetadata
): Promise<IScanResult> {
// Skip if content scanning is disabled
if (!this.config.contentScanning || !this.config.scanners?.length) {
return {
id: metadata.id,
action: 'accept'
};
}
// Default result
const result: IScanResult = {
id: metadata.id,
action: 'accept'
};
// Placeholder for scanning results
let spamFound = false;
let virusFound = false;
const blockedAttachments: string[] = [];
// Apply each scanner
for (const scanner of this.config.scanners) {
switch (scanner.type) {
case 'spam':
// Placeholder for spam scanning
// In a real implementation, we would use a spam scanning library
const spamScore = Math.random() * 10; // Fake score between 0-10
result.spamScore = spamScore;
if (scanner.threshold && spamScore > scanner.threshold) {
spamFound = true;
if (scanner.action === 'reject') {
result.action = 'reject';
result.reason = `Spam score ${spamScore} exceeds threshold ${scanner.threshold}`;
} else if (scanner.action === 'tag') {
result.action = 'tag';
}
}
break;
case 'virus':
// Placeholder for virus scanning
// In a real implementation, we would use a virus scanning library
const hasVirus = false; // Fake result
result.hasVirus = hasVirus;
if (hasVirus) {
virusFound = true;
if (scanner.action === 'reject') {
result.action = 'reject';
result.reason = 'Message contains virus';
} else if (scanner.action === 'tag') {
result.action = 'tag';
}
}
break;
case 'attachment':
// Check attachments against blocked extensions
if (scanner.blockedExtensions && message.attachments?.length) {
for (const attachment of message.attachments) {
const filename = attachment.filename || '';
const extension = filename.substring(filename.lastIndexOf('.')).toLowerCase();
if (scanner.blockedExtensions.includes(extension)) {
blockedAttachments.push(filename);
if (scanner.action === 'reject') {
result.action = 'reject';
result.reason = `Blocked attachment type: ${extension}`;
} else if (scanner.action === 'tag') {
result.action = 'tag';
}
}
}
}
break;
}
// Set blocked attachments in result if any
if (blockedAttachments.length) {
result.blockedAttachments = blockedAttachments;
}
}
return result;
}
/**
* Determine routing for an email
* @param message Parsed email
* @param metadata Email metadata
*/
private async determineRouting(
message: ExtendedParsedMail,
metadata: IEmailMetadata
): Promise<IRoutingDecision> {
// Start with the default routing
const defaultRouting: IRoutingDecision = {
id: metadata.id,
targetServer: this.config.defaultServer,
port: this.config.defaultPort || 25,
useTls: this.config.useTls || false
};
// If no domain configs, use default routing
if (!this.config.domainConfigs?.length) {
return defaultRouting;
}
// Try to find matching domain config based on recipient domains
for (const domain of metadata.toDomains) {
for (const domainConfig of this.config.domainConfigs) {
// Check if domain matches any of the configured domains
if (domainConfig.domains.some(configDomain => this.domainMatches(domain, configDomain))) {
// Create routing from domain config
const routing: IRoutingDecision = {
id: metadata.id,
targetServer: domainConfig.targetIPs[0], // Use first target IP
port: domainConfig.port || 25,
useTls: domainConfig.useTls || false
};
// Add authentication if specified
if (domainConfig.authentication) {
routing.authentication = domainConfig.authentication;
}
// Add header modifications if specified
if (domainConfig.addHeaders && domainConfig.headerInfo?.length) {
routing.headers = domainConfig.headerInfo.map(h => ({
name: h.name,
value: h.value,
append: false
}));
}
// Add DKIM signing if specified
if (domainConfig.signDkim && domainConfig.dkimOptions) {
routing.signDkim = true;
routing.dkimOptions = domainConfig.dkimOptions;
}
return routing;
}
}
}
// No match found, use default routing
return defaultRouting;
}
/**
* Apply transformations to the email
* @param message Original parsed email
* @param routing Routing decision
* @param scanResult Scan result
*/
private async applyTransformations(
message: ExtendedParsedMail,
routing: IRoutingDecision,
scanResult: IScanResult
): Promise<ExtendedParsedMail> {
// Skip if no transformations configured
if (!this.config.transformations?.length) {
return message;
}
// Clone the message for modifications
// Note: In a real implementation, we would need to properly clone the message
const modifiedMessage = { ...message };
// Apply each transformation
for (const transformation of this.config.transformations) {
switch (transformation.type) {
case 'addHeader':
// Add a header to the message
if (transformation.header && transformation.value) {
// In a real implementation, we would modify the raw message headers
console.log(`Adding header ${transformation.header}: ${transformation.value}`);
}
break;
case 'dkimSign':
// Sign the message with DKIM
if (routing.signDkim && routing.dkimOptions) {
// In a real implementation, we would use mailauth.dkimSign
console.log(`Signing message with DKIM for domain ${routing.dkimOptions.domainName}`);
}
break;
}
}
return modifiedMessage;
}
/**
* Check if a domain matches a pattern (including wildcards)
* @param domain Domain to check
* @param pattern Pattern to match against
*/
private domainMatches(domain: string, pattern: string): boolean {
domain = domain.toLowerCase();
pattern = pattern.toLowerCase();
// Exact match
if (domain === pattern) {
return true;
}
// Wildcard match (*.example.com)
if (pattern.startsWith('*.')) {
const suffix = pattern.slice(2);
return domain.endsWith(suffix) && domain.length > suffix.length;
}
return false;
}
/**
* Update processor configuration
* @param config New configuration
*/
public updateConfig(config: Partial<ISmtpConfig>): void {
this.config = {
...this.config,
...config
};
this.emit('configUpdated', this.config);
}
}

View File

@ -0,0 +1,170 @@
import * as plugins from '../plugins.js';
/**
* Configuration for SMTP authentication
*/
export interface ISmtpAuthConfig {
/** Whether authentication is required */
required?: boolean;
/** Supported authentication methods */
methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
/** Static user credentials */
users?: Array<{username: string, password: string}>;
/** LDAP URL for authentication */
ldapUrl?: string;
}
/**
* Configuration for TLS in SMTP connections
*/
export interface ISmtpTlsConfig {
/** Path to certificate file */
certPath?: string;
/** Path to key file */
keyPath?: string;
/** Path to CA certificate */
caPath?: string;
/** Minimum TLS version */
minVersion?: string;
/** Whether to use STARTTLS upgrade or implicit TLS */
useStartTls?: boolean;
/** Cipher suite for TLS */
ciphers?: string;
}
/**
* Configuration for content scanning
*/
export interface IContentScannerConfig {
/** Type of scanner */
type: 'spam' | 'virus' | 'attachment';
/** Threshold for spam detection */
threshold?: number;
/** Action to take when content matches scanning criteria */
action: 'tag' | 'reject';
/** File extensions to block (for attachment scanner) */
blockedExtensions?: string[];
}
/**
* Configuration for email transformations
*/
export interface ITransformationConfig {
/** Type of transformation */
type: string;
/** Header name for adding/modifying headers */
header?: string;
/** Header value for adding/modifying headers */
value?: string;
/** Domains for DKIM signing */
domains?: string[];
/** Whether to append to existing header or replace */
append?: boolean;
/** Additional transformation parameters */
[key: string]: any;
}
/**
* Configuration for DKIM signing
*/
export interface IDkimConfig {
/** Domain name for DKIM signature */
domainName: string;
/** Selector for DKIM */
keySelector: string;
/** Private key for DKIM signing */
privateKey: string;
}
/**
* Domain-specific routing configuration
*/
export interface ISmtpDomainConfig {
/** Domains this configuration applies to */
domains: string[];
/** Target SMTP servers for this domain */
targetIPs: string[];
/** Target port */
port?: number;
/** Whether to use TLS when connecting to target */
useTls?: boolean;
/** Authentication credentials for target server */
authentication?: {
user?: string;
pass?: string;
};
/** Allowed client IPs */
allowedIPs?: string[];
/** Rate limits for this domain */
rateLimits?: {
maxMessagesPerMinute?: number;
maxRecipientsPerMessage?: number;
};
/** Whether to add custom headers */
addHeaders?: boolean;
/** Headers to add */
headerInfo?: Array<{
name: string;
value: string;
}>;
/** Whether to sign emails with DKIM */
signDkim?: boolean;
/** DKIM configuration */
dkimOptions?: IDkimConfig;
}
/**
* Queue configuration
*/
export interface IQueueConfig {
/** Storage type for queue */
storageType?: 'memory' | 'disk';
/** Path for disk storage */
persistentPath?: string;
/** Maximum retry attempts */
maxRetries?: number;
/** Base delay between retries (ms) */
baseRetryDelay?: number;
/** Maximum delay between retries (ms) */
maxRetryDelay?: number;
/** Maximum queue size */
maxQueueSize?: number;
}
/**
* Complete SMTP configuration
*/
export interface ISmtpConfig {
/** SMTP ports to listen on */
ports: number[];
/** Hostname for SMTP server */
hostname: string;
/** Banner text for SMTP server */
banner?: string;
/** Maximum message size in bytes */
maxMessageSize?: number;
/** TLS configuration */
tls?: ISmtpTlsConfig;
/** Authentication configuration */
auth?: ISmtpAuthConfig;
/** Domain-specific configurations */
domainConfigs: ISmtpDomainConfig[];
/** Default routing */
defaultServer: string;
defaultPort?: number;
useTls?: boolean;
/** Content scanning configuration */
contentScanning?: boolean;
scanners?: IContentScannerConfig[];
/** Message transformations */
transformations?: ITransformationConfig[];
/** Queue configuration */
queue?: IQueueConfig;
}

View File

@ -0,0 +1,423 @@
import * as plugins from '../plugins.js';
import { Readable } from 'node:stream';
import type { ISmtpConfig, ISmtpAuthConfig } from './classes.smtp.config.js';
import { EventEmitter } from 'node:events';
/**
* Connection session information
*/
export interface ISmtpSession {
id: string;
remoteAddress: string;
remotePort: number;
clientHostname?: string;
secure: boolean;
transmissionType?: 'SMTP' | 'ESMTP';
user?: {
username: string;
[key: string]: any;
};
envelope?: {
mailFrom: {
address: string;
args: any;
};
rcptTo: Array<{
address: string;
args: any;
}>;
};
}
/**
* Authentication data
*/
export interface IAuthData {
method: string;
username: string;
password: string;
}
/**
* SMTP Server class for receiving emails
*/
export class SmtpServer extends EventEmitter {
private config: ISmtpConfig;
private server: any; // Will be SMTPServer from smtp-server once we add the dependency
private incomingConnections: Map<string, ISmtpSession> = new Map();
/**
* Create a new SMTP server
* @param config SMTP server configuration
*/
constructor(config: ISmtpConfig) {
super();
this.config = config;
}
/**
* Initialize and start the SMTP server
*/
public async start(): Promise<void> {
try {
// This is a placeholder for the actual server creation
// In the real implementation, we would use the smtp-server package
console.log(`Starting SMTP server on ports ${this.config.ports.join(', ')}`);
// Setup TLS options if provided
const tlsOptions = this.config.tls ? {
key: this.config.tls.keyPath ? await plugins.fs.promises.readFile(this.config.tls.keyPath, 'utf8') : undefined,
cert: this.config.tls.certPath ? await plugins.fs.promises.readFile(this.config.tls.certPath, 'utf8') : undefined,
ca: this.config.tls.caPath ? await plugins.fs.promises.readFile(this.config.tls.caPath, 'utf8') : undefined,
minVersion: this.config.tls.minVersion || 'TLSv1.2',
ciphers: this.config.tls.ciphers
} : undefined;
// Create the server
// Note: In the actual implementation, this would use SMTPServer from smtp-server
this.server = {
// Placeholder for server instance
async close() {
console.log('SMTP server closed');
}
};
// Set up event handlers
this.setupEventHandlers();
// Listen on all specified ports
for (const port of this.config.ports) {
// In actual implementation, this would call server.listen(port)
console.log(`SMTP server listening on port ${port}`);
}
this.emit('started');
} catch (error) {
console.error('Failed to start SMTP server:', error);
throw error;
}
}
/**
* Stop the SMTP server
*/
public async stop(): Promise<void> {
try {
if (this.server) {
// Close the server
await this.server.close();
this.server = null;
// Clear connection tracking
this.incomingConnections.clear();
this.emit('stopped');
}
} catch (error) {
console.error('Error stopping SMTP server:', error);
throw error;
}
}
/**
* Set up event handlers for the SMTP server
*/
private setupEventHandlers(): void {
// These would be connected to actual server events in implementation
// Connection handler
this.onConnect((session, callback) => {
// Store connection in tracking map
this.incomingConnections.set(session.id, session);
// Check if connection is allowed based on IP
if (!this.isIpAllowed(session.remoteAddress)) {
return callback(new Error('Connection refused'));
}
// Accept the connection
callback();
});
// Authentication handler
this.onAuth((auth, session, callback) => {
// Skip auth check if not required
if (!this.config.auth?.required) {
return callback(null, { user: auth.username });
}
// Check authentication
if (this.authenticateUser(auth)) {
return callback(null, { user: auth.username });
}
// Authentication failed
callback(new Error('Invalid credentials'));
});
// Sender validation
this.onMailFrom((address, session, callback) => {
// Validate sender address if needed
// Accept the sender
callback();
});
// Recipient validation
this.onRcptTo((address, session, callback) => {
// Validate recipient address
// Check if we handle this domain
if (!this.isDomainHandled(address.address.split('@')[1])) {
return callback(new Error('Domain not handled by this server'));
}
// Accept the recipient
callback();
});
// Message data handler
this.onData((stream, session, callback) => {
// Process the incoming message
this.processMessageData(stream, session)
.then(() => callback())
.catch(err => callback(err));
});
}
/**
* Process incoming message data
* @param stream Message data stream
* @param session SMTP session
*/
private async processMessageData(stream: Readable, session: ISmtpSession): Promise<void> {
return new Promise<void>((resolve, reject) => {
// Collect the message data
let messageData = '';
let messageSize = 0;
stream.on('data', (chunk) => {
messageData += chunk;
messageSize += chunk.length;
// Check size limits
if (this.config.maxMessageSize && messageSize > this.config.maxMessageSize) {
stream.unpipe();
return reject(new Error('Message size exceeds limit'));
}
});
stream.on('end', async () => {
try {
// Parse the email using mailparser
const parsedMail = await this.parseEmail(messageData);
// Emit message received event
this.emit('message', {
session,
mail: parsedMail,
rawData: messageData
});
resolve();
} catch (error) {
reject(error);
}
});
stream.on('error', (error) => {
reject(error);
});
});
}
/**
* Parse raw email data using mailparser
* @param rawData Raw email data
*/
private async parseEmail(rawData: string): Promise<any> {
// Use mailparser to parse the email
// We return 'any' here which will be treated as ExtendedParsedMail by consumers
return plugins.mailparser.simpleParser(rawData);
}
/**
* Check if an IP address is allowed to connect
* @param ip IP address
*/
private isIpAllowed(ip: string): boolean {
// Default to allowing all IPs if no restrictions
const defaultAllowed = ['0.0.0.0/0'];
// Check domain configs for IP restrictions
for (const domainConfig of this.config.domainConfigs) {
if (domainConfig.allowedIPs && domainConfig.allowedIPs.length > 0) {
// Check if IP matches any of the allowed IPs
for (const allowedIp of domainConfig.allowedIPs) {
if (this.ipMatchesRange(ip, allowedIp)) {
return true;
}
}
}
}
// Check against default allowed IPs
for (const allowedIp of defaultAllowed) {
if (this.ipMatchesRange(ip, allowedIp)) {
return true;
}
}
return false;
}
/**
* Check if an IP matches a range
* @param ip IP address to check
* @param range IP range in CIDR notation
*/
private ipMatchesRange(ip: string, range: string): boolean {
try {
// Use the 'ip' package to check if IP is in range
return plugins.ip.cidrSubnet(range).contains(ip);
} catch (error) {
console.error(`Invalid IP range: ${range}`, error);
return false;
}
}
/**
* Check if a domain is handled by this server
* @param domain Domain to check
*/
private isDomainHandled(domain: string): boolean {
// Check if the domain is configured in any domain config
for (const domainConfig of this.config.domainConfigs) {
for (const configDomain of domainConfig.domains) {
if (this.domainMatches(domain, configDomain)) {
return true;
}
}
}
return false;
}
/**
* Check if a domain matches a pattern (including wildcards)
* @param domain Domain to check
* @param pattern Pattern to match against
*/
private domainMatches(domain: string, pattern: string): boolean {
domain = domain.toLowerCase();
pattern = pattern.toLowerCase();
// Exact match
if (domain === pattern) {
return true;
}
// Wildcard match (*.example.com)
if (pattern.startsWith('*.')) {
const suffix = pattern.slice(2);
return domain.endsWith(suffix) && domain.length > suffix.length;
}
return false;
}
/**
* Authenticate a user
* @param auth Authentication data
*/
private authenticateUser(auth: IAuthData): boolean {
// Skip if no auth config
if (!this.config.auth) {
return true;
}
// Check if auth method is supported
if (this.config.auth.methods && !this.config.auth.methods.includes(auth.method as any)) {
return false;
}
// Check static user credentials
if (this.config.auth.users) {
const user = this.config.auth.users.find(u =>
u.username === auth.username && u.password === auth.password);
if (user) {
return true;
}
}
// LDAP authentication would go here
return false;
}
/**
* Event handler for connection
* @param handler Function to handle connection
*/
public onConnect(handler: (session: ISmtpSession, callback: (err?: Error) => void) => void): void {
// In actual implementation, this would connect to the server's 'connection' event
this.on('connect', handler);
}
/**
* Event handler for authentication
* @param handler Function to handle authentication
*/
public onAuth(handler: (auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void) => void): void {
// In actual implementation, this would connect to the server's 'auth' event
this.on('auth', handler);
}
/**
* Event handler for MAIL FROM command
* @param handler Function to handle MAIL FROM
*/
public onMailFrom(handler: (address: { address: string; args: any }, session: ISmtpSession, callback: (err?: Error) => void) => void): void {
// In actual implementation, this would connect to the server's 'mail' event
this.on('mail', handler);
}
/**
* Event handler for RCPT TO command
* @param handler Function to handle RCPT TO
*/
public onRcptTo(handler: (address: { address: string; args: any }, session: ISmtpSession, callback: (err?: Error) => void) => void): void {
// In actual implementation, this would connect to the server's 'rcpt' event
this.on('rcpt', handler);
}
/**
* Event handler for DATA command
* @param handler Function to handle DATA
*/
public onData(handler: (stream: Readable, session: ISmtpSession, callback: (err?: Error) => void) => void): void {
// In actual implementation, this would connect to the server's 'data' event
this.on('dataReady', handler);
}
/**
* Update the server configuration
* @param config New configuration
*/
public updateConfig(config: Partial<ISmtpConfig>): void {
this.config = {
...this.config,
...config
};
// In a real implementation, this might require restarting the server
this.emit('configUpdated', this.config);
}
/**
* Get server statistics
*/
public getStats(): any {
return {
connections: this.incomingConnections.size,
// Additional stats would be included here
};
}
}

View File

@ -1,3 +1,11 @@
// Core DcRouter components
export * from './classes.dcrouter.js';
export * from './classes.smtp.portconfig.js';
export * from './classes.email.domainrouter.js';
// SMTP Store-and-Forward components
export * from './classes.smtp.config.js';
export * from './classes.smtp.server.js';
export * from './classes.email.processor.js';
export * from './classes.delivery.queue.js';
export * from './classes.delivery.system.js';

View File

@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/platformservice',
version: '2.5.0',
version: '2.6.0',
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
}

Binary file not shown.