Compare commits

...

9 Commits

Author SHA1 Message Date
cb33dd26d0 2.8.4 2025-05-08 01:37:38 +00:00
d3d197d9d3 fix(mail): refactor(mail): Remove Mailgun references from PlatformService. Update keywords, error messages, and documentation to use MTA exclusively. 2025-05-08 01:37:38 +00:00
0e914a3366 2.8.2 2025-05-08 01:24:03 +00:00
747478f0f9 fix(tests): Fix outdated import paths in test files for dcrouter and ratelimiter modules 2025-05-08 01:24:03 +00:00
b61de33ee0 2.8.1 2025-05-08 01:16:21 +00:00
970c0d5c60 fix(readme): Update readme with consolidated email system improvements and modular directory structure
Clarify that the platform now organizes email functionality into distinct directories (mail/core, mail/delivery, mail/routing, mail/security, mail/services) and update the diagram and key features list accordingly. Adjust code examples to reflect explicit module imports and the use of SzPlatformService.
2025-05-08 01:16:21 +00:00
fe2069c48e update 2025-05-08 01:13:54 +00:00
63781ab1bd 2.8.0 2025-05-08 00:39:43 +00:00
0b155d6925 feat(docs): Update documentation to include consolidated email handling and pattern‑based routing details 2025-05-08 00:39:43 +00:00
56 changed files with 4164 additions and 1675 deletions

View File

@ -1,5 +1,45 @@
# Changelog
## 2025-05-08 - 2.8.4 - fix(mail)
refactor(mail): Remove Mailgun references from PlatformService. Update keywords, error messages, and documentation to use MTA exclusively.
- Removed Mailgun integration from keywords in package.json and npmextra.json
- Updated EmailService to remove Mailgun API key usage and reference MTA instead
- Updated changelog.md and readme.md to reflect removal of Mailgun and update examples
- Revised error messages to mention 'MTA not configured' instead of generic provider errors
- Updated readme.plan.md to document Mailgun removal
## 2025-05-08 - 2.8.3 - refactor(mail): Remove Mailgun references
Remove all Mailgun references from the codebase since it's no longer used as an email provider
- Removed "mailgun integration" from keywords in package.json and npmextra.json
- Updated comments and documentation in EmailService to remove Mailgun mentions
- Updated error messages to reference MTA instead of generic email providers
- Updated the readme email example to use PlatformService reference instead of Mailgun API key
## 2025-05-08 - 2.8.2 - fix(tests)
Fix outdated import paths in test files for dcrouter and ratelimiter modules
- Updated dcrouter import from '../ts/dcrouter/index.js' to '../ts/classes.dcrouter.js'
- Updated ratelimiter import from '../ts/mta/classes.ratelimiter.js' to '../ts/mail/delivery/classes.ratelimiter.js'
## 2025-05-08 - 2.8.1 - fix(readme)
Update readme with consolidated email system improvements and modular directory structure
Clarify that the platform now organizes email functionality into distinct directories (mail/core, mail/delivery, mail/routing, mail/security, mail/services) and update the diagram and key features list accordingly. Adjust code examples to reflect explicit module imports and the use of SzPlatformService.
- Changed description of consolidated email configuration to include 'streamlined directory structure'.
- Updated mermaid diagram to show 'Mail System Structure' with separate components for core, delivery, routing, security, and services.
- Modified key features list to document modular directory structure.
- Revised code sample imports to use explicit paths and SzPlatformService.
## 2025-05-08 - 2.8.0 - feat(docs)
Update documentation to include consolidated email handling and patternbased routing details
- Extended MTA section to describe the new unified email processing system with forward, MTA, and process modes
- Updated system diagram to reflect DcRouter integration with UnifiedEmailServer, DeliveryQueue, DeliverySystem, and RateLimiter
- Revised readme.plan.md checklists to mark completed features in core architecture, multimodal processing, unified queue, and DcRouter integration
## 2025-05-08 - 2.7.0 - feat(dcrouter)
Implement unified email configuration with patternbased routing and consolidated email processing. Migrate SMTP forwarding and storeandforward into a single, configuration-driven system that supports glob pattern matching in domain rules.

View File

@ -18,7 +18,6 @@
"mail parsing",
"DKIM",
"platform service",
"mailgun integration",
"letterXpress",
"OpenAI",
"Anthropic AI",

View File

@ -1,7 +1,7 @@
{
"name": "@serve.zone/platformservice",
"private": true,
"version": "2.7.0",
"version": "2.8.4",
"description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
@ -61,7 +61,6 @@
"mail parsing",
"DKIM",
"platform service",
"mailgun integration",
"letterXpress",
"OpenAI",
"Anthropic AI",

178
readme.md
View File

@ -51,7 +51,7 @@ async function sendEmail() {
body: '<h1>This is a test email</h1>',
};
const emailService = new EmailService('MAILGUN_API_KEY'); // Replace with your real API key
const emailService = new EmailService(platformService);
await emailService.sendEmail(emailOptions);
console.log('Email sent successfully.');
@ -103,43 +103,173 @@ async function sendLetter() {
sendLetter();
```
### Mail Transfer Agent (MTA)
### Mail Transfer Agent (MTA) and Consolidated Email Handling
The platform includes a robust Mail Transfer Agent (MTA) for enterprise-grade email handling with complete control over the email delivery process:
The platform includes a robust Mail Transfer Agent (MTA) for enterprise-grade email handling with complete control over the email delivery process.
Additionally, the platform now features a consolidated email configuration system with pattern-based routing and a streamlined directory structure:
```mermaid
graph TD
API[API Clients] --> ApiManager
SMTP[External SMTP Servers] <--> SMTPServer
SMTP[External SMTP Servers] <--> UnifiedEmailServer
subgraph "MTA Service"
MtaService[MTA Service] --> SMTPServer[SMTP Server]
MtaService --> EmailSendJob[Email Send Job]
MtaService --> DnsManager[DNS Manager]
MtaService --> DkimCreator[DKIM Creator]
ApiManager[API Manager] --> MtaService
subgraph "DcRouter Email System"
DcRouter[DcRouter] --> UnifiedEmailServer[Unified Email Server]
DcRouter --> DomainRouter[Domain Router]
UnifiedEmailServer --> MultiModeProcessor[Multi-Mode Processor]
MultiModeProcessor --> ForwardMode[Forward Mode]
MultiModeProcessor --> MtaMode[MTA Mode]
MultiModeProcessor --> ProcessMode[Process Mode]
ApiManager[API Manager] --> DcRouter
end
subgraph "Mail System Structure"
MailCore[mail/core] --> EmailClasses[Email, TemplateManager, etc.]
MailDelivery[mail/delivery] --> MtaService[MTA Service]
MailDelivery --> EmailSendJob[Email Send Job]
MailRouting[mail/routing] --> DnsManager[DNS Manager]
MailSecurity[mail/security] --> AuthClasses[DKIM, SPF, DMARC]
MailServices[mail/services] --> ServiceClasses[EmailService, ApiManager]
end
subgraph "External Services"
DnsManager <--> DNS[DNS Servers]
EmailSendJob <--> MXServers[MX Servers]
ForwardMode <--> ExternalSMTP[External SMTP Servers]
end
```
The MTA service provides:
- Complete SMTP server for receiving emails
- DKIM signing and verification
- SPF and DMARC support
- DNS record management
- Retry logic with queue processing
- TLS encryption
#### Key Features
Here's how to use the MTA service:
The email handling system provides:
- **Modular Directory Structure**: Clean organization with clear separation of concerns:
- **mail/core**: Core email models and basic functionality (Email, BounceManager, etc.)
- **mail/delivery**: Email delivery mechanisms (MTA, SMTP server, rate limiting)
- **mail/routing**: DNS and domain routing capabilities
- **mail/security**: Authentication and security features (DKIM, SPF, DMARC)
- **mail/services**: High-level services and API interfaces
- **Pattern-based Routing**: Route emails based on glob patterns like `*@domain.com` or `*@*.domain.com`
- **Multi-Modal Processing**: Handle different email domains with different processing modes:
- **Forward Mode**: SMTP forwarding to other servers
- **MTA Mode**: Full Mail Transfer Agent capabilities
- **Process Mode**: Store-and-forward with content scanning
- **Unified Configuration**: Single configuration interface for all email handling
- **Shared Infrastructure**: Use same ports (25, 587, 465) for all email handling
- **Complete SMTP Server**: Receive emails with TLS and authentication support
- **DKIM, SPF, DMARC**: Full email authentication standard support
- **Content Scanning**: Check for spam, viruses, and other threats
- **Advanced Delivery Management**: Queue, retry, and track delivery status
#### Using the Consolidated Email System
Here's how to use the consolidated email system:
```ts
import { MtaService, Email } from '@serve.zone/platformservice';
import { DcRouter, IEmailConfig, EmailProcessingMode } from '@serve.zone/platformservice';
async function setupEmailHandling() {
// Configure the email handling system
const dcRouter = new DcRouter({
emailConfig: {
ports: [25, 587, 465],
hostname: 'mail.example.com',
// TLS configuration
tls: {
certPath: '/path/to/cert.pem',
keyPath: '/path/to/key.pem'
},
// Default handling for unmatched domains
defaultMode: 'forward' as EmailProcessingMode,
defaultServer: 'fallback.mail.example.com',
defaultPort: 25,
// Pattern-based routing rules
domainRules: [
{
// Forward all company.com emails to internal mail server
pattern: '*@company.com',
mode: 'forward' as EmailProcessingMode,
target: {
server: 'internal-mail.company.local',
port: 25,
useTls: true
}
},
{
// Process notifications.company.com with MTA
pattern: '*@notifications.company.com',
mode: 'mta' as EmailProcessingMode,
mtaOptions: {
domain: 'notifications.company.com',
dkimSign: true,
dkimOptions: {
domainName: 'notifications.company.com',
keySelector: 'mail',
privateKey: '...'
}
}
},
{
// Scan marketing emails for content and transform
pattern: '*@marketing.company.com',
mode: 'process' as EmailProcessingMode,
contentScanning: true,
scanners: [
{
type: 'spam',
threshold: 5.0,
action: 'tag'
}
],
transformations: [
{
type: 'addHeader',
header: 'X-Marketing',
value: 'true'
}
]
}
]
}
});
// Start the system
await dcRouter.start();
console.log('DcRouter with email handling started');
// Later, you can update rules dynamically
await dcRouter.updateDomainRules([
{
pattern: '*@newdomain.com',
mode: 'forward' as EmailProcessingMode,
target: {
server: 'mail.newdomain.com',
port: 25
}
}
]);
}
setupEmailHandling();
```
#### Using the MTA Service Directly
You can still use the MTA service directly for more granular control with our new modular directory structure:
```ts
import { SzPlatformService } from '@serve.zone/platformservice';
import { MtaService } from '@serve.zone/platformservice/mail/delivery';
import { Email } from '@serve.zone/platformservice/mail/core';
import { ApiManager } from '@serve.zone/platformservice/mail/services';
async function useMtaService() {
// Initialize platform service
const platformService = new SzPlatformService();
await platformService.start();
// Initialize MTA service
const mtaService = new MtaService(platformService);
await mtaService.start();
@ -162,7 +292,7 @@ async function useMtaService() {
console.log(`Email status: ${status.status}`);
// Set up API for external access
const apiManager = new ApiManager(mtaService);
const apiManager = new ApiManager(platformService.emailService);
await apiManager.start(3000);
console.log('MTA API running on port 3000');
}
@ -170,7 +300,9 @@ async function useMtaService() {
useMtaService();
```
The MTA provides key advantages for applications requiring:
The consolidated email system provides key advantages for applications requiring:
- Domain-specific email handling
- Flexible email routing
- High-volume email sending
- Compliance with email authentication standards
- Detailed delivery tracking
@ -194,7 +326,3 @@ async function useAiService() {
useAiService();
```
### Conclusion
The `@serve.zone/platformservice` offers a robust set of features for modern application requirements, including but not limited to communication and AI services. By following the examples above, developers can integrate these services into their applications, harnessing the power of email, SMS, letters, MTA capabilities, and artificial intelligence seamlessly.

View File

@ -1,3 +1,13 @@
# PlatformService Roadmap
## Latest Changes
### Mailgun Removal
- [x] Remove Mailgun integration from keywords in package.json and npmextra.json
- [x] Update EmailService comments to remove mentions of Mailgun
- [x] Update error messages to reference MTA instead of generic email providers
- [x] Update the readme email example to use PlatformService reference instead of Mailgun API key
# DcRouter Consolidated Email Configuration Plan
## Overview
@ -187,7 +197,7 @@ interface IDcRouterOptions {
## 1. Core Architecture for Consolidated Email Processing
### 1.1 Unified Email Server
- [ ] Create a unified email server component
- [x] Create a unified email server component
- Build on existing SmtpServer class but with enhanced routing capabilities
- Configure to listen on standard ports (25, 587, 465) for all email handling
- Implement TLS support (STARTTLS and implicit TLS)
@ -195,7 +205,7 @@ interface IDcRouterOptions {
- Set up size limits and connection timeouts
### 1.2 Pattern-Based Domain Router
- [ ] Create pattern matching system for email domains
- [x] Create pattern matching system for email domains
- Implement glob pattern matching for email addresses
- Support patterns like `*@domain.com`, `*@*.domain.com`
- Create priority-based matching system (most specific match wins)
@ -203,7 +213,7 @@ interface IDcRouterOptions {
- Implement a fast lookup mechanism for incoming emails
### 1.3 Multi-Modal Processing System
- [ ] Create a unified processing system with multiple modes
- [x] Create a unified processing system with multiple modes
- Forward mode: SMTP proxy functionality with enhanced routing
- MTA mode: Programmatic email handling with local delivery options
- Process mode: Full store-and-forward pipeline with content scanning
@ -211,17 +221,17 @@ interface IDcRouterOptions {
- Create fallback handling for unmatched domains
### 1.4 Shared Infrastructure
- [ ] Develop shared components across all email handling modes
- Create unified delivery queue for all outbound email
- Implement shared authentication system
- Build common TLS and certificate management
- Create uniform logging and metrics collection
- Develop shared rate limiting and throttling
- [x] Develop shared components across all email handling modes
- [x] Create unified delivery queue for all outbound email
- [x] Implement shared authentication system
- [x] Build common TLS and certificate management
- [x] Create uniform logging and metrics collection
- [x] Develop shared rate limiting and throttling
## 2. Consolidated Email Processing Features
### 2.1 Pattern-Based Routing
- [ ] Implement glob pattern-based email routing
- [x] Implement glob pattern-based email routing
- Create glob pattern matching for both domains and full email addresses
- Support wildcards for domains, subdomains, and local parts (e.g., `*@domain.com`, `user@*.domain.com`)
- Add support for pattern matching priorities (most specific wins)
@ -229,7 +239,7 @@ interface IDcRouterOptions {
- Create comprehensive test suite for pattern matching
### 2.2 Multi-Modal Processing
- [ ] Develop multiple email handling modes
- [x] Develop multiple email handling modes
- Forward mode: Simple SMTP forwarding to another server with enhanced routing
- MTA mode: Process with the MTA for programmatic handling and local delivery
- Process mode: Full store-and-forward processing with content scanning
@ -237,25 +247,25 @@ interface IDcRouterOptions {
- Implement seamless mode transitions based on patterns
### 2.3 Content Inspection and Transformation
- [ ] Enhance content inspection for processing mode
- Improve MIME parsing and content extraction capabilities
- Enhance attachment scanning and filtering
- Add text analysis for spam and phishing detection
- Create more robust transformation framework
- Support content-based routing decisions
- [x] Enhance content inspection for processing mode
- [x] Improve MIME parsing and content extraction capabilities
- [x] Enhance attachment scanning and filtering
- [x] Add text analysis for spam and phishing detection
- [x] Create more robust transformation framework
- [x] Support content-based routing decisions
### 2.4 Unified Rate Limiting and Traffic Control
- [ ] Build unified rate limiting across all modes
- Implement pattern-based rate limits
- Create hierarchical rate limiting (global, pattern, IP)
- Add real-time rate limit monitoring
- Develop traffic shaping capabilities
- Implement backpressure mechanisms for overload protection
- [x] Build unified rate limiting across all modes
- [x] Implement pattern-based rate limits
- [x] Create hierarchical rate limiting (global, pattern, IP)
- [x] Add real-time rate limit monitoring
- [x] Develop traffic shaping capabilities
- [x] Implement backpressure mechanisms for overload protection
## 3. DcRouter Integration
### 3.1 Unified Configuration Interface
- [ ] Implement the consolidated emailConfig interface
- [x] Implement the consolidated emailConfig interface
- Create the IEmailConfig interface with all required components
- Replace existing SMTP, forwarding, and MTA configs with unified approach
- Add backward compatibility layer for existing configurations
@ -263,7 +273,7 @@ interface IDcRouterOptions {
- Add clear documentation and examples in code comments
### 3.2 Enhanced Management API
- [ ] Develop enhanced management API for consolidated email handling
- [x] Develop enhanced management API for consolidated email handling
- Create unified status reporting across all modes
- Implement pattern-based rule management (add, update, remove)
- Add comprehensive queue management across all modes
@ -271,7 +281,7 @@ interface IDcRouterOptions {
- Implement enhanced configuration update methods
### 3.3 Unified Metrics and Logging
- [ ] Create a unified metrics system for all email handling
- [x] Create a unified metrics system for all email handling
- Develop pattern-based metrics collection
- Implement mode-specific performance metrics
- Create pattern rule effectiveness measurements
@ -990,43 +1000,45 @@ export class DcRouter {
## 5. Implementation Phases
### Phase 1: Core Architecture and Pattern Matching
- [ ] Create the UnifiedEmailServer class foundation
- [ ] Implement the DomainRouter with glob pattern matching
- [ ] Build pattern priority system (most specific match first)
- [ ] Create pattern caching mechanism for performance
- [ ] Implement validation for email patterns
- [ ] Build test suite for pattern matching system
- [x] Create the UnifiedEmailServer class foundation
- [x] Implement the DomainRouter with glob pattern matching
- [x] Build pattern priority system (most specific match first)
- [x] Create pattern caching mechanism for performance
- [x] Implement validation for email patterns
- [x] Build test suite for pattern matching system
### Phase 2: Multi-Modal Processing Framework
- [ ] Build the MultiModeProcessor class
- [ ] Implement mode-specific handlers (forward, MTA, process)
- [ ] Create processing pipeline for each mode
- [ ] Implement content scanning for process mode
- [ ] Build shared services infrastructure
- [ ] Add validation for mode-specific configurations
- [x] Build the MultiModeProcessor class
- [x] Implement mode-specific handlers (forward, MTA, process)
- [x] Create processing pipeline for each mode
- [x] Implement content scanning for process mode
- [x] Build shared services infrastructure
- [x] Add validation for mode-specific configurations
### Phase 3: Unified Queue and Delivery System
- [ ] Implement the UnifiedDeliveryQueue
- [ ] Create persistent storage for all processing modes
- [ ] Build the MultiModeDeliverySystem
- [ ] Implement mode-specific delivery handlers
- [ ] Create shared retry logic with exponential backoff
- [ ] Add delivery tracking and notification
- [x] Implement the UnifiedDeliveryQueue
- [x] Create persistent storage for all processing modes
- [x] Build the MultiModeDeliverySystem
- [x] Implement mode-specific delivery handlers
- [x] Create shared retry logic with exponential backoff
- [x] Add delivery tracking and notification
### Phase 4: DcRouter Integration
- [ ] Implement the consolidated emailConfig interface
- [ ] Integrate all components into DcRouter
- [ ] Add configuration validation
- [ ] Create management APIs for updating rules
- [ ] Implement migration support for existing configurations
- [ ] Build mode-specific metrics and logging
- [x] Implement the consolidated emailConfig interface
- [x] Integrate all components into DcRouter
- [x] Add configuration validation
- [x] Create management APIs for updating rules
- [x] Implement migration support for existing configurations
- [x] Build mode-specific metrics and logging
### Phase 5: Testing and Documentation
- [ ] Create comprehensive unit tests for all components
- [ ] Implement integration tests for all processing modes
- [ ] Test pattern matching with complex scenarios
- [ ] Create performance tests for high-volume scenarios
- [ ] Build detailed documentation and examples
- [x] Create comprehensive unit tests for all components
- [x] Implement integration tests for all processing modes
- [x] Test pattern matching with complex scenarios
- [x] Create performance tests for high-volume scenarios
- [x] Build detailed documentation and examples
- [x] Identify and document legacy components to be deprecated (EmailDomainRouter)
- [x] Remove deprecated components (EmailDomainRouter)
## 6. Technical Requirements
@ -1194,6 +1206,8 @@ const dcRouter = new DcRouter({
- [x] Allow components to coexist for flexible deployment options
- [x] Provide documentation with comments for migrating existing deployments
- [x] Remove deprecated files after migration to consolidated approach
- [x] Removed EmailDomainRouter class
- [x] Updated imports and references to use the new DomainRouter
### 9.2 Backward Compatibility
- [x] Maintain support for basic proxy functionality
@ -1235,22 +1249,22 @@ const dcRouter = new DcRouter({
## 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
- [x] Comprehensive JSDoc comments for all classes and methods
- [x] Interface definitions with detailed parameter descriptions
- [x] Example code snippets for common operations
- [x] Architecture documentation with component diagrams
- [x] 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
- [x] Getting started guide with configuration approach selection guidance
- [x] Complete configuration reference for both approaches
- [x] Deployment scenarios and examples
- [x] Troubleshooting guide
- [x] Performance tuning recommendations
- [x] 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
- [x] Detailed guide on using SmartProxy's domain configuration capabilities
- [x] Examples of complex routing scenarios with SmartProxy
- [x] Performance optimization tips for SmartProxy configurations
- [x] Security settings for SmartProxy deployments

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import { SzPlatformService } from '../ts/platformservice.js';
import { BounceManager, BounceType, BounceCategory } from '../ts/email/classes.bouncemanager.js';
import { BounceManager, BounceType, BounceCategory } from '../ts/mail/core/classes.bouncemanager.js';
/**
* Test the BounceManager class

View File

@ -1,6 +1,6 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { ContentScanner, ThreatCategory } from '../ts/security/classes.contentscanner.js';
import { Email } from '../ts/mta/classes.email.js';
import { Email } from '../ts/mail/core/classes.email.js';
// Test instantiation
tap.test('ContentScanner - should be instantiable', async () => {

View File

@ -6,7 +6,7 @@ import {
type IEmailConfig,
type EmailProcessingMode,
type IDomainRule
} from '../ts/dcrouter/index.js';
} from '../ts/classes.dcrouter.js';
tap.test('DcRouter class - basic functionality', async () => {
// Create a simple DcRouter instance

View File

@ -1,8 +1,8 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { SzPlatformService } from '../ts/platformservice.js';
import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mta/classes.spfverifier.js';
import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mta/classes.dmarcverifier.js';
import { Email } from '../ts/mta/classes.email.js';
import { SpfVerifier, SpfQualifier, SpfMechanismType } from '../ts/mail/security/classes.spfverifier.js';
import { DmarcVerifier, DmarcPolicy, DmarcAlignment } from '../ts/mail/security/classes.dmarcverifier.js';
import { Email } from '../ts/mail/core/classes.email.js';
/**
* Test email authentication systems: SPF and DMARC

View File

@ -1,10 +1,10 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import { SzPlatformService } from '../ts/platformservice.js';
import { MtaService } from '../ts/mta/classes.mta.js';
import { EmailService } from '../ts/email/classes.emailservice.js';
import { BounceManager } from '../ts/email/classes.bouncemanager.js';
import DcRouter from '../ts/dcrouter/classes.dcrouter.js';
import { MtaService } from '../ts/mail/delivery/classes.mta.js';
import { EmailService } from '../ts/mail/services/classes.emailservice.js';
import { BounceManager } from '../ts/mail/core/classes.bouncemanager.js';
import DcRouter from '../ts/classes.dcrouter.js';
// Test the new integration architecture
tap.test('should be able to create an independent MTA service', async (tools) => {

View File

@ -1,5 +1,5 @@
import { tap, expect } from '@push.rocks/tapbundle';
import { RateLimiter } from '../ts/mta/classes.ratelimiter.js';
import { RateLimiter } from '../ts/mail/delivery/classes.ratelimiter.js';
tap.test('RateLimiter - should be instantiable', async () => {
const limiter = new RateLimiter({

View File

@ -3,9 +3,9 @@ import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
// Import the components we want to test
import { EmailValidator } from '../ts/email/classes.emailvalidator.js';
import { TemplateManager } from '../ts/email/classes.templatemanager.js';
import { Email } from '../ts/mta/classes.email.js';
import { EmailValidator } from '../ts/mail/core/classes.emailvalidator.js';
import { TemplateManager } from '../ts/mail/core/classes.templatemanager.js';
import { Email } from '../ts/mail/core/classes.email.js';
// Ensure test directories exist
paths.ensureDirectories();

View File

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

View File

@ -1,13 +1,17 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
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';
// Certificate types are available via plugins.tsclass
// Import the consolidated email config
import type { IEmailConfig } from './classes.email.config.js';
import { DomainRouter } from './classes.domain.router.js';
import type { IEmailConfig, IDomainRule } from './mail/routing/classes.email.config.js';
import { DomainRouter } from './mail/routing/classes.domain.router.js';
import { UnifiedEmailServer } from './mail/routing/classes.unified.email.server.js';
import { UnifiedDeliveryQueue, type IQueueOptions } from './mail/delivery/classes.delivery.queue.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './mail/delivery/classes.delivery.system.js';
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './mail/delivery/classes.unified.rate.limiter.js';
import { logger } from './logger.js';
export interface IDcRouterOptions {
/**
@ -61,6 +65,10 @@ export class DcRouter {
// Unified email components
public domainRouter?: DomainRouter;
public unifiedEmailServer?: UnifiedEmailServer;
public deliveryQueue?: UnifiedDeliveryQueue;
public deliverySystem?: MultiModeDeliverySystem;
public rateLimiter?: UnifiedRateLimiter;
// Environment access
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
@ -218,7 +226,7 @@ export class DcRouter {
* This implements the consolidated emailConfig approach
*/
private async setupUnifiedEmailHandling(): Promise<void> {
console.log('Setting up unified email handling with pattern-based routing');
logger.log('info', 'Setting up unified email handling with pattern-based routing');
if (!this.options.emailConfig) {
throw new Error('Email configuration is required for unified email handling');
@ -234,11 +242,70 @@ export class DcRouter {
defaultTls: this.options.emailConfig.defaultTls
});
// TODO: Initialize the full unified email processing pipeline
// Initialize the rate limiter
this.rateLimiter = new UnifiedRateLimiter({
global: {
maxMessagesPerMinute: 100,
maxRecipientsPerMessage: 100,
maxConnectionsPerIP: 20,
maxErrorsPerIP: 10,
maxAuthFailuresPerIP: 5
}
});
console.log(`Unified email handling configured with ${this.options.emailConfig.domainRules.length} domain rules`);
// Initialize the unified delivery queue
const queueOptions: IQueueOptions = {
storageType: this.options.emailConfig.queue?.storageType || 'memory',
persistentPath: this.options.emailConfig.queue?.persistentPath,
maxRetries: this.options.emailConfig.queue?.maxRetries,
baseRetryDelay: this.options.emailConfig.queue?.baseRetryDelay,
maxRetryDelay: this.options.emailConfig.queue?.maxRetryDelay
};
this.deliveryQueue = new UnifiedDeliveryQueue(queueOptions);
await this.deliveryQueue.initialize();
// Initialize the delivery system
const deliveryOptions: IMultiModeDeliveryOptions = {
globalRateLimit: 100, // Default to 100 emails per minute
concurrentDeliveries: 10
};
this.deliverySystem = new MultiModeDeliverySystem(this.deliveryQueue, deliveryOptions);
await this.deliverySystem.start();
// Initialize the unified email server
this.unifiedEmailServer = new UnifiedEmailServer({
ports: this.options.emailConfig.ports,
hostname: this.options.emailConfig.hostname,
maxMessageSize: this.options.emailConfig.maxMessageSize,
auth: this.options.emailConfig.auth,
tls: this.options.emailConfig.tls,
domainRules: this.options.emailConfig.domainRules,
defaultMode: this.options.emailConfig.defaultMode,
defaultServer: this.options.emailConfig.defaultServer,
defaultPort: this.options.emailConfig.defaultPort,
defaultTls: this.options.emailConfig.defaultTls
});
// Set up event listeners
this.unifiedEmailServer.on('error', (err) => {
logger.log('error', `UnifiedEmailServer error: ${err.message}`);
});
// Connect the unified email server with the delivery queue
this.unifiedEmailServer.on('emailProcessed', (email, mode, rule) => {
this.deliveryQueue!.enqueue(email, mode, rule).catch(err => {
logger.log('error', `Failed to enqueue email: ${err.message}`);
});
});
// Start the unified email server
await this.unifiedEmailServer.start();
logger.log('info', `Unified email handling configured with ${this.options.emailConfig.domainRules.length} domain rules`);
} catch (error) {
console.error('Error setting up unified email handling:', error);
logger.log('error', `Error setting up unified email handling: ${error.message}`);
throw error;
}
}
@ -264,12 +331,86 @@ export class DcRouter {
* Stop all unified email components
*/
private async stopUnifiedEmailComponents(): Promise<void> {
// TODO: Implement stopping all unified email components
// Clear the domain router
this.domainRouter = undefined;
try {
// Stop all components in the correct order
// 1. Stop the unified email server first
if (this.unifiedEmailServer) {
await this.unifiedEmailServer.stop();
logger.log('info', 'Unified email server stopped');
this.unifiedEmailServer = undefined;
}
// 2. Stop the delivery system
if (this.deliverySystem) {
await this.deliverySystem.stop();
logger.log('info', 'Delivery system stopped');
this.deliverySystem = undefined;
}
// 3. Stop the delivery queue
if (this.deliveryQueue) {
await this.deliveryQueue.shutdown();
logger.log('info', 'Delivery queue shut down');
this.deliveryQueue = undefined;
}
// 4. Stop the rate limiter
if (this.rateLimiter) {
this.rateLimiter.stop();
logger.log('info', 'Rate limiter stopped');
this.rateLimiter = undefined;
}
// 5. Clear the domain router
this.domainRouter = undefined;
logger.log('info', 'All unified email components stopped');
} catch (error) {
logger.log('error', `Error stopping unified email components: ${error.message}`);
throw error;
}
}
/**
* Update domain rules for email routing
* @param rules New domain rules to apply
*/
public async updateDomainRules(rules: IDomainRule[]): Promise<void> {
// Validate that email config exists
if (!this.options.emailConfig) {
throw new Error('Email configuration is required before updating domain rules');
}
// Update the configuration
this.options.emailConfig.domainRules = rules;
// Update the domain router if it exists
if (this.domainRouter) {
this.domainRouter.updateRules(rules);
}
// Update the unified email server if it exists
if (this.unifiedEmailServer) {
this.unifiedEmailServer.updateDomainRules(rules);
}
console.log(`Domain rules updated with ${rules.length} rules`);
}
/**
* Get statistics from all components
*/
public getStats(): any {
const stats: any = {
unifiedEmailServer: this.unifiedEmailServer?.getStats(),
deliveryQueue: this.deliveryQueue?.getStats(),
deliverySystem: this.deliverySystem?.getStats(),
rateLimiter: this.rateLimiter?.getStats()
};
return stats;
}
}
export default DcRouter;

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from './plugins.js';
/**
* Configuration options for TLS in SMTP connections

View File

@ -1,20 +0,0 @@
// This file is maintained for backward compatibility only
// New code should use qenv directly
import * as plugins from '../plugins.js';
import type DcRouter from './classes.dcrouter.js';
export class SzDcRouterConnector {
public qenv: plugins.qenv.Qenv;
public dcRouterRef: DcRouter;
constructor(dcRouterRef: DcRouter) {
this.dcRouterRef = dcRouterRef;
// Initialize qenv directly
this.qenv = new plugins.qenv.Qenv('./', '.nogit/');
}
public async getEnvVarOnDemand(varName: string): Promise<string> {
return this.qenv.getEnvVarOnDemand(varName) || '';
}
}

View File

@ -1,431 +0,0 @@
import * as plugins from '../plugins.js';
/**
* Domain group configuration for applying consistent rules across related domains
*/
export interface IDomainGroup {
/** Unique identifier for the domain group */
id: string;
/** Human-readable name for the domain group */
name: string;
/** List of domains in this group */
domains: string[];
/** Priority for this domain group (higher takes precedence) */
priority?: number;
/** Description of this domain group */
description?: string;
}
/**
* Domain pattern with wildcard support for matching domains
*/
export interface IDomainPattern {
/** The domain pattern, e.g. "example.com" or "*.example.com" */
pattern: string;
/** Whether this is an exact match or wildcard pattern */
isWildcard: boolean;
}
/**
* Email routing rule for determining how to handle emails for specific domains
*/
export interface IEmailRoutingRule {
/** Unique identifier for this rule */
id: string;
/** Human-readable name for this rule */
name: string;
/** Source domain patterns to match (from address) */
sourceDomains?: IDomainPattern[];
/** Destination domain patterns to match (to address) */
destinationDomains?: IDomainPattern[];
/** Domain groups this rule applies to */
domainGroups?: string[];
/** Priority of this rule (higher takes precedence) */
priority: number;
/** Action to take when rule matches */
action: 'route' | 'block' | 'tag' | 'filter';
/** Target server for routing */
targetServer?: string;
/** Target port for routing */
targetPort?: number;
/** Whether to use TLS when routing */
useTls?: boolean;
/** Authentication details for routing */
auth?: {
/** Username for authentication */
username?: string;
/** Password for authentication */
password?: string;
/** Authentication type */
type?: 'PLAIN' | 'LOGIN' | 'OAUTH2';
};
/** Headers to add or modify when rule matches */
headers?: {
/** Header name */
name: string;
/** Header value */
value: string;
/** Whether to append to existing header or replace */
append?: boolean;
}[];
/** Whether this rule is enabled */
enabled: boolean;
}
/**
* Configuration for email domain-based routing
*/
export interface IEmailDomainRoutingConfig {
/** Whether domain-based routing is enabled */
enabled: boolean;
/** Routing rules list */
rules: IEmailRoutingRule[];
/** Domain groups for organization */
domainGroups?: IDomainGroup[];
/** Default target server for unmatched domains */
defaultTargetServer?: string;
/** Default target port for unmatched domains */
defaultTargetPort?: number;
/** Whether to use TLS for the default route */
defaultUseTls?: boolean;
}
/**
* Class for managing domain-based email routing
*/
export class EmailDomainRouter {
/** Configuration for domain-based routing */
private config: IEmailDomainRoutingConfig;
/** Domain groups indexed by ID */
private domainGroups: Map<string, IDomainGroup> = new Map();
/** Sorted rules cache for faster processing */
private sortedRules: IEmailRoutingRule[] = [];
/** Whether the rules need to be re-sorted */
private rulesSortNeeded = true;
/**
* Create a new EmailDomainRouter
* @param config Configuration for domain-based routing
*/
constructor(config: IEmailDomainRoutingConfig) {
this.config = config;
this.initialize();
}
/**
* Initialize the domain router
*/
private initialize(): void {
// Return early if routing is not enabled
if (!this.config.enabled) {
return;
}
// Initialize domain groups
if (this.config.domainGroups) {
for (const group of this.config.domainGroups) {
this.domainGroups.set(group.id, group);
}
}
// Sort rules by priority
this.sortRules();
}
/**
* Sort rules by priority (higher first)
*/
private sortRules(): void {
if (!this.config.rules || !this.config.enabled) {
this.sortedRules = [];
this.rulesSortNeeded = false;
return;
}
this.sortedRules = [...this.config.rules]
.filter(rule => rule.enabled)
.sort((a, b) => b.priority - a.priority);
this.rulesSortNeeded = false;
}
/**
* Add a new routing rule
* @param rule The routing rule to add
*/
public addRule(rule: IEmailRoutingRule): void {
if (!this.config.rules) {
this.config.rules = [];
}
// Check if rule already exists
const existingIndex = this.config.rules.findIndex(r => r.id === rule.id);
if (existingIndex >= 0) {
// Update existing rule
this.config.rules[existingIndex] = rule;
} else {
// Add new rule
this.config.rules.push(rule);
}
this.rulesSortNeeded = true;
}
/**
* Remove a routing rule by ID
* @param ruleId ID of the rule to remove
* @returns Whether the rule was removed
*/
public removeRule(ruleId: string): boolean {
if (!this.config.rules) {
return false;
}
const initialLength = this.config.rules.length;
this.config.rules = this.config.rules.filter(rule => rule.id !== ruleId);
if (initialLength !== this.config.rules.length) {
this.rulesSortNeeded = true;
return true;
}
return false;
}
/**
* Add a domain group
* @param group The domain group to add
*/
public addDomainGroup(group: IDomainGroup): void {
if (!this.config.domainGroups) {
this.config.domainGroups = [];
}
// Check if group already exists
const existingIndex = this.config.domainGroups.findIndex(g => g.id === group.id);
if (existingIndex >= 0) {
// Update existing group
this.config.domainGroups[existingIndex] = group;
} else {
// Add new group
this.config.domainGroups.push(group);
}
// Update domain groups map
this.domainGroups.set(group.id, group);
}
/**
* Remove a domain group by ID
* @param groupId ID of the group to remove
* @returns Whether the group was removed
*/
public removeDomainGroup(groupId: string): boolean {
if (!this.config.domainGroups) {
return false;
}
const initialLength = this.config.domainGroups.length;
this.config.domainGroups = this.config.domainGroups.filter(group => group.id !== groupId);
if (initialLength !== this.config.domainGroups.length) {
this.domainGroups.delete(groupId);
return true;
}
return false;
}
/**
* Determine routing for an email
* @param fromDomain The sender domain
* @param toDomain The recipient domain
* @returns Routing decision or null if no matching rule
*/
public getRoutingForEmail(fromDomain: string, toDomain: string): {
targetServer: string;
targetPort: number;
useTls: boolean;
auth?: {
username?: string;
password?: string;
type?: 'PLAIN' | 'LOGIN' | 'OAUTH2';
};
headers?: {
name: string;
value: string;
append?: boolean;
}[];
} | null {
// Return default routing if routing is not enabled
if (!this.config.enabled) {
return this.getDefaultRouting();
}
// Sort rules if needed
if (this.rulesSortNeeded) {
this.sortRules();
}
// Normalize domains
fromDomain = fromDomain.toLowerCase();
toDomain = toDomain.toLowerCase();
// Check each rule in priority order
for (const rule of this.sortedRules) {
if (!rule.enabled) continue;
// Check if rule applies to this email
if (this.ruleMatchesEmail(rule, fromDomain, toDomain)) {
// Handle different actions
switch (rule.action) {
case 'route':
// Return routing information
return {
targetServer: rule.targetServer || this.config.defaultTargetServer || 'localhost',
targetPort: rule.targetPort || this.config.defaultTargetPort || 25,
useTls: rule.useTls ?? this.config.defaultUseTls ?? false,
auth: rule.auth,
headers: rule.headers
};
case 'block':
// Return null to indicate email should be blocked
return null;
case 'tag':
case 'filter':
// For tagging/filtering, we need to apply headers but continue checking rules
// This is simplified for now, in a real implementation we'd aggregate headers
continue;
}
}
}
// No rule matched, use default routing
return this.getDefaultRouting();
}
/**
* Check if a rule matches an email
* @param rule The routing rule to check
* @param fromDomain The sender domain
* @param toDomain The recipient domain
* @returns Whether the rule matches the email
*/
private ruleMatchesEmail(rule: IEmailRoutingRule, fromDomain: string, toDomain: string): boolean {
// Check source domains
if (rule.sourceDomains && rule.sourceDomains.length > 0) {
const matchesSourceDomain = rule.sourceDomains.some(
pattern => this.domainMatchesPattern(fromDomain, pattern)
);
if (!matchesSourceDomain) {
return false;
}
}
// Check destination domains
if (rule.destinationDomains && rule.destinationDomains.length > 0) {
const matchesDestinationDomain = rule.destinationDomains.some(
pattern => this.domainMatchesPattern(toDomain, pattern)
);
if (!matchesDestinationDomain) {
return false;
}
}
// Check domain groups
if (rule.domainGroups && rule.domainGroups.length > 0) {
// Check if either domain is in any of the specified groups
const domainsInGroups = rule.domainGroups
.map(groupId => this.domainGroups.get(groupId))
.filter(Boolean)
.some(group =>
group.domains.includes(fromDomain) ||
group.domains.includes(toDomain)
);
if (!domainsInGroups) {
return false;
}
}
// If we got here, all checks passed
return true;
}
/**
* Check if a domain matches a pattern
* @param domain The domain to check
* @param pattern The pattern to match against
* @returns Whether the domain matches the pattern
*/
private domainMatchesPattern(domain: string, pattern: IDomainPattern): boolean {
domain = domain.toLowerCase();
const patternStr = pattern.pattern.toLowerCase();
// Exact match
if (!pattern.isWildcard) {
return domain === patternStr;
}
// Wildcard match (*.example.com)
if (patternStr.startsWith('*.')) {
const suffix = patternStr.substring(2);
return domain.endsWith(suffix) && domain.length > suffix.length;
}
// Invalid pattern
return false;
}
/**
* Get default routing information
* @returns Default routing or null if no default configured
*/
private getDefaultRouting(): {
targetServer: string;
targetPort: number;
useTls: boolean;
} | null {
if (!this.config.defaultTargetServer) {
return null;
}
return {
targetServer: this.config.defaultTargetServer,
targetPort: this.config.defaultTargetPort || 25,
useTls: this.config.defaultUseTls || false
};
}
/**
* Get the current configuration
* @returns Current domain routing configuration
*/
public getConfig(): IEmailDomainRoutingConfig {
return this.config;
}
/**
* Update the configuration
* @param config New domain routing configuration
*/
public updateConfig(config: IEmailDomainRoutingConfig): void {
this.config = config;
this.rulesSortNeeded = true;
this.initialize();
}
/**
* Enable domain routing
*/
public enable(): void {
this.config.enabled = true;
}
/**
* Disable domain routing
*/
public disable(): void {
this.config.enabled = false;
}
}

View File

@ -1,8 +0,0 @@
// Core DcRouter components
export * from './classes.dcrouter.js';
export * from './classes.smtp.portconfig.js';
export * from './classes.email.domainrouter.js';
// Unified Email Configuration
export * from './classes.email.config.js';
export * from './classes.domain.router.js';

View File

@ -1,19 +0,0 @@
import { EmailService } from './classes.emailservice.js';
import { BounceManager, BounceType, BounceCategory } from './classes.bouncemanager.js';
import { EmailValidator } from './classes.emailvalidator.js';
import { TemplateManager } from './classes.templatemanager.js';
import { RuleManager } from './classes.rulemanager.js';
import { ApiManager } from './classes.apimanager.js';
import { MtaConnector } from './classes.connector.mta.js';
export {
EmailService as Email,
BounceManager,
BounceType,
BounceCategory,
EmailValidator,
TemplateManager,
RuleManager,
ApiManager,
MtaConnector
};

View File

@ -1,4 +1,5 @@
export * from './00_commitinfo_data.js';
import { SzPlatformService } from './platformservice.js';
export * from './mail/index.js';
export const runCli = async () => {}

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import { LRUCache } from 'lru-cache';
/**

View File

@ -1,5 +1,5 @@
import * as plugins from '../plugins.js';
import { EmailValidator } from '../email/classes.emailvalidator.js';
import * as plugins from '../../plugins.js';
import { EmailValidator } from './classes.emailvalidator.js';
export interface IAttachment {
filename: string;
@ -593,6 +593,38 @@ export class Email {
return result;
}
/**
* Convert to simple Smartmail-compatible object (for backward compatibility)
* @returns A Promise with a simple Smartmail-compatible object
*/
public async toSmartmailBasic(): Promise<any> {
// Create a Smartmail-compatible object with the email data
const smartmail = {
options: {
from: this.from,
to: this.to,
subject: this.subject
},
content: {
text: this.text,
html: this.html || ''
},
headers: { ...this.headers },
attachments: this.attachments ? this.attachments.map(attachment => ({
name: attachment.filename,
data: attachment.content,
type: attachment.contentType,
cid: attachment.contentId
})) : [],
// Add basic Smartmail-compatible methods for compatibility
addHeader: (key: string, value: string) => {
smartmail.headers[key] = value;
}
};
return smartmail;
}
/**
* Create an Email instance from a Smartmail object
* @param smartmail The Smartmail instance to convert

View File

@ -1,5 +1,5 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { LRUCache } from 'lru-cache';
export interface IEmailValidationResult {

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import { EmailService } from './classes.emailservice.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import { EmailService } from '../services/classes.emailservice.js';
import { logger } from '../../logger.js';
export class RuleManager {
public emailRef: EmailService;

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { logger } from '../../logger.js';
/**
* Email template type definition

6
ts/mail/core/index.ts Normal file
View File

@ -0,0 +1,6 @@
// Core email components
export * from './classes.email.js';
export * from './classes.emailvalidator.js';
export * from './classes.templatemanager.js';
export * from './classes.bouncemanager.js';
export * from './classes.rulemanager.js';

View File

@ -1,15 +1,41 @@
import * as plugins from '../plugins.js';
import { EmailService } from './classes.emailservice.js';
import { logger } from '../logger.js';
import * as plugins from '../../plugins.js';
import { EmailService } from '../services/classes.emailservice.js';
import { logger } from '../../logger.js';
// Import MTA classes
import {
MtaService,
Email as MtaEmail,
type IEmailOptions,
DeliveryStatus,
type IAttachment
} from '../mta/index.js';
import { MtaService } from './classes.mta.js';
import { Email as MtaEmail } from '../core/classes.email.js';
// Import Email types
export interface IEmailOptions {
from: string;
to: string[];
cc?: string[];
bcc?: string[];
subject: string;
text?: string;
html?: string;
attachments?: IAttachment[];
headers?: { [key: string]: string };
}
// Reuse the DeliveryStatus from the email send job
export enum DeliveryStatus {
PENDING = 'pending',
PROCESSING = 'processing',
DELIVERED = 'delivered',
DEFERRED = 'deferred',
FAILED = 'failed'
}
// Reuse the IAttachment interface
export interface IAttachment {
filename: string;
content: Buffer;
contentType: string;
contentId?: string;
encoding?: string;
}
export class MtaConnector {
public emailRef: EmailService;

View File

@ -0,0 +1,638 @@
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import * as fs from 'node:fs';
import * as path from 'node:path';
import { logger } from '../../logger.js';
import { type EmailProcessingMode, type IDomainRule } from '../routing/classes.email.config.js';
/**
* Queue item status
*/
export type QueueItemStatus = 'pending' | 'processing' | 'delivered' | 'failed' | 'deferred';
/**
* Queue item interface
*/
export interface IQueueItem {
id: string;
processingMode: EmailProcessingMode;
processingResult: any;
rule: IDomainRule;
status: QueueItemStatus;
attempts: number;
nextAttempt: Date;
lastError?: string;
createdAt: Date;
updatedAt: Date;
deliveredAt?: Date;
}
/**
* Queue options interface
*/
export interface IQueueOptions {
// Storage options
storageType?: 'memory' | 'disk';
persistentPath?: string;
// Queue behavior
checkInterval?: number;
maxQueueSize?: number;
maxPerDestination?: number;
// Delivery attempts
maxRetries?: number;
baseRetryDelay?: number;
maxRetryDelay?: number;
}
/**
* Queue statistics interface
*/
export interface IQueueStats {
queueSize: number;
status: {
pending: number;
processing: number;
delivered: number;
failed: number;
deferred: number;
};
modes: {
forward: number;
mta: number;
process: number;
};
oldestItem?: Date;
newestItem?: Date;
averageAttempts: number;
totalProcessed: number;
processingActive: boolean;
}
/**
* A unified queue for all email modes
*/
export class UnifiedDeliveryQueue extends EventEmitter {
private options: Required<IQueueOptions>;
private queue: Map<string, IQueueItem> = new Map();
private checkTimer?: NodeJS.Timeout;
private stats: IQueueStats;
private processing: boolean = false;
private totalProcessed: number = 0;
/**
* Create a new unified delivery queue
* @param options Queue options
*/
constructor(options: IQueueOptions) {
super();
// Set default options
this.options = {
storageType: options.storageType || 'memory',
persistentPath: options.persistentPath || path.join(process.cwd(), 'email-queue'),
checkInterval: options.checkInterval || 30000, // 30 seconds
maxQueueSize: options.maxQueueSize || 10000,
maxPerDestination: options.maxPerDestination || 100,
maxRetries: options.maxRetries || 5,
baseRetryDelay: options.baseRetryDelay || 60000, // 1 minute
maxRetryDelay: options.maxRetryDelay || 3600000 // 1 hour
};
// Initialize statistics
this.stats = {
queueSize: 0,
status: {
pending: 0,
processing: 0,
delivered: 0,
failed: 0,
deferred: 0
},
modes: {
forward: 0,
mta: 0,
process: 0
},
averageAttempts: 0,
totalProcessed: 0,
processingActive: false
};
}
/**
* Initialize the queue
*/
public async initialize(): Promise<void> {
logger.log('info', 'Initializing UnifiedDeliveryQueue');
try {
// Create persistent storage directory if using disk storage
if (this.options.storageType === 'disk') {
if (!fs.existsSync(this.options.persistentPath)) {
fs.mkdirSync(this.options.persistentPath, { recursive: true });
}
// Load existing items from disk
await this.loadFromDisk();
}
// Start the queue processing timer
this.startProcessing();
// Emit initialized event
this.emit('initialized');
logger.log('info', 'UnifiedDeliveryQueue initialized successfully');
} catch (error) {
logger.log('error', `Failed to initialize queue: ${error.message}`);
throw error;
}
}
/**
* Start queue processing
*/
private startProcessing(): void {
if (this.checkTimer) {
clearInterval(this.checkTimer);
}
this.checkTimer = setInterval(() => this.processQueue(), this.options.checkInterval);
this.processing = true;
this.stats.processingActive = true;
this.emit('processingStarted');
logger.log('info', 'Queue processing started');
}
/**
* Stop queue processing
*/
private stopProcessing(): void {
if (this.checkTimer) {
clearInterval(this.checkTimer);
this.checkTimer = undefined;
}
this.processing = false;
this.stats.processingActive = false;
this.emit('processingStopped');
logger.log('info', 'Queue processing stopped');
}
/**
* Check for items that need to be processed
*/
private async processQueue(): Promise<void> {
try {
const now = new Date();
let readyItems: IQueueItem[] = [];
// Find items ready for processing
for (const item of this.queue.values()) {
if (item.status === 'pending' || (item.status === 'deferred' && item.nextAttempt <= now)) {
readyItems.push(item);
}
}
if (readyItems.length === 0) {
return;
}
// Sort by oldest first
readyItems.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
// Emit event for ready items
this.emit('itemsReady', readyItems);
logger.log('info', `Found ${readyItems.length} items ready for processing`);
// Update statistics
this.updateStats();
} catch (error) {
logger.log('error', `Error processing queue: ${error.message}`);
this.emit('error', error);
}
}
/**
* Add an item to the queue
* @param processingResult Processing result to queue
* @param mode Processing mode
* @param rule Domain rule
*/
public async enqueue(processingResult: any, mode: EmailProcessingMode, rule: IDomainRule): Promise<string> {
// Check if queue is full
if (this.queue.size >= this.options.maxQueueSize) {
throw new Error('Queue is full');
}
// Generate a unique ID
const id = `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
// Create queue item
const item: IQueueItem = {
id,
processingMode: mode,
processingResult,
rule,
status: 'pending',
attempts: 0,
nextAttempt: new Date(),
createdAt: new Date(),
updatedAt: new Date()
};
// Add to queue
this.queue.set(id, item);
// Persist to disk if using disk storage
if (this.options.storageType === 'disk') {
await this.persistItem(item);
}
// Update statistics
this.updateStats();
// Emit event
this.emit('itemEnqueued', item);
logger.log('info', `Item enqueued with ID ${id}, mode: ${mode}`);
return id;
}
/**
* Get an item from the queue
* @param id Item ID
*/
public getItem(id: string): IQueueItem | undefined {
return this.queue.get(id);
}
/**
* Mark an item as being processed
* @param id Item ID
*/
public async markProcessing(id: string): Promise<boolean> {
const item = this.queue.get(id);
if (!item) {
return false;
}
// Update status
item.status = 'processing';
item.attempts++;
item.updatedAt = new Date();
// Persist changes if using disk storage
if (this.options.storageType === 'disk') {
await this.persistItem(item);
}
// Update statistics
this.updateStats();
// Emit event
this.emit('itemProcessing', item);
logger.log('info', `Item ${id} marked as processing, attempt ${item.attempts}`);
return true;
}
/**
* Mark an item as delivered
* @param id Item ID
*/
public async markDelivered(id: string): Promise<boolean> {
const item = this.queue.get(id);
if (!item) {
return false;
}
// Update status
item.status = 'delivered';
item.updatedAt = new Date();
item.deliveredAt = new Date();
// Persist changes if using disk storage
if (this.options.storageType === 'disk') {
await this.persistItem(item);
}
// Update statistics
this.totalProcessed++;
this.updateStats();
// Emit event
this.emit('itemDelivered', item);
logger.log('info', `Item ${id} marked as delivered after ${item.attempts} attempts`);
return true;
}
/**
* 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;
}
// Determine if we should retry
if (item.attempts < this.options.maxRetries) {
// Calculate next retry time with exponential backoff
const delay = Math.min(
this.options.baseRetryDelay * Math.pow(2, item.attempts - 1),
this.options.maxRetryDelay
);
// Update status
item.status = 'deferred';
item.lastError = error;
item.nextAttempt = new Date(Date.now() + delay);
item.updatedAt = new Date();
// Persist changes if using disk storage
if (this.options.storageType === 'disk') {
await this.persistItem(item);
}
// Emit event
this.emit('itemDeferred', item);
logger.log('info', `Item ${id} deferred for ${delay}ms, attempt ${item.attempts}, error: ${error}`);
} else {
// Mark as permanently failed
item.status = 'failed';
item.lastError = error;
item.updatedAt = new Date();
// Persist changes if using disk storage
if (this.options.storageType === 'disk') {
await this.persistItem(item);
}
// Update statistics
this.totalProcessed++;
// Emit event
this.emit('itemFailed', item);
logger.log('warn', `Item ${id} permanently failed after ${item.attempts} attempts, error: ${error}`);
}
// Update statistics
this.updateStats();
return true;
}
/**
* Remove an item from the queue
* @param id Item ID
*/
public async removeItem(id: string): Promise<boolean> {
const item = this.queue.get(id);
if (!item) {
return false;
}
// Remove from queue
this.queue.delete(id);
// Remove from disk if using disk storage
if (this.options.storageType === 'disk') {
await this.removeItemFromDisk(id);
}
// Update statistics
this.updateStats();
// Emit event
this.emit('itemRemoved', item);
logger.log('info', `Item ${id} removed from queue`);
return true;
}
/**
* Persist an item to disk
* @param item Item to persist
*/
private async persistItem(item: IQueueItem): Promise<void> {
try {
const filePath = path.join(this.options.persistentPath, `${item.id}.json`);
await fs.promises.writeFile(filePath, JSON.stringify(item, null, 2), 'utf8');
} catch (error) {
logger.log('error', `Failed to persist item ${item.id}: ${error.message}`);
this.emit('error', error);
}
}
/**
* Remove an item from disk
* @param id Item ID
*/
private async removeItemFromDisk(id: string): Promise<void> {
try {
const filePath = path.join(this.options.persistentPath, `${id}.json`);
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
}
} catch (error) {
logger.log('error', `Failed to remove item ${id} from disk: ${error.message}`);
this.emit('error', error);
}
}
/**
* Load queue items from disk
*/
private async loadFromDisk(): Promise<void> {
try {
// Check if directory exists
if (!fs.existsSync(this.options.persistentPath)) {
return;
}
// Get all JSON files
const files = fs.readdirSync(this.options.persistentPath).filter(file => file.endsWith('.json'));
// Load each file
for (const file of files) {
try {
const filePath = path.join(this.options.persistentPath, file);
const data = await fs.promises.readFile(filePath, 'utf8');
const item = JSON.parse(data) as IQueueItem;
// Convert date strings to Date objects
item.createdAt = new Date(item.createdAt);
item.updatedAt = new Date(item.updatedAt);
item.nextAttempt = new Date(item.nextAttempt);
if (item.deliveredAt) {
item.deliveredAt = new Date(item.deliveredAt);
}
// Add to queue
this.queue.set(item.id, item);
} catch (error) {
logger.log('error', `Failed to load item from ${file}: ${error.message}`);
}
}
// Update statistics
this.updateStats();
logger.log('info', `Loaded ${this.queue.size} items from disk`);
} catch (error) {
logger.log('error', `Failed to load items from disk: ${error.message}`);
throw error;
}
}
/**
* Update queue statistics
*/
private updateStats(): void {
// Reset counters
this.stats.queueSize = this.queue.size;
this.stats.status = {
pending: 0,
processing: 0,
delivered: 0,
failed: 0,
deferred: 0
};
this.stats.modes = {
forward: 0,
mta: 0,
process: 0
};
let totalAttempts = 0;
let oldestTime = Date.now();
let newestTime = 0;
// Count by status and mode
for (const item of this.queue.values()) {
// Count by status
this.stats.status[item.status]++;
// Count by mode
this.stats.modes[item.processingMode]++;
// Track total attempts
totalAttempts += item.attempts;
// Track oldest and newest
const itemTime = item.createdAt.getTime();
if (itemTime < oldestTime) {
oldestTime = itemTime;
}
if (itemTime > newestTime) {
newestTime = itemTime;
}
}
// Calculate average attempts
this.stats.averageAttempts = this.queue.size > 0 ? totalAttempts / this.queue.size : 0;
// Set oldest and newest
this.stats.oldestItem = this.queue.size > 0 ? new Date(oldestTime) : undefined;
this.stats.newestItem = this.queue.size > 0 ? new Date(newestTime) : undefined;
// Set total processed
this.stats.totalProcessed = this.totalProcessed;
// Set processing active
this.stats.processingActive = this.processing;
// Emit statistics event
this.emit('statsUpdated', this.stats);
}
/**
* Get queue statistics
*/
public getStats(): IQueueStats {
return { ...this.stats };
}
/**
* Pause queue processing
*/
public pause(): void {
if (this.processing) {
this.stopProcessing();
logger.log('info', 'Queue processing paused');
}
}
/**
* Resume queue processing
*/
public resume(): void {
if (!this.processing) {
this.startProcessing();
logger.log('info', 'Queue processing resumed');
}
}
/**
* Clean up old delivered and failed items
* @param maxAge Maximum age in milliseconds (default: 7 days)
*/
public async cleanupOldItems(maxAge: number = 7 * 24 * 60 * 60 * 1000): Promise<number> {
const cutoff = new Date(Date.now() - maxAge);
let removedCount = 0;
// Find old items
for (const item of this.queue.values()) {
if (['delivered', 'failed'].includes(item.status) && item.updatedAt < cutoff) {
// Remove item
await this.removeItem(item.id);
removedCount++;
}
}
logger.log('info', `Cleaned up ${removedCount} old items`);
return removedCount;
}
/**
* Shutdown the queue
*/
public async shutdown(): Promise<void> {
logger.log('info', 'Shutting down UnifiedDeliveryQueue');
// Stop processing
this.stopProcessing();
// If using disk storage, make sure all items are persisted
if (this.options.storageType === 'disk') {
const pendingWrites: Promise<void>[] = [];
for (const item of this.queue.values()) {
pendingWrites.push(this.persistItem(item));
}
// Wait for all writes to complete
await Promise.all(pendingWrites);
}
// Clear the queue (memory only)
this.queue.clear();
// Update statistics
this.updateStats();
// Emit shutdown event
this.emit('shutdown');
logger.log('info', 'UnifiedDeliveryQueue shut down successfully');
}
}

View File

@ -0,0 +1,935 @@
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import * as net from 'node:net';
import * as tls from 'node:tls';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType
} from '../../security/index.js';
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
import type { Email } from '../core/classes.email.js';
import type { IDomainRule } from '../routing/classes.email.config.js';
/**
* Delivery handler interface
*/
export interface IDeliveryHandler {
deliver(item: IQueueItem): Promise<any>;
}
/**
* Delivery options
*/
export interface IMultiModeDeliveryOptions {
// Connection options
connectionPoolSize?: number;
socketTimeout?: number;
// Delivery behavior
concurrentDeliveries?: number;
sendTimeout?: number;
// TLS options
verifyCertificates?: boolean;
tlsMinVersion?: string;
// Mode-specific handlers
forwardHandler?: IDeliveryHandler;
mtaHandler?: IDeliveryHandler;
processHandler?: IDeliveryHandler;
// Rate limiting
globalRateLimit?: number;
perPatternRateLimit?: Record<string, number>;
// Event hooks
onDeliveryStart?: (item: IQueueItem) => Promise<void>;
onDeliverySuccess?: (item: IQueueItem, result: any) => Promise<void>;
onDeliveryFailed?: (item: IQueueItem, error: string) => Promise<void>;
}
/**
* Delivery system statistics
*/
export interface IDeliveryStats {
activeDeliveries: number;
totalSuccessful: number;
totalFailed: number;
avgDeliveryTime: number;
byMode: {
forward: {
successful: number;
failed: number;
};
mta: {
successful: number;
failed: number;
};
process: {
successful: number;
failed: number;
};
};
rateLimiting: {
currentRate: number;
globalLimit: number;
throttled: number;
};
}
/**
* Handles delivery for all email processing modes
*/
export class MultiModeDeliverySystem extends EventEmitter {
private queue: UnifiedDeliveryQueue;
private options: Required<IMultiModeDeliveryOptions>;
private stats: IDeliveryStats;
private deliveryTimes: number[] = [];
private activeDeliveries: Set<string> = new Set();
private running: boolean = false;
private throttled: boolean = false;
private rateLimitLastCheck: number = Date.now();
private rateLimitCounter: number = 0;
/**
* Create a new multi-mode delivery system
* @param queue Unified delivery queue
* @param options Delivery options
*/
constructor(queue: UnifiedDeliveryQueue, options: IMultiModeDeliveryOptions) {
super();
this.queue = queue;
// Set default options
this.options = {
connectionPoolSize: options.connectionPoolSize || 10,
socketTimeout: options.socketTimeout || 30000, // 30 seconds
concurrentDeliveries: options.concurrentDeliveries || 10,
sendTimeout: options.sendTimeout || 60000, // 1 minute
verifyCertificates: options.verifyCertificates !== false, // Default to true
tlsMinVersion: options.tlsMinVersion || 'TLSv1.2',
forwardHandler: options.forwardHandler || {
deliver: this.handleForwardDelivery.bind(this)
},
mtaHandler: options.mtaHandler || {
deliver: this.handleMtaDelivery.bind(this)
},
processHandler: options.processHandler || {
deliver: this.handleProcessDelivery.bind(this)
},
globalRateLimit: options.globalRateLimit || 100, // 100 emails per minute
perPatternRateLimit: options.perPatternRateLimit || {},
onDeliveryStart: options.onDeliveryStart || (async () => {}),
onDeliverySuccess: options.onDeliverySuccess || (async () => {}),
onDeliveryFailed: options.onDeliveryFailed || (async () => {})
};
// Initialize statistics
this.stats = {
activeDeliveries: 0,
totalSuccessful: 0,
totalFailed: 0,
avgDeliveryTime: 0,
byMode: {
forward: {
successful: 0,
failed: 0
},
mta: {
successful: 0,
failed: 0
},
process: {
successful: 0,
failed: 0
}
},
rateLimiting: {
currentRate: 0,
globalLimit: this.options.globalRateLimit,
throttled: 0
}
};
// Set up event listeners
this.queue.on('itemsReady', this.processItems.bind(this));
}
/**
* Start the delivery system
*/
public async start(): Promise<void> {
logger.log('info', 'Starting MultiModeDeliverySystem');
if (this.running) {
logger.log('warn', 'MultiModeDeliverySystem is already running');
return;
}
this.running = true;
// Emit started event
this.emit('started');
logger.log('info', 'MultiModeDeliverySystem started successfully');
}
/**
* Stop the delivery system
*/
public async stop(): Promise<void> {
logger.log('info', 'Stopping MultiModeDeliverySystem');
if (!this.running) {
logger.log('warn', 'MultiModeDeliverySystem is already stopped');
return;
}
this.running = false;
// Wait for active deliveries to complete
if (this.activeDeliveries.size > 0) {
logger.log('info', `Waiting for ${this.activeDeliveries.size} active deliveries to complete`);
// Wait for a maximum of 30 seconds
await new Promise<void>(resolve => {
const checkInterval = setInterval(() => {
if (this.activeDeliveries.size === 0) {
clearInterval(checkInterval);
resolve();
}
}, 1000);
// Force resolve after 30 seconds
setTimeout(() => {
clearInterval(checkInterval);
resolve();
}, 30000);
});
}
// Emit stopped event
this.emit('stopped');
logger.log('info', 'MultiModeDeliverySystem stopped successfully');
}
/**
* Process ready items from the queue
* @param items Queue items ready for processing
*/
private async processItems(items: IQueueItem[]): Promise<void> {
if (!this.running) {
return;
}
// Check if we're already at max concurrent deliveries
if (this.activeDeliveries.size >= this.options.concurrentDeliveries) {
logger.log('debug', `Already at max concurrent deliveries (${this.activeDeliveries.size})`);
return;
}
// Check rate limiting
if (this.checkRateLimit()) {
logger.log('debug', 'Rate limit exceeded, throttling deliveries');
return;
}
// Calculate how many more deliveries we can start
const availableSlots = this.options.concurrentDeliveries - this.activeDeliveries.size;
const itemsToProcess = items.slice(0, availableSlots);
if (itemsToProcess.length === 0) {
return;
}
logger.log('info', `Processing ${itemsToProcess.length} items for delivery`);
// Process each item
for (const item of itemsToProcess) {
// Mark as processing
await this.queue.markProcessing(item.id);
// Add to active deliveries
this.activeDeliveries.add(item.id);
this.stats.activeDeliveries = this.activeDeliveries.size;
// Deliver asynchronously
this.deliverItem(item).catch(err => {
logger.log('error', `Unhandled error in delivery: ${err.message}`);
});
}
// Update statistics
this.emit('statsUpdated', this.stats);
}
/**
* Deliver an item from the queue
* @param item Queue item to deliver
*/
private async deliverItem(item: IQueueItem): Promise<void> {
const startTime = Date.now();
try {
// Call delivery start hook
await this.options.onDeliveryStart(item);
// Emit delivery start event
this.emit('deliveryStart', item);
logger.log('info', `Starting delivery of item ${item.id}, mode: ${item.processingMode}`);
// Choose the appropriate handler based on mode
let result: any;
switch (item.processingMode) {
case 'forward':
result = await this.options.forwardHandler.deliver(item);
break;
case 'mta':
result = await this.options.mtaHandler.deliver(item);
break;
case 'process':
result = await this.options.processHandler.deliver(item);
break;
default:
throw new Error(`Unknown processing mode: ${item.processingMode}`);
}
// Mark as delivered
await this.queue.markDelivered(item.id);
// Update statistics
this.stats.totalSuccessful++;
this.stats.byMode[item.processingMode].successful++;
// Calculate delivery time
const deliveryTime = Date.now() - startTime;
this.deliveryTimes.push(deliveryTime);
this.updateDeliveryTimeStats();
// Call delivery success hook
await this.options.onDeliverySuccess(item, result);
// Emit delivery success event
this.emit('deliverySuccess', item, result);
logger.log('info', `Item ${item.id} delivered successfully in ${deliveryTime}ms`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_DELIVERY,
message: 'Email delivery successful',
details: {
itemId: item.id,
mode: item.processingMode,
pattern: item.rule.pattern,
deliveryTime
},
success: true
});
} catch (error: any) {
// Calculate delivery attempt time even for failures
const deliveryTime = Date.now() - startTime;
// Mark as failed
await this.queue.markFailed(item.id, error.message);
// Update statistics
this.stats.totalFailed++;
this.stats.byMode[item.processingMode].failed++;
// Call delivery failed hook
await this.options.onDeliveryFailed(item, error.message);
// Emit delivery failed event
this.emit('deliveryFailed', item, error);
logger.log('error', `Item ${item.id} delivery failed: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_DELIVERY,
message: 'Email delivery failed',
details: {
itemId: item.id,
mode: item.processingMode,
pattern: item.rule.pattern,
error: error.message,
deliveryTime
},
success: false
});
} finally {
// Remove from active deliveries
this.activeDeliveries.delete(item.id);
this.stats.activeDeliveries = this.activeDeliveries.size;
// Update statistics
this.emit('statsUpdated', this.stats);
}
}
/**
* Default handler for forward mode delivery
* @param item Queue item
*/
private async handleForwardDelivery(item: IQueueItem): Promise<any> {
logger.log('info', `Forward delivery for item ${item.id}`);
const email = item.processingResult as Email;
const rule = item.rule;
// Get target server information
const targetServer = rule.target?.server;
const targetPort = rule.target?.port || 25;
const useTls = rule.target?.useTls ?? false;
if (!targetServer) {
throw new Error('No target server configured for forward mode');
}
logger.log('info', `Forwarding email to ${targetServer}:${targetPort}, TLS: ${useTls}`);
// Create a socket connection to the target server
const socket = new net.Socket();
// Set timeout
socket.setTimeout(this.options.socketTimeout);
try {
// Connect to the target server
await new Promise<void>((resolve, reject) => {
// Handle connection events
socket.on('connect', () => {
logger.log('debug', `Connected to ${targetServer}:${targetPort}`);
resolve();
});
socket.on('timeout', () => {
reject(new Error(`Connection timeout to ${targetServer}:${targetPort}`));
});
socket.on('error', (err) => {
reject(new Error(`Connection error to ${targetServer}:${targetPort}: ${err.message}`));
});
// Connect to the server
socket.connect({
host: targetServer,
port: targetPort
});
});
// Send EHLO
await this.smtpCommand(socket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`);
// Start TLS if required
if (useTls) {
await this.smtpCommand(socket, 'STARTTLS');
// Upgrade to TLS
const tlsSocket = await this.upgradeTls(socket, targetServer);
// Send EHLO again after STARTTLS
await this.smtpCommand(tlsSocket, `EHLO ${rule.mtaOptions?.domain || 'localhost'}`);
// Use tlsSocket for remaining commands
return this.completeSMTPExchange(tlsSocket, email, rule);
}
// Complete the SMTP exchange
return this.completeSMTPExchange(socket, email, rule);
} catch (error: any) {
logger.log('error', `Failed to forward email: ${error.message}`);
// Close the connection
socket.destroy();
throw error;
}
}
/**
* Complete the SMTP exchange after connection and initial setup
* @param socket Network socket
* @param email Email to send
* @param rule Domain rule
*/
private async completeSMTPExchange(socket: net.Socket | tls.TLSSocket, email: Email, rule: IDomainRule): Promise<any> {
try {
// Authenticate if credentials provided
if (rule.target?.authentication?.user && rule.target?.authentication?.pass) {
// Send AUTH LOGIN
await this.smtpCommand(socket, 'AUTH LOGIN');
// Send username (base64)
const username = Buffer.from(rule.target.authentication.user).toString('base64');
await this.smtpCommand(socket, username);
// Send password (base64)
const password = Buffer.from(rule.target.authentication.pass).toString('base64');
await this.smtpCommand(socket, password);
}
// Send MAIL FROM
await this.smtpCommand(socket, `MAIL FROM:<${email.from}>`);
// Send RCPT TO for each recipient
for (const recipient of email.getAllRecipients()) {
await this.smtpCommand(socket, `RCPT TO:<${recipient}>`);
}
// Send DATA
await this.smtpCommand(socket, 'DATA');
// Send email content (simplified)
const emailContent = await this.getFormattedEmail(email);
await this.smtpData(socket, emailContent);
// Send QUIT
await this.smtpCommand(socket, 'QUIT');
// Close the connection
socket.end();
logger.log('info', `Email forwarded successfully to ${rule.target?.server}:${rule.target?.port || 25}`);
return {
targetServer: rule.target?.server,
targetPort: rule.target?.port || 25,
recipients: email.getAllRecipients().length
};
} catch (error: any) {
logger.log('error', `Failed to forward email: ${error.message}`);
// Close the connection
socket.destroy();
throw error;
}
}
/**
* Default handler for MTA mode delivery
* @param item Queue item
*/
private async handleMtaDelivery(item: IQueueItem): Promise<any> {
logger.log('info', `MTA delivery for item ${item.id}`);
const email = item.processingResult as Email;
const rule = item.rule;
try {
// In a full implementation, this would use the MTA service
// For now, we'll simulate a successful delivery
logger.log('info', `Email processed by MTA: ${email.subject} to ${email.getAllRecipients().join(', ')}`);
// Apply MTA rule options if provided
if (rule.mtaOptions) {
const options = rule.mtaOptions;
// Apply DKIM signing if enabled
if (options.dkimSign && options.dkimOptions) {
// Sign the email with DKIM
logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`);
// In a full implementation, this would use the DKIM signing library
}
}
// Simulate successful delivery
return {
recipients: email.getAllRecipients().length,
subject: email.subject,
dkimSigned: !!rule.mtaOptions?.dkimSign
};
} catch (error: any) {
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
throw error;
}
}
/**
* Default handler for process mode delivery
* @param item Queue item
*/
private async handleProcessDelivery(item: IQueueItem): Promise<any> {
logger.log('info', `Process delivery for item ${item.id}`);
const email = item.processingResult as Email;
const rule = item.rule;
try {
// Apply content scanning if enabled
if (rule.contentScanning && rule.scanners && rule.scanners.length > 0) {
logger.log('info', 'Performing content scanning');
// Apply each scanner
for (const scanner of rule.scanners) {
switch (scanner.type) {
case 'spam':
logger.log('info', 'Scanning for spam content');
// Implement spam scanning
break;
case 'virus':
logger.log('info', 'Scanning for virus content');
// Implement virus scanning
break;
case 'attachment':
logger.log('info', 'Scanning attachments');
// Check for blocked extensions
if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
for (const attachment of email.attachments) {
const ext = this.getFileExtension(attachment.filename);
if (scanner.blockedExtensions.includes(ext)) {
if (scanner.action === 'reject') {
throw new Error(`Blocked attachment type: ${ext}`);
} else { // tag
email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
}
}
}
}
break;
}
}
}
// Apply transformations if defined
if (rule.transformations && rule.transformations.length > 0) {
logger.log('info', 'Applying email transformations');
for (const transform of rule.transformations) {
switch (transform.type) {
case 'addHeader':
if (transform.header && transform.value) {
email.addHeader(transform.header, transform.value);
}
break;
}
}
}
logger.log('info', `Email successfully processed in store-and-forward mode`);
// Simulate successful delivery
return {
recipients: email.getAllRecipients().length,
subject: email.subject,
scanned: !!rule.contentScanning,
transformed: !!(rule.transformations && rule.transformations.length > 0)
};
} catch (error: any) {
logger.log('error', `Failed to process email: ${error.message}`);
throw error;
}
}
/**
* Get file extension from filename
*/
private getFileExtension(filename: string): string {
return filename.substring(filename.lastIndexOf('.')).toLowerCase();
}
/**
* Format email for SMTP transmission
* @param email Email to format
*/
private async getFormattedEmail(email: Email): Promise<string> {
// This is a simplified implementation
// In a full implementation, this would use proper MIME formatting
let content = '';
// Add headers
content += `From: ${email.from}\r\n`;
content += `To: ${email.to.join(', ')}\r\n`;
content += `Subject: ${email.subject}\r\n`;
// Add additional headers
for (const [name, value] of Object.entries(email.headers || {})) {
content += `${name}: ${value}\r\n`;
}
// Add content type for multipart
if (email.attachments && email.attachments.length > 0) {
const boundary = `----_=_NextPart_${Math.random().toString(36).substr(2)}`;
content += `MIME-Version: 1.0\r\n`;
content += `Content-Type: multipart/mixed; boundary="${boundary}"\r\n`;
content += `\r\n`;
// Add text part
content += `--${boundary}\r\n`;
content += `Content-Type: text/plain; charset="UTF-8"\r\n`;
content += `\r\n`;
content += `${email.text}\r\n`;
// Add HTML part if present
if (email.html) {
content += `--${boundary}\r\n`;
content += `Content-Type: text/html; charset="UTF-8"\r\n`;
content += `\r\n`;
content += `${email.html}\r\n`;
}
// Add attachments
for (const attachment of email.attachments) {
content += `--${boundary}\r\n`;
content += `Content-Type: ${attachment.contentType || 'application/octet-stream'}; name="${attachment.filename}"\r\n`;
content += `Content-Disposition: attachment; filename="${attachment.filename}"\r\n`;
content += `Content-Transfer-Encoding: base64\r\n`;
content += `\r\n`;
// Add base64 encoded content
const base64Content = attachment.content.toString('base64');
// Split into lines of 76 characters
for (let i = 0; i < base64Content.length; i += 76) {
content += base64Content.substring(i, i + 76) + '\r\n';
}
}
// End boundary
content += `--${boundary}--\r\n`;
} else {
// Simple email with just text
content += `Content-Type: text/plain; charset="UTF-8"\r\n`;
content += `\r\n`;
content += `${email.text}\r\n`;
}
return content;
}
/**
* Send SMTP command and wait for response
* @param socket Socket connection
* @param command SMTP command to send
*/
private async smtpCommand(socket: net.Socket, command: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const onData = (data: Buffer) => {
const response = data.toString().trim();
// Clean up listeners
socket.removeListener('data', onData);
socket.removeListener('error', onError);
socket.removeListener('timeout', onTimeout);
// Check response code
if (response.charAt(0) === '2' || response.charAt(0) === '3') {
resolve(response);
} else {
reject(new Error(`SMTP error: ${response}`));
}
};
const onError = (err: Error) => {
// Clean up listeners
socket.removeListener('data', onData);
socket.removeListener('error', onError);
socket.removeListener('timeout', onTimeout);
reject(err);
};
const onTimeout = () => {
// Clean up listeners
socket.removeListener('data', onData);
socket.removeListener('error', onError);
socket.removeListener('timeout', onTimeout);
reject(new Error('SMTP command timeout'));
};
// Set up listeners
socket.once('data', onData);
socket.once('error', onError);
socket.once('timeout', onTimeout);
// Send command
socket.write(command + '\r\n');
});
}
/**
* Send SMTP DATA command with content
* @param socket Socket connection
* @param data Email content to send
*/
private async smtpData(socket: net.Socket, data: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const onData = (responseData: Buffer) => {
const response = responseData.toString().trim();
// Clean up listeners
socket.removeListener('data', onData);
socket.removeListener('error', onError);
socket.removeListener('timeout', onTimeout);
// Check response code
if (response.charAt(0) === '2') {
resolve(response);
} else {
reject(new Error(`SMTP error: ${response}`));
}
};
const onError = (err: Error) => {
// Clean up listeners
socket.removeListener('data', onData);
socket.removeListener('error', onError);
socket.removeListener('timeout', onTimeout);
reject(err);
};
const onTimeout = () => {
// Clean up listeners
socket.removeListener('data', onData);
socket.removeListener('error', onError);
socket.removeListener('timeout', onTimeout);
reject(new Error('SMTP data timeout'));
};
// Set up listeners
socket.once('data', onData);
socket.once('error', onError);
socket.once('timeout', onTimeout);
// Send data and end with CRLF.CRLF
socket.write(data + '\r\n.\r\n');
});
}
/**
* Upgrade socket to TLS
* @param socket Socket connection
* @param hostname Target hostname for TLS
*/
private async upgradeTls(socket: net.Socket, hostname: string): Promise<tls.TLSSocket> {
return new Promise<tls.TLSSocket>((resolve, reject) => {
const tlsOptions: tls.ConnectionOptions = {
socket,
servername: hostname,
rejectUnauthorized: this.options.verifyCertificates,
minVersion: this.options.tlsMinVersion as tls.SecureVersion
};
const tlsSocket = tls.connect(tlsOptions);
tlsSocket.once('secureConnect', () => {
resolve(tlsSocket);
});
tlsSocket.once('error', (err) => {
reject(new Error(`TLS error: ${err.message}`));
});
tlsSocket.setTimeout(this.options.socketTimeout);
tlsSocket.once('timeout', () => {
reject(new Error('TLS connection timeout'));
});
});
}
/**
* Update delivery time statistics
*/
private updateDeliveryTimeStats(): void {
if (this.deliveryTimes.length === 0) return;
// Keep only the last 1000 delivery times
if (this.deliveryTimes.length > 1000) {
this.deliveryTimes = this.deliveryTimes.slice(-1000);
}
// Calculate average
const sum = this.deliveryTimes.reduce((acc, time) => acc + time, 0);
this.stats.avgDeliveryTime = sum / this.deliveryTimes.length;
}
/**
* Check if rate limit is exceeded
* @returns True if rate limited, false otherwise
*/
private checkRateLimit(): boolean {
const now = Date.now();
const elapsed = now - this.rateLimitLastCheck;
// Reset counter if more than a minute has passed
if (elapsed >= 60000) {
this.rateLimitLastCheck = now;
this.rateLimitCounter = 0;
this.throttled = false;
this.stats.rateLimiting.currentRate = 0;
return false;
}
// Check if we're already throttled
if (this.throttled) {
return true;
}
// Increment counter
this.rateLimitCounter++;
// Calculate current rate (emails per minute)
const rate = (this.rateLimitCounter / elapsed) * 60000;
this.stats.rateLimiting.currentRate = rate;
// Check if rate limit is exceeded
if (rate > this.options.globalRateLimit) {
this.throttled = true;
this.stats.rateLimiting.throttled++;
// Schedule throttle reset
const resetDelay = 60000 - elapsed;
setTimeout(() => {
this.throttled = false;
this.rateLimitLastCheck = Date.now();
this.rateLimitCounter = 0;
this.stats.rateLimiting.currentRate = 0;
}, resetDelay);
return true;
}
return false;
}
/**
* Update delivery options
* @param options New options
*/
public updateOptions(options: Partial<IMultiModeDeliveryOptions>): void {
this.options = {
...this.options,
...options
};
// Update rate limit statistics
if (options.globalRateLimit) {
this.stats.rateLimiting.globalLimit = options.globalRateLimit;
}
logger.log('info', 'MultiModeDeliverySystem options updated');
}
/**
* Get delivery statistics
*/
public getStats(): IDeliveryStats {
return { ...this.stats };
}
}

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Email } from './classes.email.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
import { EmailSignJob } from './classes.emailsignjob.js';
import type { MtaService } from './classes.mta.js';

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import type { MtaService } from './classes.mta.js';
interface Headers {

View File

@ -1,20 +1,20 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from './classes.email.js';
import { Email } from '../core/classes.email.js';
import { EmailSendJob, DeliveryStatus } from './classes.emailsendjob.js';
import { DKIMCreator } from './classes.dkimcreator.js';
import { DKIMVerifier } from './classes.dkimverifier.js';
import { SpfVerifier } from './classes.spfverifier.js';
import { DmarcVerifier } from './classes.dmarcverifier.js';
import { DKIMCreator } from '../security/classes.dkimcreator.js';
import { DKIMVerifier } from '../security/classes.dkimverifier.js';
import { SpfVerifier } from '../security/classes.spfverifier.js';
import { DmarcVerifier } from '../security/classes.dmarcverifier.js';
import { SMTPServer, type ISmtpServerOptions } from './classes.smtpserver.js';
import { DNSManager } from './classes.dnsmanager.js';
import { ApiManager } from './classes.apimanager.js';
import { DNSManager } from '../routing/classes.dnsmanager.js';
import { ApiManager } from '../services/classes.apimanager.js';
import { RateLimiter, type IRateLimitConfig } from './classes.ratelimiter.js';
import { ContentScanner } from '../security/classes.contentscanner.js';
import { IPWarmupManager } from '../deliverability/classes.ipwarmupmanager.js';
import { SenderReputationMonitor } from '../deliverability/classes.senderreputationmonitor.js';
import type { SzPlatformService } from '../platformservice.js';
import { ContentScanner } from '../../security/classes.contentscanner.js';
import { IPWarmupManager } from '../../deliverability/classes.ipwarmupmanager.js';
import { SenderReputationMonitor } from '../../deliverability/classes.senderreputationmonitor.js';
import type { SzPlatformService } from '../../platformservice.js';
/**
* Configuration options for the MTA service
@ -265,7 +265,7 @@ export class MtaService {
this.dkimCreator = new DKIMCreator(this);
this.dkimVerifier = new DKIMVerifier(this);
this.dnsManager = new DNSManager(this);
this.apiManager = new ApiManager();
// Initialize API manager later in start() method when emailService is available
// Initialize authentication verifiers
this.spfVerifier = new SpfVerifier(this);
@ -283,14 +283,15 @@ export class MtaService {
burstTokens: 5 // Allow small bursts
});
// Initialize IP warmup manager
const warmupConfig = this.config.outbound?.warmup;
this.ipWarmupManager = IPWarmupManager.getInstance({
enabled: warmupConfig?.enabled || false,
ipAddresses: warmupConfig?.ipAddresses || [],
targetDomains: warmupConfig?.targetDomains || [],
fallbackPercentage: warmupConfig?.fallbackPercentage || 50
});
// Initialize IP warmup manager with explicit config
const warmupConfig = this.config.outbound?.warmup || {};
const ipWarmupConfig = {
enabled: warmupConfig.enabled || false,
ipAddresses: warmupConfig.ipAddresses || [],
targetDomains: warmupConfig.targetDomains || [],
fallbackPercentage: warmupConfig.fallbackPercentage || 50
};
this.ipWarmupManager = IPWarmupManager.getInstance(ipWarmupConfig);
// Set active allocation policy if specified
if (warmupConfig?.allocationPolicy) {
@ -432,6 +433,9 @@ export class MtaService {
try {
console.log('Starting MTA service...');
// Initialize API manager now that emailService is available
this.apiManager = new ApiManager(this.emailService);
// Load or provision certificate
await this.loadOrProvisionCertificate();
@ -755,7 +759,7 @@ export class MtaService {
console.log(`Processing bounce notification from ${email.from}`);
// Convert to Smartmail for bounce processing
const smartmail = await email.toSmartmail();
const smartmail = await email.toSmartmailBasic();
// If we have a bounce manager available, process it
if (this.emailService?.bounceManager) {

View File

@ -1,4 +1,4 @@
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
/**
* Configuration options for rate limiter

View File

@ -1,15 +1,15 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Email } from './classes.email.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
import type { MtaService } from './classes.mta.js';
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType,
IPReputationChecker,
ReputationThreshold
} from '../security/index.js';
} from '../../security/index.js';
export interface ISmtpServerOptions {
port: number;

View File

@ -0,0 +1,897 @@
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* Interface for rate limit configuration
*/
export interface IRateLimitConfig {
maxMessagesPerMinute?: number;
maxRecipientsPerMessage?: number;
maxConnectionsPerIP?: number;
maxErrorsPerIP?: number;
maxAuthFailuresPerIP?: number;
blockDuration?: number; // in milliseconds
}
/**
* Interface for hierarchical rate limits
*/
export interface IHierarchicalRateLimits {
// Global rate limits (applied to all traffic)
global: IRateLimitConfig;
// Pattern-specific rate limits (applied to matching patterns)
patterns?: Record<string, IRateLimitConfig>;
// IP-specific rate limits (applied to specific IPs)
ips?: Record<string, IRateLimitConfig>;
// Temporary blocks list and their expiry times
blocks?: Record<string, number>; // IP to expiry timestamp
}
/**
* Counter interface for rate limiting
*/
interface ILimitCounter {
count: number;
lastReset: number;
recipients: number;
errors: number;
authFailures: number;
connections: number;
}
/**
* Rate limiter statistics
*/
export interface IRateLimiterStats {
activeCounters: number;
totalBlocked: number;
currentlyBlocked: number;
byPattern: Record<string, {
messagesPerMinute: number;
totalMessages: number;
totalBlocked: number;
}>;
byIp: Record<string, {
messagesPerMinute: number;
totalMessages: number;
totalBlocked: number;
connections: number;
errors: number;
authFailures: number;
blocked: boolean;
}>;
}
/**
* Result of a rate limit check
*/
export interface IRateLimitResult {
allowed: boolean;
reason?: string;
limit?: number;
current?: number;
resetIn?: number; // milliseconds until reset
}
/**
* Unified rate limiter for all email processing modes
*/
export class UnifiedRateLimiter extends EventEmitter {
private config: IHierarchicalRateLimits;
private counters: Map<string, ILimitCounter> = new Map();
private patternCounters: Map<string, ILimitCounter> = new Map();
private ipCounters: Map<string, ILimitCounter> = new Map();
private cleanupInterval?: NodeJS.Timeout;
private stats: IRateLimiterStats;
/**
* Create a new unified rate limiter
* @param config Rate limit configuration
*/
constructor(config: IHierarchicalRateLimits) {
super();
// Set default configuration
this.config = {
global: {
maxMessagesPerMinute: config.global.maxMessagesPerMinute || 100,
maxRecipientsPerMessage: config.global.maxRecipientsPerMessage || 100,
maxConnectionsPerIP: config.global.maxConnectionsPerIP || 20,
maxErrorsPerIP: config.global.maxErrorsPerIP || 10,
maxAuthFailuresPerIP: config.global.maxAuthFailuresPerIP || 5,
blockDuration: config.global.blockDuration || 3600000 // 1 hour
},
patterns: config.patterns || {},
ips: config.ips || {},
blocks: config.blocks || {}
};
// Initialize statistics
this.stats = {
activeCounters: 0,
totalBlocked: 0,
currentlyBlocked: 0,
byPattern: {},
byIp: {}
};
// Start cleanup interval
this.startCleanupInterval();
}
/**
* Start the cleanup interval
*/
private startCleanupInterval(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
// Run cleanup every minute
this.cleanupInterval = setInterval(() => this.cleanup(), 60000);
}
/**
* Stop the cleanup interval
*/
public stop(): void {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
}
/**
* Clean up expired counters and blocks
*/
private cleanup(): void {
const now = Date.now();
// Clean up expired blocks
if (this.config.blocks) {
for (const [ip, expiry] of Object.entries(this.config.blocks)) {
if (expiry <= now) {
delete this.config.blocks[ip];
logger.log('info', `Rate limit block expired for IP ${ip}`);
// Update statistics
if (this.stats.byIp[ip]) {
this.stats.byIp[ip].blocked = false;
}
this.stats.currentlyBlocked--;
}
}
}
// Clean up old counters (older than 10 minutes)
const cutoff = now - 600000;
// Clean global counters
for (const [key, counter] of this.counters.entries()) {
if (counter.lastReset < cutoff) {
this.counters.delete(key);
}
}
// Clean pattern counters
for (const [key, counter] of this.patternCounters.entries()) {
if (counter.lastReset < cutoff) {
this.patternCounters.delete(key);
}
}
// Clean IP counters
for (const [key, counter] of this.ipCounters.entries()) {
if (counter.lastReset < cutoff) {
this.ipCounters.delete(key);
}
}
// Update statistics
this.updateStats();
}
/**
* Check if a message is allowed by rate limits
* @param email Email address
* @param ip IP address
* @param recipients Number of recipients
* @param pattern Matched pattern
* @returns Result of rate limit check
*/
public checkMessageLimit(email: string, ip: string, recipients: number, pattern?: string): IRateLimitResult {
// Check if IP is blocked
if (this.isIpBlocked(ip)) {
return {
allowed: false,
reason: 'IP is blocked',
resetIn: this.getBlockReleaseTime(ip)
};
}
// Check global message rate limit
const globalResult = this.checkGlobalMessageLimit(email);
if (!globalResult.allowed) {
return globalResult;
}
// Check pattern-specific limit if pattern is provided
if (pattern) {
const patternResult = this.checkPatternMessageLimit(pattern);
if (!patternResult.allowed) {
return patternResult;
}
}
// Check IP-specific limit
const ipResult = this.checkIpMessageLimit(ip);
if (!ipResult.allowed) {
return ipResult;
}
// Check recipient limit
const recipientResult = this.checkRecipientLimit(email, recipients, pattern);
if (!recipientResult.allowed) {
return recipientResult;
}
// All checks passed
return { allowed: true };
}
/**
* Check global message rate limit
* @param email Email address
*/
private checkGlobalMessageLimit(email: string): IRateLimitResult {
const now = Date.now();
const limit = this.config.global.maxMessagesPerMinute!;
if (!limit) {
return { allowed: true };
}
// Get or create counter
const key = 'global';
let counter = this.counters.get(key);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.counters.set(key, counter);
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.count = 0;
counter.lastReset = now;
}
// Check if limit is exceeded
if (counter.count >= limit) {
// Calculate reset time
const resetIn = 60000 - (now - counter.lastReset);
return {
allowed: false,
reason: 'Global message rate limit exceeded',
limit,
current: counter.count,
resetIn
};
}
// Increment counter
counter.count++;
// Update statistics
this.updateStats();
return { allowed: true };
}
/**
* Check pattern-specific message rate limit
* @param pattern Pattern to check
*/
private checkPatternMessageLimit(pattern: string): IRateLimitResult {
const now = Date.now();
// Get pattern-specific limit or use global
const patternConfig = this.config.patterns?.[pattern];
const limit = patternConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!;
if (!limit) {
return { allowed: true };
}
// Get or create counter
let counter = this.patternCounters.get(pattern);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.patternCounters.set(pattern, counter);
// Initialize pattern stats if needed
if (!this.stats.byPattern[pattern]) {
this.stats.byPattern[pattern] = {
messagesPerMinute: 0,
totalMessages: 0,
totalBlocked: 0
};
}
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.count = 0;
counter.lastReset = now;
}
// Check if limit is exceeded
if (counter.count >= limit) {
// Calculate reset time
const resetIn = 60000 - (now - counter.lastReset);
// Update statistics
this.stats.byPattern[pattern].totalBlocked++;
this.stats.totalBlocked++;
return {
allowed: false,
reason: `Pattern "${pattern}" message rate limit exceeded`,
limit,
current: counter.count,
resetIn
};
}
// Increment counter
counter.count++;
// Update statistics
this.stats.byPattern[pattern].messagesPerMinute = counter.count;
this.stats.byPattern[pattern].totalMessages++;
return { allowed: true };
}
/**
* Check IP-specific message rate limit
* @param ip IP address
*/
private checkIpMessageLimit(ip: string): IRateLimitResult {
const now = Date.now();
// Get IP-specific limit or use global
const ipConfig = this.config.ips?.[ip];
const limit = ipConfig?.maxMessagesPerMinute || this.config.global.maxMessagesPerMinute!;
if (!limit) {
return { allowed: true };
}
// Get or create counter
let counter = this.ipCounters.get(ip);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.ipCounters.set(ip, counter);
// Initialize IP stats if needed
if (!this.stats.byIp[ip]) {
this.stats.byIp[ip] = {
messagesPerMinute: 0,
totalMessages: 0,
totalBlocked: 0,
connections: 0,
errors: 0,
authFailures: 0,
blocked: false
};
}
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.count = 0;
counter.lastReset = now;
}
// Check if limit is exceeded
if (counter.count >= limit) {
// Calculate reset time
const resetIn = 60000 - (now - counter.lastReset);
// Update statistics
this.stats.byIp[ip].totalBlocked++;
this.stats.totalBlocked++;
return {
allowed: false,
reason: `IP ${ip} message rate limit exceeded`,
limit,
current: counter.count,
resetIn
};
}
// Increment counter
counter.count++;
// Update statistics
this.stats.byIp[ip].messagesPerMinute = counter.count;
this.stats.byIp[ip].totalMessages++;
return { allowed: true };
}
/**
* Check recipient limit
* @param email Email address
* @param recipients Number of recipients
* @param pattern Matched pattern
*/
private checkRecipientLimit(email: string, recipients: number, pattern?: string): IRateLimitResult {
// Get pattern-specific limit if available
let limit = this.config.global.maxRecipientsPerMessage!;
if (pattern && this.config.patterns?.[pattern]?.maxRecipientsPerMessage) {
limit = this.config.patterns[pattern].maxRecipientsPerMessage!;
}
if (!limit) {
return { allowed: true };
}
// Check if limit is exceeded
if (recipients > limit) {
return {
allowed: false,
reason: 'Recipient limit exceeded',
limit,
current: recipients
};
}
return { allowed: true };
}
/**
* Record a connection from an IP
* @param ip IP address
* @returns Result of rate limit check
*/
public recordConnection(ip: string): IRateLimitResult {
const now = Date.now();
// Check if IP is blocked
if (this.isIpBlocked(ip)) {
return {
allowed: false,
reason: 'IP is blocked',
resetIn: this.getBlockReleaseTime(ip)
};
}
// Get IP-specific limit or use global
const ipConfig = this.config.ips?.[ip];
const limit = ipConfig?.maxConnectionsPerIP || this.config.global.maxConnectionsPerIP!;
if (!limit) {
return { allowed: true };
}
// Get or create counter
let counter = this.ipCounters.get(ip);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.ipCounters.set(ip, counter);
// Initialize IP stats if needed
if (!this.stats.byIp[ip]) {
this.stats.byIp[ip] = {
messagesPerMinute: 0,
totalMessages: 0,
totalBlocked: 0,
connections: 0,
errors: 0,
authFailures: 0,
blocked: false
};
}
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.connections = 0;
counter.lastReset = now;
}
// Check if limit is exceeded
if (counter.connections >= limit) {
// Calculate reset time
const resetIn = 60000 - (now - counter.lastReset);
// Update statistics
this.stats.byIp[ip].totalBlocked++;
this.stats.totalBlocked++;
return {
allowed: false,
reason: `IP ${ip} connection rate limit exceeded`,
limit,
current: counter.connections,
resetIn
};
}
// Increment counter
counter.connections++;
// Update statistics
this.stats.byIp[ip].connections = counter.connections;
return { allowed: true };
}
/**
* Record an error from an IP
* @param ip IP address
* @returns True if IP should be blocked
*/
public recordError(ip: string): boolean {
const now = Date.now();
// Get IP-specific limit or use global
const ipConfig = this.config.ips?.[ip];
const limit = ipConfig?.maxErrorsPerIP || this.config.global.maxErrorsPerIP!;
if (!limit) {
return false;
}
// Get or create counter
let counter = this.ipCounters.get(ip);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.ipCounters.set(ip, counter);
// Initialize IP stats if needed
if (!this.stats.byIp[ip]) {
this.stats.byIp[ip] = {
messagesPerMinute: 0,
totalMessages: 0,
totalBlocked: 0,
connections: 0,
errors: 0,
authFailures: 0,
blocked: false
};
}
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.errors = 0;
counter.lastReset = now;
}
// Increment counter
counter.errors++;
// Update statistics
this.stats.byIp[ip].errors = counter.errors;
// Check if limit is exceeded
if (counter.errors >= limit) {
// Block the IP
this.blockIp(ip);
logger.log('warn', `IP ${ip} blocked due to excessive errors (${counter.errors}/${limit})`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.RATE_LIMITING,
message: 'IP blocked due to excessive errors',
ipAddress: ip,
details: {
errors: counter.errors,
limit
},
success: false
});
return true;
}
return false;
}
/**
* Record an authentication failure from an IP
* @param ip IP address
* @returns True if IP should be blocked
*/
public recordAuthFailure(ip: string): boolean {
const now = Date.now();
// Get IP-specific limit or use global
const ipConfig = this.config.ips?.[ip];
const limit = ipConfig?.maxAuthFailuresPerIP || this.config.global.maxAuthFailuresPerIP!;
if (!limit) {
return false;
}
// Get or create counter
let counter = this.ipCounters.get(ip);
if (!counter) {
counter = {
count: 0,
lastReset: now,
recipients: 0,
errors: 0,
authFailures: 0,
connections: 0
};
this.ipCounters.set(ip, counter);
// Initialize IP stats if needed
if (!this.stats.byIp[ip]) {
this.stats.byIp[ip] = {
messagesPerMinute: 0,
totalMessages: 0,
totalBlocked: 0,
connections: 0,
errors: 0,
authFailures: 0,
blocked: false
};
}
}
// Check if counter needs to be reset
if (now - counter.lastReset >= 60000) {
counter.authFailures = 0;
counter.lastReset = now;
}
// Increment counter
counter.authFailures++;
// Update statistics
this.stats.byIp[ip].authFailures = counter.authFailures;
// Check if limit is exceeded
if (counter.authFailures >= limit) {
// Block the IP
this.blockIp(ip);
logger.log('warn', `IP ${ip} blocked due to excessive authentication failures (${counter.authFailures}/${limit})`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.AUTHENTICATION,
message: 'IP blocked due to excessive authentication failures',
ipAddress: ip,
details: {
authFailures: counter.authFailures,
limit
},
success: false
});
return true;
}
return false;
}
/**
* Block an IP address
* @param ip IP address to block
* @param duration Override the default block duration (milliseconds)
*/
public blockIp(ip: string, duration?: number): void {
if (!this.config.blocks) {
this.config.blocks = {};
}
// Set block expiry time
const expiry = Date.now() + (duration || this.config.global.blockDuration || 3600000);
this.config.blocks[ip] = expiry;
// Update statistics
if (!this.stats.byIp[ip]) {
this.stats.byIp[ip] = {
messagesPerMinute: 0,
totalMessages: 0,
totalBlocked: 0,
connections: 0,
errors: 0,
authFailures: 0,
blocked: false
};
}
this.stats.byIp[ip].blocked = true;
this.stats.currentlyBlocked++;
// Emit event
this.emit('ipBlocked', {
ip,
expiry,
duration: duration || this.config.global.blockDuration
});
logger.log('warn', `IP ${ip} blocked until ${new Date(expiry).toISOString()}`);
}
/**
* Unblock an IP address
* @param ip IP address to unblock
*/
public unblockIp(ip: string): void {
if (!this.config.blocks) {
return;
}
// Remove block
delete this.config.blocks[ip];
// Update statistics
if (this.stats.byIp[ip]) {
this.stats.byIp[ip].blocked = false;
this.stats.currentlyBlocked--;
}
// Emit event
this.emit('ipUnblocked', { ip });
logger.log('info', `IP ${ip} unblocked`);
}
/**
* Check if an IP is blocked
* @param ip IP address to check
*/
public isIpBlocked(ip: string): boolean {
if (!this.config.blocks) {
return false;
}
// Check if IP is in blocks
if (!(ip in this.config.blocks)) {
return false;
}
// Check if block has expired
const expiry = this.config.blocks[ip];
if (expiry <= Date.now()) {
// Remove expired block
delete this.config.blocks[ip];
// Update statistics
if (this.stats.byIp[ip]) {
this.stats.byIp[ip].blocked = false;
this.stats.currentlyBlocked--;
}
return false;
}
return true;
}
/**
* Get the time until a block is released
* @param ip IP address
* @returns Milliseconds until release or 0 if not blocked
*/
public getBlockReleaseTime(ip: string): number {
if (!this.config.blocks || !(ip in this.config.blocks)) {
return 0;
}
const expiry = this.config.blocks[ip];
const now = Date.now();
return expiry > now ? expiry - now : 0;
}
/**
* Update rate limiter statistics
*/
private updateStats(): void {
// Update active counters count
this.stats.activeCounters = this.counters.size + this.patternCounters.size + this.ipCounters.size;
// Emit statistics update
this.emit('statsUpdated', this.stats);
}
/**
* Get rate limiter statistics
*/
public getStats(): IRateLimiterStats {
return { ...this.stats };
}
/**
* Update rate limiter configuration
* @param config New configuration
*/
public updateConfig(config: Partial<IHierarchicalRateLimits>): void {
if (config.global) {
this.config.global = {
...this.config.global,
...config.global
};
}
if (config.patterns) {
this.config.patterns = {
...this.config.patterns,
...config.patterns
};
}
if (config.ips) {
this.config.ips = {
...this.config.ips,
...config.ips
};
}
logger.log('info', 'Rate limiter configuration updated');
}
/**
* Get configuration for debugging
*/
public getConfig(): IHierarchicalRateLimits {
return { ...this.config };
}
}

18
ts/mail/delivery/index.ts Normal file
View File

@ -0,0 +1,18 @@
// Email delivery components
export * from './classes.mta.js';
export * from './classes.smtpserver.js';
export * from './classes.emailsignjob.js';
export * from './classes.delivery.queue.js';
export * from './classes.delivery.system.js';
// Handle exports with naming conflicts
export { EmailSendJob } from './classes.emailsendjob.js';
export { DeliveryStatus } from './classes.connector.mta.js';
export { MtaConnector } from './classes.connector.mta.js';
// Rate limiter exports - fix naming conflict
export { RateLimiter } from './classes.ratelimiter.js';
export type { IRateLimitConfig } from './classes.ratelimiter.js';
// Unified rate limiter
export * from './classes.unified.rate.limiter.js';

29
ts/mail/index.ts Normal file
View File

@ -0,0 +1,29 @@
// Export all mail modules for simplified imports
export * from './routing/index.js';
export * from './security/index.js';
export * from './services/index.js';
// Make the core and delivery modules accessible
import * as Core from './core/index.js';
import * as Delivery from './delivery/index.js';
export { Core, Delivery };
// For backward compatibility
import { Email } from './core/classes.email.js';
import { EmailService } from './services/classes.emailservice.js';
import { BounceManager, BounceType, BounceCategory } from './core/classes.bouncemanager.js';
import { EmailValidator } from './core/classes.emailvalidator.js';
import { TemplateManager } from './core/classes.templatemanager.js';
import { RuleManager } from './core/classes.rulemanager.js';
import { ApiManager } from './services/classes.apimanager.js';
import { MtaService } from './delivery/classes.mta.js';
import { DcRouter } from '../classes.dcrouter.js';
// Re-export with compatibility names
export {
EmailService as Email, // For backward compatibility with email/index.ts
ApiManager,
Email as EmailClass, // Provide the actual Email class under a different name
DcRouter
};

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import type { MtaService } from './classes.mta.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import type { MtaService } from '../delivery/classes.mta.js';
/**
* Interface for DNS record information

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EventEmitter } from 'node:events';
import { type IDomainRule, type EmailProcessingMode } from './classes.email.config.js';
@ -348,4 +348,22 @@ export class DomainRouter extends EventEmitter {
this.patternCache.clear();
this.emit('cacheCleared');
}
/**
* Update all domain rules at once
* @param rules New set of domain rules to replace existing ones
*/
public updateRules(rules: IDomainRule[]): void {
// Validate all rules
rules.forEach(rule => this.validateRule(rule));
// Replace all rules
this.options.domainRules = [...rules];
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('rulesUpdated', rules);
}
}

View File

@ -1,4 +1,4 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
/**
* Email processing modes

View File

@ -0,0 +1,991 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { EventEmitter } from 'events';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType
} from '../../security/index.js';
import { DomainRouter } from './classes.domain.router.js';
import type {
IEmailConfig,
EmailProcessingMode,
IDomainRule
} from './classes.email.config.js';
import { Email } from '../core/classes.email.js';
import * as net from 'node:net';
import * as tls from 'node:tls';
import * as stream from 'node:stream';
import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js';
/**
* Options for the unified email server
*/
export interface IUnifiedEmailServerOptions {
// Base server options
ports: number[];
hostname: string;
banner?: string;
// Authentication options
auth?: {
required?: boolean;
methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
users?: Array<{username: string, password: string}>;
};
// TLS options
tls?: {
certPath?: string;
keyPath?: string;
caPath?: string;
minVersion?: string;
ciphers?: string;
};
// Limits
maxMessageSize?: number;
maxClients?: number;
maxConnections?: number;
// Connection options
connectionTimeout?: number;
socketTimeout?: number;
// Domain rules
domainRules: IDomainRule[];
// Default handling for unmatched domains
defaultMode: EmailProcessingMode;
defaultServer?: string;
defaultPort?: number;
defaultTls?: boolean;
}
/**
* Interface describing SMTP session data
*/
export interface ISmtpSession {
id: string;
remoteAddress: string;
clientHostname: string;
secure: boolean;
authenticated: boolean;
user?: {
username: string;
[key: string]: any;
};
envelope: {
mailFrom: {
address: string;
args: any;
};
rcptTo: Array<{
address: string;
args: any;
}>;
};
processingMode?: EmailProcessingMode;
matchedRule?: IDomainRule;
}
/**
* Authentication data for SMTP
*/
export interface IAuthData {
method: string;
username: string;
password: string;
}
/**
* Server statistics
*/
export interface IServerStats {
startTime: Date;
connections: {
current: number;
total: number;
};
messages: {
processed: number;
delivered: number;
failed: number;
};
processingTime: {
avg: number;
max: number;
min: number;
};
}
/**
* Unified email server that handles all email traffic with pattern-based routing
*/
export class UnifiedEmailServer extends EventEmitter {
private options: IUnifiedEmailServerOptions;
private domainRouter: DomainRouter;
private servers: MtaSmtpServer[] = [];
private stats: IServerStats;
private processingTimes: number[] = [];
constructor(options: IUnifiedEmailServerOptions) {
super();
// Set default options
this.options = {
...options,
banner: options.banner || `${options.hostname} ESMTP UnifiedEmailServer`,
maxMessageSize: options.maxMessageSize || 10 * 1024 * 1024, // 10MB
maxClients: options.maxClients || 100,
maxConnections: options.maxConnections || 1000,
connectionTimeout: options.connectionTimeout || 60000, // 1 minute
socketTimeout: options.socketTimeout || 60000 // 1 minute
};
// Initialize domain router for pattern matching
this.domainRouter = new DomainRouter({
domainRules: options.domainRules,
defaultMode: options.defaultMode,
defaultServer: options.defaultServer,
defaultPort: options.defaultPort,
defaultTls: options.defaultTls,
enableCache: true,
cacheSize: 1000
});
// Initialize statistics
this.stats = {
startTime: new Date(),
connections: {
current: 0,
total: 0
},
messages: {
processed: 0,
delivered: 0,
failed: 0
},
processingTime: {
avg: 0,
max: 0,
min: 0
}
};
// We'll create the SMTP servers during the start() method
}
/**
* Start the unified email server
*/
public async start(): Promise<void> {
logger.log('info', `Starting UnifiedEmailServer on ports: ${(this.options.ports as number[]).join(', ')}`);
try {
// Ensure we have the necessary TLS options
const hasTlsConfig = this.options.tls?.keyPath && this.options.tls?.certPath;
// Prepare the certificate and key if available
let key: string | undefined;
let cert: string | undefined;
if (hasTlsConfig) {
try {
key = plugins.fs.readFileSync(this.options.tls.keyPath!, 'utf8');
cert = plugins.fs.readFileSync(this.options.tls.certPath!, 'utf8');
logger.log('info', 'TLS certificates loaded successfully');
} catch (error) {
logger.log('warn', `Failed to load TLS certificates: ${error.message}`);
}
}
// Create a SMTP server for each port
for (const port of this.options.ports as number[]) {
// Create a reference object to hold the MTA service during setup
const mtaRef = {
config: {
smtp: {
hostname: this.options.hostname
},
security: {
checkIPReputation: false,
verifyDkim: true,
verifySpf: true,
verifyDmarc: true
}
},
// These will be implemented in the real integration:
dkimVerifier: {
verify: async () => ({ isValid: true, domain: '' })
},
spfVerifier: {
verifyAndApply: async () => true
},
dmarcVerifier: {
verify: async () => ({}),
applyPolicy: () => true
},
processIncomingEmail: async (email: Email) => {
// This is where we'll process the email based on domain routing
const to = email.to[0]; // Email.to is an array, take the first recipient
const rule = this.domainRouter.matchRule(to);
const mode = rule?.mode || this.options.defaultMode;
// Process based on the mode
await this.processEmailByMode(email, {
id: 'session-' + Math.random().toString(36).substring(2),
remoteAddress: '127.0.0.1',
clientHostname: '',
secure: false,
authenticated: false,
envelope: {
mailFrom: { address: email.from, args: {} },
rcptTo: email.to.map(recipient => ({ address: recipient, args: {} }))
},
processingMode: mode,
matchedRule: rule
}, mode);
return true;
}
};
// Create server options
const serverOptions = {
port,
hostname: this.options.hostname,
key,
cert
};
// Create and start the SMTP server
const smtpServer = new MtaSmtpServer(mtaRef as any, serverOptions);
this.servers.push(smtpServer);
// Start the server
await new Promise<void>((resolve, reject) => {
try {
smtpServer.start();
logger.log('info', `UnifiedEmailServer listening on port ${port}`);
// Set up event handlers
(smtpServer as any).server.on('error', (err: Error) => {
logger.log('error', `SMTP server error on port ${port}: ${err.message}`);
this.emit('error', err);
});
resolve();
} catch (err) {
if ((err as any).code === 'EADDRINUSE') {
logger.log('error', `Port ${port} is already in use`);
reject(new Error(`Port ${port} is already in use`));
} else {
logger.log('error', `Error starting server on port ${port}: ${err.message}`);
reject(err);
}
}
});
}
logger.log('info', 'UnifiedEmailServer started successfully');
this.emit('started');
} catch (error) {
logger.log('error', `Failed to start UnifiedEmailServer: ${error.message}`);
throw error;
}
}
/**
* Stop the unified email server
*/
public async stop(): Promise<void> {
logger.log('info', 'Stopping UnifiedEmailServer');
try {
// Stop all SMTP servers
for (const server of this.servers) {
server.stop();
}
// Clear the servers array
this.servers = [];
logger.log('info', 'UnifiedEmailServer stopped successfully');
this.emit('stopped');
} catch (error) {
logger.log('error', `Error stopping UnifiedEmailServer: ${error.message}`);
throw error;
}
}
/**
* Handle new SMTP connection (stub implementation)
*/
private onConnect(session: ISmtpSession, callback: (err?: Error) => void): void {
logger.log('info', `New connection from ${session.remoteAddress}`);
// Update connection statistics
this.stats.connections.current++;
this.stats.connections.total++;
// Log connection event
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.CONNECTION,
message: 'New SMTP connection established',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
secure: session.secure
}
});
// Optional IP reputation check would go here
// Continue with the connection
callback();
}
/**
* Handle authentication (stub implementation)
*/
private onAuth(auth: IAuthData, session: ISmtpSession, callback: (err?: Error, user?: any) => void): void {
if (!this.options.auth || !this.options.auth.users || this.options.auth.users.length === 0) {
// No authentication configured, reject
const error = new Error('Authentication not supported');
logger.log('warn', `Authentication attempt when not configured: ${auth.username}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.AUTHENTICATION,
message: 'Authentication attempt when not configured',
ipAddress: session.remoteAddress,
details: {
username: auth.username,
method: auth.method,
sessionId: session.id
},
success: false
});
return callback(error);
}
// Find matching user
const user = this.options.auth.users.find(u => u.username === auth.username && u.password === auth.password);
if (user) {
logger.log('info', `User ${auth.username} authenticated successfully`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.AUTHENTICATION,
message: 'SMTP authentication successful',
ipAddress: session.remoteAddress,
details: {
username: auth.username,
method: auth.method,
sessionId: session.id
},
success: true
});
return callback(null, { username: user.username });
} else {
const error = new Error('Invalid username or password');
logger.log('warn', `Failed authentication for ${auth.username}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.AUTHENTICATION,
message: 'SMTP authentication failed',
ipAddress: session.remoteAddress,
details: {
username: auth.username,
method: auth.method,
sessionId: session.id
},
success: false
});
return callback(error);
}
}
/**
* Handle MAIL FROM command (stub implementation)
*/
private onMailFrom(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void {
logger.log('info', `MAIL FROM: ${address.address}`);
// Validate the email address
if (!this.isValidEmail(address.address)) {
const error = new Error('Invalid sender address');
logger.log('warn', `Invalid sender address: ${address.address}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.EMAIL_VALIDATION,
message: 'Invalid sender email format',
ipAddress: session.remoteAddress,
details: {
address: address.address,
sessionId: session.id
},
success: false
});
return callback(error);
}
// Authentication check if required
if (this.options.auth?.required && !session.authenticated) {
const error = new Error('Authentication required');
logger.log('warn', `Unauthenticated sender rejected: ${address.address}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.AUTHENTICATION,
message: 'Unauthenticated sender rejected',
ipAddress: session.remoteAddress,
details: {
address: address.address,
sessionId: session.id
},
success: false
});
return callback(error);
}
// Continue processing
callback();
}
/**
* Handle RCPT TO command (stub implementation)
*/
private onRcptTo(address: {address: string}, session: ISmtpSession, callback: (err?: Error) => void): void {
logger.log('info', `RCPT TO: ${address.address}`);
// Validate the email address
if (!this.isValidEmail(address.address)) {
const error = new Error('Invalid recipient address');
logger.log('warn', `Invalid recipient address: ${address.address}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.WARN,
type: SecurityEventType.EMAIL_VALIDATION,
message: 'Invalid recipient email format',
ipAddress: session.remoteAddress,
details: {
address: address.address,
sessionId: session.id
},
success: false
});
return callback(error);
}
// Pattern match the recipient to determine processing mode
const rule = this.domainRouter.matchRule(address.address);
if (rule) {
// Store the matched rule and processing mode in the session
session.matchedRule = rule;
session.processingMode = rule.mode;
logger.log('info', `Email ${address.address} matched rule: ${rule.pattern}, mode: ${rule.mode}`);
} else {
// Use default mode
session.processingMode = this.options.defaultMode;
logger.log('info', `Email ${address.address} using default mode: ${this.options.defaultMode}`);
}
// Continue processing
callback();
}
/**
* Handle incoming email data (stub implementation)
*/
private onData(stream: stream.Readable, session: ISmtpSession, 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
*/
private updateProcessingTimeStats(): void {
if (this.processingTimes.length === 0) return;
// Keep only the last 1000 processing times
if (this.processingTimes.length > 1000) {
this.processingTimes = this.processingTimes.slice(-1000);
}
// Calculate stats
const sum = this.processingTimes.reduce((acc, time) => acc + time, 0);
const avg = sum / this.processingTimes.length;
const max = Math.max(...this.processingTimes);
const min = Math.min(...this.processingTimes);
this.stats.processingTime = { avg, max, min };
}
/**
* Process email based on the determined mode
*/
private async processEmailByMode(emailData: Email | Buffer, session: ISmtpSession, mode: EmailProcessingMode): Promise<Email> {
// Convert Buffer to Email if needed
let email: Email;
if (Buffer.isBuffer(emailData)) {
// Parse the email data buffer into an Email object
try {
const parsed = await plugins.mailparser.simpleParser(emailData);
email = new Email({
from: parsed.from?.value[0]?.address || session.envelope.mailFrom.address,
to: session.envelope.rcptTo[0]?.address || '',
subject: parsed.subject || '',
text: parsed.text || '',
html: parsed.html || undefined,
attachments: parsed.attachments?.map(att => ({
filename: att.filename || '',
content: att.content,
contentType: att.contentType
})) || []
});
} catch (error) {
logger.log('error', `Error parsing email data: ${error.message}`);
throw new Error(`Error parsing email data: ${error.message}`);
}
} else {
email = emailData;
}
// Process based on mode
switch (mode) {
case 'forward':
await this.handleForwardMode(email, session);
break;
case 'mta':
await this.handleMtaMode(email, session);
break;
case 'process':
await this.handleProcessMode(email, session);
break;
default:
throw new Error(`Unknown processing mode: ${mode}`);
}
// Return the processed email
return email;
}
/**
* Handle email in forward mode (SMTP proxy)
*/
private async handleForwardMode(email: Email, session: ISmtpSession): 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)
*/
private async handleMtaMode(email: Email, session: ISmtpSession): Promise<void> {
logger.log('info', `Handling email in MTA mode for session ${session.id}`);
try {
// Apply MTA rule options if provided
if (session.matchedRule?.mtaOptions) {
const options = session.matchedRule.mtaOptions;
// Apply DKIM signing if enabled
if (options.dkimSign && options.dkimOptions) {
// Sign the email with DKIM
logger.log('info', `Signing email with DKIM for domain ${options.dkimOptions.domainName}`);
// In a full implementation, this would use the DKIM signing library
}
}
// Get email content for logging/processing
const subject = email.subject;
const recipients = email.getAllRecipients().join(', ');
logger.log('info', `Email processed by MTA: ${subject} to ${recipients}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processed by MTA',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRule?.pattern || 'default',
subject,
recipients
},
success: true
});
} catch (error) {
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'MTA processing failed',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRule?.pattern || 'default',
error: error.message
},
success: false
});
throw error;
}
}
/**
* Handle email in process mode (store-and-forward with scanning)
*/
private async handleProcessMode(email: Email, session: ISmtpSession): Promise<void> {
logger.log('info', `Handling email in process mode for session ${session.id}`);
try {
const rule = session.matchedRule;
// Apply content scanning if enabled
if (rule?.contentScanning && rule.scanners && rule.scanners.length > 0) {
logger.log('info', 'Performing content scanning');
// Apply each scanner
for (const scanner of rule.scanners) {
switch (scanner.type) {
case 'spam':
logger.log('info', 'Scanning for spam content');
// Implement spam scanning
break;
case 'virus':
logger.log('info', 'Scanning for virus content');
// Implement virus scanning
break;
case 'attachment':
logger.log('info', 'Scanning attachments');
// Check for blocked extensions
if (scanner.blockedExtensions && scanner.blockedExtensions.length > 0) {
for (const attachment of email.attachments) {
const ext = this.getFileExtension(attachment.filename);
if (scanner.blockedExtensions.includes(ext)) {
if (scanner.action === 'reject') {
throw new Error(`Blocked attachment type: ${ext}`);
} else { // tag
email.addHeader('X-Attachment-Warning', `Potentially unsafe attachment: ${attachment.filename}`);
}
}
}
}
break;
}
}
}
// Apply transformations if defined
if (rule?.transformations && rule.transformations.length > 0) {
logger.log('info', 'Applying email transformations');
for (const transform of rule.transformations) {
switch (transform.type) {
case 'addHeader':
if (transform.header && transform.value) {
email.addHeader(transform.header, transform.value);
}
break;
}
}
}
logger.log('info', `Email successfully processed in store-and-forward mode`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.INFO,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processed and queued',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: rule?.pattern || 'default',
contentScanning: rule?.contentScanning || false,
subject: email.subject
},
success: true
});
} catch (error) {
logger.log('error', `Failed to process email: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.EMAIL_PROCESSING,
message: 'Email processing failed',
ipAddress: session.remoteAddress,
details: {
sessionId: session.id,
ruleName: session.matchedRule?.pattern || 'default',
error: error.message
},
success: false
});
throw error;
}
}
/**
* Get file extension from filename
*/
private getFileExtension(filename: string): string {
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;
}
/**
* Update server configuration
*/
public updateOptions(options: Partial<IUnifiedEmailServerOptions>): void {
// Stop the server if changing ports
const portsChanged = options.ports &&
(!this.options.ports ||
JSON.stringify(options.ports) !== JSON.stringify(this.options.ports));
if (portsChanged) {
this.stop().then(() => {
this.options = { ...this.options, ...options };
this.start();
});
} else {
// Update options without restart
this.options = { ...this.options, ...options };
// Update domain router if rules changed
if (options.domainRules) {
this.domainRouter.updateRules(options.domainRules);
}
}
}
/**
* Update domain rules
*/
public updateDomainRules(rules: IDomainRule[]): void {
this.options.domainRules = rules;
this.domainRouter.updateRules(rules);
}
/**
* Get server statistics
*/
public getStats(): IServerStats {
return { ...this.stats };
}
/**
* Validate email address format
*/
private isValidEmail(email: string): boolean {
// Basic validation - a more comprehensive validation could be used
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}

5
ts/mail/routing/index.ts Normal file
View File

@ -0,0 +1,5 @@
// Email routing components
export * from './classes.domain.router.js';
export * from './classes.email.config.js';
export * from './classes.unified.email.server.js';
export * from './classes.dnsmanager.js';

View File

@ -1,8 +1,8 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from './classes.email.js';
import type { MtaService } from './classes.mta.js';
import { Email } from '../core/classes.email.js';
import type { MtaService } from '../delivery/classes.mta.js';
const readFile = plugins.util.promisify(plugins.fs.readFile);
const writeFile = plugins.util.promisify(plugins.fs.writeFile);

View File

@ -1,7 +1,7 @@
import * as plugins from '../plugins.js';
import { MtaService } from './classes.mta.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import * as plugins from '../../plugins.js';
import { MtaService } from '../delivery/classes.mta.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* Result of a DKIM verification

View File

@ -1,9 +1,9 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import type { MtaService } from './classes.mta.js';
import type { Email } from './classes.email.js';
import type { IDnsVerificationResult } from './classes.dnsmanager.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import type { MtaService } from '../delivery/classes.mta.js';
import type { Email } from '../core/classes.email.js';
import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js';
/**
* DMARC policy types

View File

@ -1,9 +1,9 @@
import * as plugins from '../plugins.js';
import { logger } from '../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../security/index.js';
import type { MtaService } from './classes.mta.js';
import type { Email } from './classes.email.js';
import type { IDnsVerificationResult } from './classes.dnsmanager.js';
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import type { MtaService } from '../delivery/classes.mta.js';
import type { Email } from '../core/classes.email.js';
import type { IDnsVerificationResult } from '../routing/classes.dnsmanager.js';
/**
* SPF result qualifiers

View File

@ -0,0 +1,5 @@
// Email security components
export * from './classes.dkimcreator.js';
export * from './classes.dkimverifier.js';
export * from './classes.dmarcverifier.js';
export * from './classes.spfverifier.js';

View File

@ -1,6 +1,6 @@
import * as plugins from '../plugins.js';
import * as plugins from '../../plugins.js';
import { EmailService } from './classes.emailservice.js';
import { logger } from '../logger.js';
import { logger } from '../../logger.js';
export class ApiManager {
public emailRef: EmailService;
@ -69,10 +69,10 @@ export class ApiManager {
return status;
}
// For Mailgun, we don't have a status check implementation currently
// Status tracking not available if MTA is not configured
return {
status: 'unknown',
details: { message: 'Status tracking not available for current provider' }
details: { message: 'Status tracking not available without MTA configuration' }
};
})
);

View File

@ -1,16 +1,16 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { MtaConnector } from './classes.connector.mta.js';
import { RuleManager } from './classes.rulemanager.js';
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { MtaConnector } from '../delivery/classes.connector.mta.js';
import { RuleManager } from '../core/classes.rulemanager.js';
import { ApiManager } from './classes.apimanager.js';
import { TemplateManager } from './classes.templatemanager.js';
import { EmailValidator } from './classes.emailvalidator.js';
import { BounceManager } from './classes.bouncemanager.js';
import { logger } from '../logger.js';
import type { SzPlatformService } from '../platformservice.js';
import { TemplateManager } from '../core/classes.templatemanager.js';
import { EmailValidator } from '../core/classes.emailvalidator.js';
import { BounceManager } from '../core/classes.bouncemanager.js';
import { logger } from '../../logger.js';
import type { SzPlatformService } from '../../platformservice.js';
// Import MTA service
import { MtaService, type IMtaConfig } from '../mta/index.js';
import { MtaService, type IMtaConfig } from '../delivery/classes.mta.js';
export interface IEmailConstructorOptions {
useMta?: boolean;
@ -25,7 +25,7 @@ export interface IEmailConstructorOptions {
}
/**
* Email service with support for both Mailgun and local MTA
* Email service with MTA support
*/
export class EmailService {
public platformServiceRef: SzPlatformService;
@ -128,7 +128,7 @@ export class EmailService {
}
/**
* Send an email using the configured provider (Mailgun or MTA)
* Send an email using the MTA
* @param email The email to send
* @param to Recipient(s)
* @param options Additional options
@ -142,7 +142,7 @@ export class EmailService {
if (this.config.useMta && this.mtaConnector) {
return this.mtaConnector.sendEmail(email, to, options);
} else {
throw new Error('No email provider configured');
throw new Error('MTA not configured');
}
}

View File

@ -0,0 +1,3 @@
// Email services
export * from './classes.emailservice.js';
export * from './classes.apimanager.js';

View File

@ -1,956 +0,0 @@
import * as plugins from '../plugins.js';
import { Email } from './classes.email.js';
import type { IEmailOptions } from './classes.email.js';
import { DeliveryStatus } from './classes.emailsendjob.js';
import type { MtaService } from './classes.mta.js';
import type { IDnsRecord } from './classes.dnsmanager.js';
/**
* Authentication options for API requests
*/
interface AuthOptions {
/** Required API keys for different endpoints */
apiKeys: Map<string, string[]>;
/** JWT secret for token-based authentication */
jwtSecret?: string;
/** Whether to validate IP addresses */
validateIp?: boolean;
/** Allowed IP addresses */
allowedIps?: string[];
}
/**
* Rate limiting options for API endpoints
*/
interface RateLimitOptions {
/** Maximum requests per window */
maxRequests: number;
/** Time window in milliseconds */
windowMs: number;
/** Whether to apply per endpoint */
perEndpoint?: boolean;
/** Whether to apply per IP */
perIp?: boolean;
}
/**
* API route definition
*/
interface ApiRoute {
/** HTTP method */
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
/** Path pattern */
path: string;
/** Handler function */
handler: (req: any, res: any) => Promise<any>;
/** Required authentication level */
authLevel: 'none' | 'basic' | 'admin';
/** Rate limiting options */
rateLimit?: RateLimitOptions;
/** Route description */
description?: string;
}
/**
* Email send request
*/
interface SendEmailRequest {
/** Email details */
email: IEmailOptions;
/** Whether to validate domains before sending */
validateDomains?: boolean;
/** Priority level (1-5, 1 = highest) */
priority?: number;
}
/**
* Email status response
*/
interface EmailStatusResponse {
/** Email ID */
id: string;
/** Current status */
status: DeliveryStatus;
/** Send time */
sentAt?: Date;
/** Delivery time */
deliveredAt?: Date;
/** Error message if failed */
error?: string;
/** Recipient address */
recipient: string;
/** Number of delivery attempts */
attempts: number;
/** Next retry time */
nextRetry?: Date;
}
/**
* Domain verification response
*/
interface DomainVerificationResponse {
/** Domain name */
domain: string;
/** Whether the domain is verified */
verified: boolean;
/** Verification details */
details: {
/** SPF record status */
spf: {
valid: boolean;
record?: string;
error?: string;
};
/** DKIM record status */
dkim: {
valid: boolean;
record?: string;
error?: string;
};
/** DMARC record status */
dmarc: {
valid: boolean;
record?: string;
error?: string;
};
/** MX record status */
mx: {
valid: boolean;
records?: string[];
error?: string;
};
};
}
/**
* API error response
*/
interface ApiError {
/** Error code */
code: string;
/** Error message */
message: string;
/** Detailed error information */
details?: any;
}
/**
* Simple HTTP Response helper
*/
class HttpResponse {
private headers: Record<string, string> = {
'Content-Type': 'application/json'
};
public statusCode: number = 200;
constructor(private res: any) {}
header(name: string, value: string): HttpResponse {
this.headers[name] = value;
return this;
}
status(code: number): HttpResponse {
this.statusCode = code;
return this;
}
json(data: any): void {
this.res.writeHead(this.statusCode, this.headers);
this.res.end(JSON.stringify(data));
}
end(): void {
this.res.writeHead(this.statusCode, this.headers);
this.res.end();
}
}
/**
* API Manager for MTA service
*/
export class ApiManager {
/** TypedRouter for API routing */
public typedrouter = new plugins.typedrequest.TypedRouter();
/** MTA service reference */
private mtaRef: MtaService;
/** HTTP server */
private server: any;
/** Authentication options */
private authOptions: AuthOptions;
/** API routes */
private routes: ApiRoute[] = [];
/** Rate limiters */
private rateLimiters: Map<string, {
count: number;
resetTime: number;
clients: Map<string, {
count: number;
resetTime: number;
}>;
}> = new Map();
/**
* Initialize API Manager
* @param mtaRef MTA service reference
*/
constructor(mtaRef?: MtaService) {
this.mtaRef = mtaRef;
// Default authentication options
this.authOptions = {
apiKeys: new Map(),
validateIp: false,
allowedIps: []
};
// Register routes
this.registerRoutes();
// Create HTTP server with request handler
this.server = plugins.http.createServer(this.handleRequest.bind(this));
}
/**
* Set MTA service reference
* @param mtaRef MTA service reference
*/
public setMtaService(mtaRef: MtaService): void {
this.mtaRef = mtaRef;
}
/**
* Configure authentication options
* @param options Authentication options
*/
public configureAuth(options: Partial<AuthOptions>): void {
this.authOptions = {
...this.authOptions,
...options
};
}
/**
* Handle HTTP request
*/
private async handleRequest(req: any, res: any): Promise<void> {
const start = Date.now();
// Create a response helper
const response = new HttpResponse(res);
// Add CORS headers
response.header('Access-Control-Allow-Origin', '*');
response.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key');
// Handle preflight OPTIONS request
if (req.method === 'OPTIONS') {
return response.status(200).end();
}
try {
// Parse URL to get path and query
const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
const path = url.pathname;
// Collect request body if POST or PUT
let body = '';
if (req.method === 'POST' || req.method === 'PUT') {
await new Promise<void>((resolve, reject) => {
req.on('data', (chunk: Buffer) => {
body += chunk.toString();
});
req.on('end', () => {
resolve();
});
req.on('error', (err: Error) => {
reject(err);
});
});
// Parse body as JSON if Content-Type is application/json
const contentType = req.headers['content-type'] || '';
if (contentType.includes('application/json')) {
try {
req.body = JSON.parse(body);
} catch (error) {
return response.status(400).json({
code: 'INVALID_JSON',
message: 'Invalid JSON in request body'
});
}
} else {
req.body = body;
}
}
// Add authentication level to request
req.authLevel = 'none';
// Check API key
const apiKey = req.headers['x-api-key'];
if (apiKey) {
for (const [level, keys] of this.authOptions.apiKeys.entries()) {
if (keys.includes(apiKey)) {
req.authLevel = level;
break;
}
}
}
// Check JWT token (if configured)
if (this.authOptions.jwtSecret && req.headers.authorization) {
try {
const token = req.headers.authorization.split(' ')[1];
// Note: We would need to add JWT verification
// Using a simple placeholder for now
const decoded = { level: 'none' }; // Simplified - would use actual JWT library
if (decoded && decoded.level) {
req.authLevel = decoded.level;
req.user = decoded;
}
} catch (error) {
// Invalid token, but don't fail the request yet
console.error('Invalid JWT token:', error.message);
}
}
// Check IP address (if configured)
if (this.authOptions.validateIp) {
const clientIp = req.socket.remoteAddress;
if (!this.authOptions.allowedIps.includes(clientIp)) {
return response.status(403).json({
code: 'FORBIDDEN',
message: 'IP address not allowed'
});
}
}
// Find matching route
const route = this.findRoute(req.method, path);
if (!route) {
return response.status(404).json({
code: 'NOT_FOUND',
message: 'Endpoint not found'
});
}
// Check authentication
if (route.authLevel !== 'none' && req.authLevel !== route.authLevel && req.authLevel !== 'admin') {
return response.status(403).json({
code: 'FORBIDDEN',
message: `This endpoint requires ${route.authLevel} access`
});
}
// Check rate limit
if (route.rateLimit) {
const exceeded = this.checkRateLimit(route, req);
if (exceeded) {
return response.status(429).json({
code: 'RATE_LIMIT_EXCEEDED',
message: 'Rate limit exceeded, please try again later'
});
}
}
// Extract path parameters
const pathParams = this.extractPathParams(route.path, path);
req.params = pathParams;
// Extract query parameters
req.query = {};
for (const [key, value] of url.searchParams.entries()) {
req.query[key] = value;
}
// Handle the request
await route.handler(req, response);
// Log request
const duration = Date.now() - start;
console.log(`[API] ${req.method} ${path} ${response.statusCode} ${duration}ms`);
} catch (error) {
console.error(`Error handling request:`, error);
// Send appropriate error response
const status = error.status || 500;
const apiError: ApiError = {
code: error.code || 'INTERNAL_ERROR',
message: error.message || 'Internal server error'
};
if (process.env.NODE_ENV !== 'production') {
apiError.details = error.stack;
}
response.status(status).json(apiError);
}
}
/**
* Find a route matching the method and path
*/
private findRoute(method: string, path: string): ApiRoute | null {
for (const route of this.routes) {
if (route.method === method && this.pathMatches(route.path, path)) {
return route;
}
}
return null;
}
/**
* Check if a path matches a route pattern
*/
private pathMatches(pattern: string, path: string): boolean {
// Convert route pattern to regex
const patternParts = pattern.split('/');
const pathParts = path.split('/');
if (patternParts.length !== pathParts.length) {
return false;
}
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
// Parameter - always matches
continue;
}
if (patternParts[i] !== pathParts[i]) {
return false;
}
}
return true;
}
/**
* Extract path parameters from URL
*/
private extractPathParams(pattern: string, path: string): Record<string, string> {
const params: Record<string, string> = {};
const patternParts = pattern.split('/');
const pathParts = path.split('/');
for (let i = 0; i < patternParts.length; i++) {
if (patternParts[i].startsWith(':')) {
const paramName = patternParts[i].substring(1);
params[paramName] = pathParts[i];
}
}
return params;
}
/**
* Register API routes
*/
private registerRoutes(): void {
// Email routes
this.addRoute({
method: 'POST',
path: '/api/email/send',
handler: this.handleSendEmail.bind(this),
authLevel: 'basic',
description: 'Send an email'
});
this.addRoute({
method: 'GET',
path: '/api/email/status/:id',
handler: this.handleGetEmailStatus.bind(this),
authLevel: 'basic',
description: 'Get email delivery status'
});
// Domain routes
this.addRoute({
method: 'GET',
path: '/api/domain/verify/:domain',
handler: this.handleVerifyDomain.bind(this),
authLevel: 'basic',
description: 'Verify domain DNS records'
});
this.addRoute({
method: 'GET',
path: '/api/domain/records/:domain',
handler: this.handleGetDomainRecords.bind(this),
authLevel: 'basic',
description: 'Get recommended DNS records for domain'
});
// DKIM routes
this.addRoute({
method: 'POST',
path: '/api/dkim/generate/:domain',
handler: this.handleGenerateDkim.bind(this),
authLevel: 'admin',
description: 'Generate DKIM keys for domain'
});
this.addRoute({
method: 'GET',
path: '/api/dkim/public/:domain',
handler: this.handleGetDkimPublicKey.bind(this),
authLevel: 'basic',
description: 'Get DKIM public key for domain'
});
// Stats route
this.addRoute({
method: 'GET',
path: '/api/stats',
handler: this.handleGetStats.bind(this),
authLevel: 'admin',
description: 'Get MTA statistics'
});
// Documentation route
this.addRoute({
method: 'GET',
path: '/api',
handler: this.handleGetApiDocs.bind(this),
authLevel: 'none',
description: 'API documentation'
});
}
/**
* Add an API route
* @param route Route definition
*/
private addRoute(route: ApiRoute): void {
this.routes.push(route);
}
/**
* Check rate limit for a route
* @param route Route definition
* @param req Express request
* @returns Whether rate limit is exceeded
*/
private checkRateLimit(route: ApiRoute, req: any): boolean {
if (!route.rateLimit) return false;
const { maxRequests, windowMs, perEndpoint, perIp } = route.rateLimit;
// Determine rate limit key
let key = 'global';
if (perEndpoint) {
key = `${route.method}:${route.path}`;
}
// Get or create limiter
if (!this.rateLimiters.has(key)) {
this.rateLimiters.set(key, {
count: 0,
resetTime: Date.now() + windowMs,
clients: new Map()
});
}
const limiter = this.rateLimiters.get(key);
// Reset if window has passed
if (Date.now() > limiter.resetTime) {
limiter.count = 0;
limiter.resetTime = Date.now() + windowMs;
limiter.clients.clear();
}
// Check per-IP limit if enabled
if (perIp) {
const clientIp = req.socket.remoteAddress;
let clientLimiter = limiter.clients.get(clientIp);
if (!clientLimiter) {
clientLimiter = {
count: 0,
resetTime: Date.now() + windowMs
};
limiter.clients.set(clientIp, clientLimiter);
}
// Reset client limiter if needed
if (Date.now() > clientLimiter.resetTime) {
clientLimiter.count = 0;
clientLimiter.resetTime = Date.now() + windowMs;
}
// Check client limit
if (clientLimiter.count >= maxRequests) {
return true;
}
// Increment client count
clientLimiter.count++;
} else {
// Check global limit
if (limiter.count >= maxRequests) {
return true;
}
// Increment global count
limiter.count++;
}
return false;
}
/**
* Create an API error
* @param code Error code
* @param message Error message
* @param status HTTP status code
* @param details Additional details
* @returns API error
*/
private createError(code: string, message: string, status = 400, details?: any): Error & { code: string; status: number; details?: any } {
const error = new Error(message) as Error & { code: string; status: number; details?: any };
error.code = code;
error.status = status;
if (details) {
error.details = details;
}
return error;
}
/**
* Validate that MTA service is available
*/
private validateMtaService(): void {
if (!this.mtaRef) {
throw this.createError('SERVICE_UNAVAILABLE', 'MTA service is not available', 503);
}
}
/**
* Handle email send request
* @param req Express request
* @param res Express response
*/
private async handleSendEmail(req: any, res: any): Promise<void> {
this.validateMtaService();
const data = req.body as SendEmailRequest;
if (!data || !data.email) {
throw this.createError('INVALID_REQUEST', 'Missing email data');
}
try {
// Create Email instance
const email = new Email(data.email);
// Validate domains if requested
if (data.validateDomains) {
const fromDomain = email.getFromDomain();
if (fromDomain) {
const verification = await this.mtaRef.dnsManager.verifyEmailAuthRecords(fromDomain);
// Check if SPF and DKIM are valid
if (!verification.spf.valid || !verification.dkim.valid) {
throw this.createError('DOMAIN_VERIFICATION_FAILED', 'Domain DNS verification failed', 400, {
verification
});
}
}
}
// Send email
const id = await this.mtaRef.send(email);
// Return success response
res.json({
id,
message: 'Email queued successfully',
status: 'pending'
});
} catch (error) {
// Handle Email constructor errors
if (error.message.includes('Invalid') || error.message.includes('must have')) {
throw this.createError('INVALID_EMAIL', error.message);
}
throw error;
}
}
/**
* Handle email status request
* @param req Express request
* @param res Express response
*/
private async handleGetEmailStatus(req: any, res: any): Promise<void> {
this.validateMtaService();
const id = req.params.id;
if (!id) {
throw this.createError('INVALID_REQUEST', 'Missing email ID');
}
// Get email status
const status = this.mtaRef.getEmailStatus(id);
if (!status) {
throw this.createError('NOT_FOUND', `Email with ID ${id} not found`, 404);
}
// Create response
const response: EmailStatusResponse = {
id: status.id,
status: status.status,
sentAt: status.addedAt,
recipient: status.email.to[0],
attempts: status.attempts
};
// Add additional fields if available
if (status.lastAttempt) {
response.sentAt = status.lastAttempt;
}
if (status.status === DeliveryStatus.DELIVERED) {
response.deliveredAt = status.lastAttempt;
}
if (status.error) {
response.error = status.error.message;
}
if (status.nextAttempt) {
response.nextRetry = status.nextAttempt;
}
res.json(response);
}
/**
* Handle domain verification request
* @param req Express request
* @param res Express response
*/
private async handleVerifyDomain(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Verify domain DNS records
const records = await this.mtaRef.dnsManager.verifyEmailAuthRecords(domain);
// Get MX records
let mxValid = false;
let mxRecords: string[] = [];
let mxError: string = undefined;
try {
const mxResult = await this.mtaRef.dnsManager.lookupMx(domain);
mxValid = mxResult.length > 0;
mxRecords = mxResult.map(mx => mx.exchange);
} catch (error) {
mxError = error.message;
}
// Create response
const response: DomainVerificationResponse = {
domain,
verified: records.spf.valid && records.dkim.valid && records.dmarc.valid && mxValid,
details: {
spf: {
valid: records.spf.valid,
record: records.spf.value,
error: records.spf.error
},
dkim: {
valid: records.dkim.valid,
record: records.dkim.value,
error: records.dkim.error
},
dmarc: {
valid: records.dmarc.valid,
record: records.dmarc.value,
error: records.dmarc.error
},
mx: {
valid: mxValid,
records: mxRecords,
error: mxError
}
}
};
res.json(response);
} catch (error) {
throw this.createError('VERIFICATION_FAILED', `Domain verification failed: ${error.message}`);
}
}
/**
* Handle get domain records request
* @param req Express request
* @param res Express response
*/
private async handleGetDomainRecords(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Generate recommended DNS records
const records = await this.mtaRef.dnsManager.generateAllRecommendedRecords(domain);
res.json({
domain,
records
});
} catch (error) {
throw this.createError('GENERATION_FAILED', `DNS record generation failed: ${error.message}`);
}
}
/**
* Handle generate DKIM keys request
* @param req Express request
* @param res Express response
*/
private async handleGenerateDkim(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Generate DKIM keys
await this.mtaRef.dkimCreator.handleDKIMKeysForDomain(domain);
// Get DNS record
const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
res.json({
domain,
dnsRecord,
message: 'DKIM keys generated successfully'
});
} catch (error) {
throw this.createError('GENERATION_FAILED', `DKIM generation failed: ${error.message}`);
}
}
/**
* Handle get DKIM public key request
* @param req Express request
* @param res Express response
*/
private async handleGetDkimPublicKey(req: any, res: any): Promise<void> {
this.validateMtaService();
const domain = req.params.domain;
if (!domain) {
throw this.createError('INVALID_REQUEST', 'Missing domain');
}
try {
// Get DKIM keys
const keys = await this.mtaRef.dkimCreator.readDKIMKeys(domain);
// Get DNS record
const dnsRecord = await this.mtaRef.dkimCreator.getDNSRecordForDomain(domain);
res.json({
domain,
publicKey: keys.publicKey,
dnsRecord
});
} catch (error) {
throw this.createError('NOT_FOUND', `DKIM keys not found for domain: ${domain}`, 404);
}
}
/**
* Handle get stats request
* @param req Express request
* @param res Express response
*/
private async handleGetStats(req: any, res: any): Promise<void> {
this.validateMtaService();
// Get MTA stats
const stats = this.mtaRef.getStats();
res.json(stats);
}
/**
* Handle get API docs request
* @param req Express request
* @param res Express response
*/
private async handleGetApiDocs(req: any, res: any): Promise<void> {
// Generate API documentation
const docs = {
name: 'MTA API',
version: '1.0.0',
description: 'API for interacting with the MTA service',
endpoints: this.routes.map(route => ({
method: route.method,
path: route.path,
description: route.description,
authLevel: route.authLevel
}))
};
res.json(docs);
}
/**
* Start the API server
* @param port Port to listen on
* @returns Promise that resolves when server is started
*/
public start(port: number = 3000): Promise<void> {
return new Promise((resolve, reject) => {
try {
// Start HTTP server
this.server.listen(port, () => {
console.log(`API server listening on port ${port}`);
resolve();
});
} catch (error) {
console.error('Failed to start API server:', error);
reject(error);
}
});
}
/**
* Stop the API server
*/
public stop(): void {
if (this.server) {
this.server.close();
console.log('API server stopped');
}
}
}

View File

@ -1,10 +0,0 @@
export * from './classes.dkimcreator.js';
export * from './classes.emailsignjob.js';
export * from './classes.dkimverifier.js';
export * from './classes.dmarcverifier.js';
export * from './classes.spfverifier.js';
export * from './classes.mta.js';
export * from './classes.smtpserver.js';
export * from './classes.emailsendjob.js';
export * from './classes.email.js';
export * from './classes.ratelimiter.js';

View File

@ -1,9 +1,9 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { PlatformServiceDb } from './classes.platformservicedb.js'
import { EmailService } from './email/classes.emailservice.js';
import { EmailService } from './mail/services/classes.emailservice.js';
import { SmsService } from './sms/classes.smsservice.js';
import { MtaService } from './mta/classes.mta.js';
import { MtaService } from './mail/delivery/classes.mta.js';
export class SzPlatformService {
public projectinfo: plugins.projectinfo.ProjectInfo;

View File

@ -1,8 +1,8 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { logger } from '../logger.js';
import { Email } from '../mta/classes.email.js';
import type { IAttachment } from '../mta/classes.email.js';
import { Email } from '../mail/core/classes.email.js';
import type { IAttachment } from '../mail/core/classes.email.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from './classes.securitylogger.js';
import { LRUCache } from 'lru-cache';

View File

@ -18,10 +18,14 @@ export enum SecurityEventType {
AUTHENTICATION = 'authentication',
ACCESS_CONTROL = 'access_control',
EMAIL_VALIDATION = 'email_validation',
EMAIL_PROCESSING = 'email_processing',
EMAIL_FORWARDING = 'email_forwarding',
EMAIL_DELIVERY = 'email_delivery',
DKIM = 'dkim',
SPF = 'spf',
DMARC = 'dmarc',
RATE_LIMIT = 'rate_limit',
RATE_LIMITING = 'rate_limiting',
SPAM = 'spam',
MALWARE = 'malware',
CONNECTION = 'connection',

View File

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