Compare commits

...

7 Commits

15 changed files with 2966 additions and 286 deletions

View File

@ -1,5 +1,44 @@
# Changelog # Changelog
## 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.
- Introduced IEmailConfig interface to consolidate MTA, forwarding, and processing settings.
- Added pattern-based domain routing with glob patterns (e.g., '*@example.com', '*@*.example.net').
- Reworked DcRouter integration to expose unified email handling and updated readme.plan.md and changelog.md accordingly.
- Removed deprecated SMTP forwarding components in favor of the consolidated approach.
## 2025-05-08 - 2.7.0 - feat(dcrouter)
Implement consolidated email configuration with pattern-based routing
- Added new pattern-based email routing with glob patterns (e.g., `*@task.vc`, `*@*.example.net`)
- Consolidated all email functionality (MTA, forwarding, processing) under a unified `emailConfig` interface
- Implemented domain router with pattern specificity calculation for most accurate matching
- Removed deprecated components (SMTP forwarding, Store-and-Forward) in favor of the unified approach
- Updated DcRouter tests to use the new consolidated email configuration pattern
- Enhanced inline documentation with detailed interface definitions and configuration examples
- Updated implementation plan with comprehensive component designs for the unified email system
## 2025-05-07 - 2.6.0 - feat(dcrouter)
Implement integrated DcRouter with comprehensive SmartProxy configuration, enhanced SMTP processing, and robust storeandforward email routing
- Marked completion of tasks in readme.plan.md with [x] flags for SMTP server setup, email processing pipeline, queue management, and delivery system.
- Reworked DcRouter to use direct SmartProxy configuration, separating smtpConfig and smtpForwarding approaches.
- Added new components for delivery queue and delivery system with persistent storage support.
- Improved SMTP server implementation with TLS support, event handlers for connection, authentication, sender/recipient validation, and data processing.
- Refined domain-based routing and transformation logic in EmailProcessor with metrics and logging.
- Updated exported modules in dcrouter index to include SMTP storeandforward components.
- Enhanced inline documentation and code comments for configuration interfaces and integration details.
## 2025-05-07 - 2.5.0 - feat(dcrouter)
Enhance DcRouter configuration and update documentation
- Added new implementation hints (readme.hints.md) and planning documentation (readme.plan.md) outlining removal of SzPlatformService dependency and improvements in SMTP forwarding, domain routing, and certificate management.
- Introduced new interfaces: ISmtpForwardingConfig and IDomainRoutingConfig for precise SMTP and HTTP domain routing configuration.
- Refactored DcRouter classes to support direct integration with SmartProxy and enhanced MTA functionality, including SMTP port configuration and improved TLS handling.
- Updated supporting modules such as SmtpPortConfig and EmailDomainRouter to provide better routing and security options.
- Enhanced test coverage across dcrouter, rate limiter, IP warmup manager, and email authentication, ensuring backward compatibility and improved quality.
## 2025-05-07 - 2.4.2 - fix(tests) ## 2025-05-07 - 2.4.2 - fix(tests)
Update test assertions and singleton instance references in DMARC, integration, and IP warmup manager tests Update test assertions and singleton instance references in DMARC, integration, and IP warmup manager tests

View File

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

View File

@ -0,0 +1,96 @@
# Implementation Hints and Learnings
## SmartProxy Usage
### Direct Component Usage
- Use SmartProxy components directly instead of creating your own wrappers
- SmartProxy already includes Port80Handler and NetworkProxy functionality
- When using SmartProxy, configure it directly rather than instantiating Port80Handler or NetworkProxy separately
```typescript
// PREFERRED: Use SmartProxy with built-in ACME support
const smartProxy = new plugins.smartproxy.SmartProxy({
fromPort: 443,
toPort: targetPort,
targetIP: targetServer,
sniEnabled: true,
acme: {
port: 80,
enabled: true,
autoRenew: true,
useProduction: true,
renewThresholdDays: 30,
accountEmail: contactEmail
},
globalPortRanges: [{ from: 443, to: 443 }],
domainConfigs: [/* domain configurations */]
});
```
### Certificate Management
- SmartProxy has built-in ACME certificate management
- Configure it in the `acme` property of SmartProxy options
- Use `accountEmail` (not `email`) for the ACME contact email
- SmartProxy handles both HTTP-01 challenges and certificate application automatically
## qenv Usage
### Direct Usage
- Use qenv directly instead of creating environment variable wrappers
- Instantiate qenv with appropriate basePath and nogitPath:
```typescript
const qenv = new plugins.qenv.Qenv('./', '.nogit/');
const value = await qenv.getEnvVarOnDemand('ENV_VAR_NAME');
```
## TypeScript Interfaces
### SmartProxy Interfaces
- Always check the interfaces from the node_modules to ensure correct property names
- Important interfaces:
- `ISmartProxyOptions`: Main configuration for SmartProxy
- `IAcmeOptions`: ACME certificate configuration
- `IDomainConfig`: Domain-specific configuration
### Required Properties
- Remember to include all required properties in your interface implementations
- For `ISmartProxyOptions`, `globalPortRanges` is required
- For `IAcmeOptions`, use `accountEmail` for the contact email
## Testing
### Test Structure
- Follow the project's test structure, using `@push.rocks/tapbundle`
- Use `expect(value).toEqual(expected)` for equality checks
- Use `expect(value).toBeTruthy()` for boolean assertions
```typescript
tap.test('test description', async () => {
const result = someFunction();
expect(result.property).toEqual('expected value');
expect(result.valid).toBeTruthy();
});
```
### Cleanup
- Include a cleanup test to ensure proper test resource handling
- Add a `stop` test to forcefully end the test when needed:
```typescript
tap.test('stop', async () => {
await tap.stopForcefully();
});
```
## Architecture Principles
### Simplicity
- Prefer direct usage of libraries instead of creating wrappers
- Don't reinvent functionality that already exists in dependencies
- Keep interfaces clean and focused, avoiding unnecessary abstraction layers
### Component Integration
- Leverage built-in integrations between components (like SmartProxy's ACME handling)
- Use parallel operations for performance (like in the `stop()` method)
- Separate concerns clearly (HTTP handling vs. SMTP handling)

File diff suppressed because it is too large Load Diff

146
test/test.dcrouter.ts Normal file
View File

@ -0,0 +1,146 @@
import { tap, expect } from '@push.rocks/tapbundle';
import * as plugins from '../ts/plugins.js';
import {
DcRouter,
type IDcRouterOptions,
type IEmailConfig,
type EmailProcessingMode,
type IDomainRule
} from '../ts/dcrouter/index.js';
tap.test('DcRouter class - basic functionality', async () => {
// Create a simple DcRouter instance
const options: IDcRouterOptions = {
tls: {
contactEmail: 'test@example.com'
}
};
const router = new DcRouter(options);
expect(router).toBeTruthy();
expect(router instanceof DcRouter).toEqual(true);
expect(router.options.tls.contactEmail).toEqual('test@example.com');
});
tap.test('DcRouter class - SmartProxy configuration', async () => {
// Create SmartProxy configuration
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
fromPort: 443,
toPort: 8080,
targetIP: '10.0.0.10',
sniEnabled: true,
acme: {
port: 80,
enabled: true,
autoRenew: true,
useProduction: false,
renewThresholdDays: 30,
accountEmail: 'admin@example.com'
},
globalPortRanges: [
{ from: 80, to: 80 },
{ from: 443, to: 443 }
],
domainConfigs: [
{
domains: ['example.com', 'www.example.com'],
allowedIPs: ['0.0.0.0/0'],
targetIPs: ['10.0.0.10'],
portRanges: [
{ from: 80, to: 80 },
{ from: 443, to: 443 }
]
}
]
};
const options: IDcRouterOptions = {
smartProxyConfig,
tls: {
contactEmail: 'test@example.com'
}
};
const router = new DcRouter(options);
expect(router.options.smartProxyConfig).toBeTruthy();
expect(router.options.smartProxyConfig.domainConfigs.length).toEqual(1);
expect(router.options.smartProxyConfig.domainConfigs[0].domains[0]).toEqual('example.com');
});
tap.test('DcRouter class - Email configuration', async () => {
// Create consolidated email configuration
const emailConfig: IEmailConfig = {
ports: [25, 587, 465],
hostname: 'mail.example.com',
maxMessageSize: 50 * 1024 * 1024, // 50MB
defaultMode: 'forward' as EmailProcessingMode,
defaultServer: 'fallback-mail.example.com',
defaultPort: 25,
defaultTls: true,
domainRules: [
{
pattern: '*@example.com',
mode: 'forward' as EmailProcessingMode,
target: {
server: 'mail1.example.com',
port: 25,
useTls: true
}
},
{
pattern: '*@example.org',
mode: 'mta' as EmailProcessingMode,
mtaOptions: {
domain: 'example.org',
allowLocalDelivery: true
}
}
]
};
const options: IDcRouterOptions = {
emailConfig,
tls: {
contactEmail: 'test@example.com'
}
};
const router = new DcRouter(options);
expect(router.options.emailConfig).toBeTruthy();
expect(router.options.emailConfig.ports.length).toEqual(3);
expect(router.options.emailConfig.domainRules.length).toEqual(2);
expect(router.options.emailConfig.domainRules[0].pattern).toEqual('*@example.com');
expect(router.options.emailConfig.domainRules[1].pattern).toEqual('*@example.org');
});
tap.test('DcRouter class - Domain pattern matching', async () => {
const router = new DcRouter({});
// Use the internal method for testing if accessible
// This requires knowledge of the implementation, so it's a bit brittle
if (typeof router['isDomainMatch'] === 'function') {
// Test exact match
expect(router['isDomainMatch']('example.com', 'example.com')).toEqual(true);
expect(router['isDomainMatch']('example.com', 'example.org')).toEqual(false);
// Test wildcard match
expect(router['isDomainMatch']('sub.example.com', '*.example.com')).toEqual(true);
expect(router['isDomainMatch']('sub.sub.example.com', '*.example.com')).toEqual(true);
expect(router['isDomainMatch']('example.com', '*.example.com')).toEqual(false);
expect(router['isDomainMatch']('sub.example.org', '*.example.com')).toEqual(false);
}
});
// Final clean-up test
tap.test('clean up after tests', async () => {
// No-op - just to make sure everything is cleaned up properly
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
// Export a function to run all tests
export default tap.start();

View File

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

View File

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

View File

@ -1,23 +1,39 @@
import * as plugins from '../plugins.js'; import * as plugins from '../plugins.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import { SzDcRouterConnector } from './classes.dcr.sz.connector.js'; import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js';
import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js';
import type { SzPlatformService } from '../platformservice.js';
import { type IMtaConfig, MtaService } from '../mta/classes.mta.js';
// Certificate types are available via plugins.tsclass // Certificate types are available via plugins.tsclass
export interface IDcRouterOptions { // Import the consolidated email config
platformServiceInstance?: SzPlatformService; import type { IEmailConfig } from './classes.email.config.js';
import { DomainRouter } from './classes.domain.router.js';
export interface IDcRouterOptions {
/**
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
*/
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
/**
* Consolidated email configuration
* This enables all email handling with pattern-based routing
*/
emailConfig?: IEmailConfig;
/** TLS/certificate configuration */
tls?: {
/** Contact email for ACME certificates */
contactEmail: string;
/** Domain for main certificate */
domain?: string;
/** Path to certificate file (if not using auto-provisioning) */
certPath?: string;
/** Path to key file (if not using auto-provisioning) */
keyPath?: string;
};
/** SmartProxy (TCP/SNI) configuration */
smartProxyOptions?: plugins.smartproxy.ISmartProxyOptions;
/** Reverse proxy host configurations for HTTP(S) layer */
reverseProxyConfigs?: plugins.smartproxy.IReverseProxyConfig[];
/** MTA (SMTP) service configuration */
mtaConfig?: IMtaConfig;
/** Existing MTA service instance to use instead of creating a new one */
mtaServiceInstance?: MtaService;
/** DNS server configuration */ /** DNS server configuration */
dnsServerConfig?: plugins.smartdns.IDnsServerOptions; dnsServerConfig?: plugins.smartdns.IDnsServerOptions;
} }
@ -35,14 +51,20 @@ export interface PortProxyRuleContext {
proxy: plugins.smartproxy.SmartProxy; proxy: plugins.smartproxy.SmartProxy;
configs: plugins.smartproxy.IPortProxySettings['domainConfigs']; configs: plugins.smartproxy.IPortProxySettings['domainConfigs'];
} }
export class DcRouter { export class DcRouter {
public szDcRouterConnector = new SzDcRouterConnector(this);
public options: IDcRouterOptions; public options: IDcRouterOptions;
// Core services
public smartProxy?: plugins.smartproxy.SmartProxy; public smartProxy?: plugins.smartproxy.SmartProxy;
public mta?: MtaService;
public dnsServer?: plugins.smartdns.DnsServer; public dnsServer?: plugins.smartdns.DnsServer;
/** SMTP rule engine */
public smtpRuleEngine?: plugins.smartrule.SmartRule<any>; // Unified email components
public domainRouter?: DomainRouter;
// Environment access
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
constructor(optionsArg: IDcRouterOptions) { constructor(optionsArg: IDcRouterOptions) {
// Set defaults in options // Set defaults in options
this.options = { this.options = {
@ -51,194 +73,203 @@ export class DcRouter {
} }
public async start() { public async start() {
// Set up MTA service - use existing instance if provided console.log('Starting DcRouter services...');
if (this.options.mtaServiceInstance) {
// Use provided MTA service instance
this.mta = this.options.mtaServiceInstance;
console.log('Using provided MTA service instance');
// Get the SMTP rule engine from the provided MTA
this.smtpRuleEngine = this.mta.smtpRuleEngine;
} else if (this.options.mtaConfig) {
// Create new MTA service with the provided configuration
this.mta = new MtaService(undefined, this.options.mtaConfig);
console.log('Created new MTA service instance');
// Initialize SMTP rule engine
this.smtpRuleEngine = this.mta.smtpRuleEngine;
}
// TCP/SNI proxy (SmartProxy)
if (this.options.smartProxyOptions) {
// Lets setup smartacme
let certProvisionFunction: plugins.smartproxy.ISmartProxyOptions['certProvisionFunction'];
// Check if we can share certificate from MTA service
if (this.options.mtaServiceInstance && this.mta) {
// Share TLS certificate with MTA service (if available)
console.log('Using MTA service certificate for SmartProxy');
// Create proxy function to get cert from MTA service
certProvisionFunction = async (domainArg) => {
// Get cert from provided MTA service if available
if (this.mta && this.mta.certificate) {
console.log(`Using MTA certificate for domain ${domainArg}`);
// Return in the format expected by SmartProxy
const certExpiry = this.mta.certificate.expiresAt;
const certObj: plugins.tsclass.network.ICert = {
id: `cert-${domainArg}`,
domainName: domainArg,
privateKey: this.mta.certificate.privateKey,
publicKey: this.mta.certificate.publicKey,
created: Date.now(),
validUntil: certExpiry instanceof Date ? certExpiry.getTime() : Date.now() + 90 * 24 * 60 * 60 * 1000,
csr: ''
};
return certObj;
} else {
console.log(`No MTA certificate available for domain ${domainArg}, falling back to ACME`);
// Return string literal instead of 'http01' enum value
return null; // Let SmartProxy fall back to its default mechanism
}
};
} else if (true) {
// Set up ACME for certificate provisioning
const smartAcmeInstance = new plugins.smartacme.SmartAcme({
accountEmail: this.options.smartProxyOptions.acme.accountEmail,
certManager: new plugins.smartacme.certmanagers.MongoCertManager({
mongoDbUrl: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_URL'),
mongoDbUser: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_USER'),
mongoDbPass: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_PASS'),
mongoDbName: await this.szDcRouterConnector.getEnvVarOnDemand('MONGO_DB_NAME'),
}),
environment: 'production',
accountPrivateKey: await this.szDcRouterConnector.getEnvVarOnDemand('ACME_ACCOUNT_PRIVATE_KEY'),
challengeHandlers: [
new plugins.smartacme.handlers.Dns01Handler(new plugins.cloudflare.CloudflareAccount('')) // TODO
],
});
certProvisionFunction = async (domainArg) => {
try { try {
const domainSupported = await smartAcmeInstance.challengeHandlers[0].checkWetherDomainIsSupported(domainArg); // Set up SmartProxy for HTTP/HTTPS and general TCP/SNI traffic
if (!domainSupported) { if (this.options.smartProxyConfig) {
return null; // Let SmartProxy handle with default mechanism await this.setupSmartProxy();
}
// Get the certificate and convert to ICert
const cert = await smartAcmeInstance.getCertificateForDomain(domainArg);
if (typeof cert === 'string') {
return null; // String result indicates fallback
} }
// Return in the format expected by SmartProxy // Set up unified email handling if configured
const result: plugins.tsclass.network.ICert = { if (this.options.emailConfig) {
id: `cert-${domainArg}`, await this.setupUnifiedEmailHandling();
domainName: domainArg,
privateKey: cert.privateKey,
publicKey: cert.publicKey,
created: Date.now(),
validUntil: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days
csr: ''
};
return result;
} catch (err) {
console.error(`Certificate error for ${domainArg}:`, err);
return null; // Let SmartProxy handle with default mechanism
}
};
} }
// Create the SmartProxy instance with the appropriate cert provisioning function // 3. Set up DNS server if configured
const smartProxyOptions = {
...this.options.smartProxyOptions,
certProvisionFunction
};
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyOptions);
// Configure SmartProxy for SMTP if we have an MTA service
if (this.mta) {
this.configureSmtpProxy();
}
}
// DNS server
if (this.options.dnsServerConfig) { if (this.options.dnsServerConfig) {
this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig); this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig);
}
// Start SmartProxy if configured
if (this.smartProxy) {
await this.smartProxy.start();
}
// Start MTA service if configured and it's our own service (not an external instance)
if (this.mta && !this.options.mtaServiceInstance) {
await this.mta.start();
}
// Start DNS server if configured
if (this.dnsServer) {
await this.dnsServer.start(); await this.dnsServer.start();
console.log('DNS server started');
}
console.log('DcRouter started successfully');
} catch (error) {
console.error('Error starting DcRouter:', error);
// Try to clean up any services that may have started
await this.stop();
throw error;
} }
} }
/** /**
* Configure SmartProxy for SMTP ports * Set up SmartProxy with direct configuration
*/ */
public configureSmtpProxy(): void { private async setupSmartProxy(): Promise<void> {
if (!this.smartProxy || !this.mta) return; if (!this.options.smartProxyConfig) {
return;
}
const mtaPort = this.mta.config.smtp?.port || 25; console.log('Setting up SmartProxy with direct configuration');
try {
// Configure SmartProxy to forward SMTP ports to the MTA service // Create SmartProxy instance with full configuration
const settings = this.smartProxy.settings; this.smartProxy = new plugins.smartproxy.SmartProxy(this.options.smartProxyConfig);
// Ensure localhost target for MTA
settings.targetIP = settings.targetIP || 'localhost'; // Set up event listeners
// Forward all SMTP ports to the MTA port this.smartProxy.on('error', (err) => {
settings.toPort = mtaPort; console.error('SmartProxy error:', err);
// Initialize globalPortRanges if needed });
if (!settings.globalPortRanges) {
settings.globalPortRanges = []; if (this.options.smartProxyConfig.acme) {
this.smartProxy.on('certificate-issued', (event) => {
console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
});
this.smartProxy.on('certificate-renewed', (event) => {
console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
});
} }
// Add SMTP ports 25, 587, 465 if not already present
for (const port of [25, 587, 465]) { // Start SmartProxy
if (!settings.globalPortRanges.some((r) => r.from <= port && port <= r.to)) { await this.smartProxy.start();
settings.globalPortRanges.push({ from: port, to: port });
console.log('SmartProxy started successfully');
} }
/**
* Check if a domain matches a pattern (including wildcard support)
* @param domain The domain to check
* @param pattern The pattern to match against (e.g., "*.example.com")
* @returns Whether the domain matches the pattern
*/
private isDomainMatch(domain: string, pattern: string): boolean {
// Normalize inputs
domain = domain.toLowerCase();
pattern = pattern.toLowerCase();
// Check for exact match
if (domain === pattern) {
return true;
} }
console.log(`Configured SmartProxy for SMTP ports: 25, 587, 465 → localhost:${mtaPort}`);
} catch (error) { // Check for wildcard match (*.example.com)
console.error('Failed to configure SmartProxy for SMTP:', error); if (pattern.startsWith('*.')) {
const patternSuffix = pattern.slice(2); // Remove the "*." prefix
// Check if domain ends with the pattern suffix and has at least one character before it
return domain.endsWith(patternSuffix) && domain.length > patternSuffix.length;
} }
// No match
return false;
} }
public async stop() { public async stop() {
// Stop SmartProxy console.log('Stopping DcRouter services...');
if (this.smartProxy) {
await this.smartProxy.stop();
}
// Stop MTA service if it's our own (not an external instance) try {
if (this.mta && !this.options.mtaServiceInstance) { // Stop all services in parallel for faster shutdown
await this.mta.stop(); await Promise.all([
} // Stop unified email components if running
this.domainRouter ? this.stopUnifiedEmailComponents().catch(err => console.error('Error stopping unified email components:', err)) : Promise.resolve(),
// Stop DNS server // Stop HTTP SmartProxy if running
if (this.dnsServer) { this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
await this.dnsServer.stop();
// Stop DNS server if running
this.dnsServer ?
this.dnsServer.stop().catch(err => console.error('Error stopping DNS server:', err)) :
Promise.resolve()
]);
console.log('All DcRouter services stopped');
} catch (error) {
console.error('Error during DcRouter shutdown:', error);
throw error;
} }
} }
/** /**
* Register an SMTP routing rule * Update SmartProxy configuration
* @param config New SmartProxy configuration
*/ */
public addSmtpRule( public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
priority: number, // Stop existing SmartProxy if running
check: (email: any) => Promise<any>, if (this.smartProxy) {
action: (email: any) => Promise<any> await this.smartProxy.stop();
): void { this.smartProxy = undefined;
this.smtpRuleEngine?.createRule(priority, check, action); }
// Update configuration
this.options.smartProxyConfig = config;
// Start new SmartProxy with updated configuration
await this.setupSmartProxy();
console.log('SmartProxy configuration updated');
}
/**
* Set up unified email handling with pattern-based routing
* This implements the consolidated emailConfig approach
*/
private async setupUnifiedEmailHandling(): Promise<void> {
console.log('Setting up unified email handling with pattern-based routing');
if (!this.options.emailConfig) {
throw new Error('Email configuration is required for unified email handling');
}
try {
// Create domain router for pattern matching
this.domainRouter = new DomainRouter({
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
});
// TODO: Initialize the full unified email processing pipeline
console.log(`Unified email handling configured with ${this.options.emailConfig.domainRules.length} domain rules`);
} catch (error) {
console.error('Error setting up unified email handling:', error);
throw error;
} }
} }
/**
* Update the unified email configuration
* @param config New email configuration
*/
public async updateEmailConfig(config: IEmailConfig): Promise<void> {
// Stop existing email components
await this.stopUnifiedEmailComponents();
// Update configuration
this.options.emailConfig = config;
// Start email handling with new configuration
await this.setupUnifiedEmailHandling();
console.log('Unified email configuration updated');
}
/**
* Stop all unified email components
*/
private async stopUnifiedEmailComponents(): Promise<void> {
// TODO: Implement stopping all unified email components
// Clear the domain router
this.domainRouter = undefined;
}
}
export default DcRouter; export default DcRouter;

View File

@ -0,0 +1,351 @@
import * as plugins from '../plugins.js';
import { EventEmitter } from 'node:events';
import { type IDomainRule, type EmailProcessingMode } from './classes.email.config.js';
/**
* Options for the domain-based router
*/
export interface IDomainRouterOptions {
// Domain rules with glob pattern matching
domainRules: IDomainRule[];
// Default handling for unmatched domains
defaultMode: EmailProcessingMode;
defaultServer?: string;
defaultPort?: number;
defaultTls?: boolean;
// Pattern matching options
caseSensitive?: boolean;
priorityOrder?: 'most-specific' | 'first-match';
// Cache settings for pattern matching
enableCache?: boolean;
cacheSize?: number;
}
/**
* Result of a pattern match operation
*/
export interface IPatternMatchResult {
rule: IDomainRule;
exactMatch: boolean;
wildcardMatch: boolean;
specificity: number; // Higher is more specific
}
/**
* A pattern matching and routing class for email domains
*/
export class DomainRouter extends EventEmitter {
private options: IDomainRouterOptions;
private patternCache: Map<string, IDomainRule | null> = new Map();
/**
* Create a new domain router
* @param options Router options
*/
constructor(options: IDomainRouterOptions) {
super();
this.options = {
// Default options
caseSensitive: false,
priorityOrder: 'most-specific',
enableCache: true,
cacheSize: 1000,
...options
};
}
/**
* Match an email address against defined rules
* @param email Email address to match
* @returns The matching rule or null if no match
*/
public matchRule(email: string): IDomainRule | null {
// Check cache first if enabled
if (this.options.enableCache && this.patternCache.has(email)) {
return this.patternCache.get(email) || null;
}
// Normalize email if case-insensitive
const normalizedEmail = this.options.caseSensitive ? email : email.toLowerCase();
// Get all matching rules
const matches = this.getAllMatchingRules(normalizedEmail);
if (matches.length === 0) {
// Cache the result (null) if caching is enabled
if (this.options.enableCache) {
this.addToCache(email, null);
}
return null;
}
// Sort by specificity or order
let matchedRule: IDomainRule;
if (this.options.priorityOrder === 'most-specific') {
// Sort by specificity (most specific first)
const sortedMatches = matches.sort((a, b) => {
const aSpecificity = this.calculateSpecificity(a.pattern);
const bSpecificity = this.calculateSpecificity(b.pattern);
return bSpecificity - aSpecificity;
});
matchedRule = sortedMatches[0];
} else {
// First match in the list
matchedRule = matches[0];
}
// Cache the result if caching is enabled
if (this.options.enableCache) {
this.addToCache(email, matchedRule);
}
return matchedRule;
}
/**
* Calculate pattern specificity
* Higher is more specific
* @param pattern Pattern to calculate specificity for
*/
private calculateSpecificity(pattern: string): number {
let specificity = 0;
// Exact match is most specific
if (!pattern.includes('*')) {
return 100;
}
// Count characters that aren't wildcards
specificity += pattern.replace(/\*/g, '').length;
// Position of wildcards affects specificity
if (pattern.startsWith('*@')) {
// Wildcard in local part
specificity += 10;
} else if (pattern.includes('@*')) {
// Wildcard in domain part
specificity += 20;
}
return specificity;
}
/**
* Check if email matches a specific pattern
* @param email Email address to check
* @param pattern Pattern to check against
* @returns True if matching, false otherwise
*/
public matchesPattern(email: string, pattern: string): boolean {
// Normalize if case-insensitive
const normalizedEmail = this.options.caseSensitive ? email : email.toLowerCase();
const normalizedPattern = this.options.caseSensitive ? pattern : pattern.toLowerCase();
// Exact match
if (normalizedEmail === normalizedPattern) {
return true;
}
// Convert glob pattern to regex
const regexPattern = this.globToRegExp(normalizedPattern);
return regexPattern.test(normalizedEmail);
}
/**
* Convert a glob pattern to a regular expression
* @param pattern Glob pattern
* @returns Regular expression
*/
private globToRegExp(pattern: string): RegExp {
// Escape special regex characters except * and ?
let regexString = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(`^${regexString}$`);
}
/**
* Get all rules that match an email address
* @param email Email address to match
* @returns Array of matching rules
*/
public getAllMatchingRules(email: string): IDomainRule[] {
return this.options.domainRules.filter(rule => this.matchesPattern(email, rule.pattern));
}
/**
* Add a new routing rule
* @param rule Domain rule to add
*/
public addRule(rule: IDomainRule): void {
// Validate the rule
this.validateRule(rule);
// Add the rule
this.options.domainRules.push(rule);
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('ruleAdded', rule);
}
/**
* Validate a domain rule
* @param rule Rule to validate
*/
private validateRule(rule: IDomainRule): void {
// Pattern is required
if (!rule.pattern) {
throw new Error('Domain rule pattern is required');
}
// Mode is required
if (!rule.mode) {
throw new Error('Domain rule mode is required');
}
// Forward mode requires target
if (rule.mode === 'forward' && !rule.target) {
throw new Error('Forward mode requires target configuration');
}
// Forward mode target requires server
if (rule.mode === 'forward' && rule.target && !rule.target.server) {
throw new Error('Forward mode target requires server');
}
}
/**
* Update an existing rule
* @param pattern Pattern to update
* @param updates Updates to apply
* @returns True if rule was found and updated, false otherwise
*/
public updateRule(pattern: string, updates: Partial<IDomainRule>): boolean {
const ruleIndex = this.options.domainRules.findIndex(r => r.pattern === pattern);
if (ruleIndex === -1) {
return false;
}
// Get current rule
const currentRule = this.options.domainRules[ruleIndex];
// Create updated rule
const updatedRule: IDomainRule = {
...currentRule,
...updates
};
// Validate the updated rule
this.validateRule(updatedRule);
// Update the rule
this.options.domainRules[ruleIndex] = updatedRule;
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('ruleUpdated', updatedRule);
return true;
}
/**
* Remove a rule
* @param pattern Pattern to remove
* @returns True if rule was found and removed, false otherwise
*/
public removeRule(pattern: string): boolean {
const initialLength = this.options.domainRules.length;
this.options.domainRules = this.options.domainRules.filter(r => r.pattern !== pattern);
const removed = initialLength > this.options.domainRules.length;
if (removed) {
// Clear cache since rules have changed
this.clearCache();
// Emit event
this.emit('ruleRemoved', pattern);
}
return removed;
}
/**
* Get rule by pattern
* @param pattern Pattern to find
* @returns Rule with matching pattern or null if not found
*/
public getRule(pattern: string): IDomainRule | null {
return this.options.domainRules.find(r => r.pattern === pattern) || null;
}
/**
* Get all rules
* @returns Array of all domain rules
*/
public getRules(): IDomainRule[] {
return [...this.options.domainRules];
}
/**
* Update options
* @param options New options
*/
public updateOptions(options: Partial<IDomainRouterOptions>): void {
this.options = {
...this.options,
...options
};
// Clear cache if cache settings changed
if ('enableCache' in options || 'cacheSize' in options) {
this.clearCache();
}
// Emit event
this.emit('optionsUpdated', this.options);
}
/**
* Add an item to the pattern cache
* @param email Email address
* @param rule Matching rule or null
*/
private addToCache(email: string, rule: IDomainRule | null): void {
// If cache is disabled, do nothing
if (!this.options.enableCache) {
return;
}
// Add to cache
this.patternCache.set(email, rule);
// Check if cache size exceeds limit
if (this.patternCache.size > (this.options.cacheSize || 1000)) {
// Remove oldest entry (first in the Map)
const firstKey = this.patternCache.keys().next().value;
this.patternCache.delete(firstKey);
}
}
/**
* Clear pattern matching cache
*/
public clearCache(): void {
this.patternCache.clear();
this.emit('cacheCleared');
}
}

View File

@ -0,0 +1,129 @@
import * as plugins from '../plugins.js';
/**
* Email processing modes
*/
export type EmailProcessingMode = 'forward' | 'mta' | 'process';
/**
* Consolidated email configuration interface
*/
export interface IEmailConfig {
// Email server settings
ports: number[];
hostname: string;
maxMessageSize?: number;
// TLS configuration for email server
tls?: {
certPath?: string;
keyPath?: string;
caPath?: string;
minVersion?: string;
};
// Authentication for inbound connections
auth?: {
required?: boolean;
methods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
users?: Array<{username: string, password: string}>;
};
// Default routing for unmatched domains
defaultMode: EmailProcessingMode;
defaultServer?: string;
defaultPort?: number;
defaultTls?: boolean;
// Domain rules with glob pattern support
domainRules: IDomainRule[];
// Queue configuration for all email processing
queue?: {
storageType?: 'memory' | 'disk';
persistentPath?: string;
maxRetries?: number;
baseRetryDelay?: number;
maxRetryDelay?: number;
};
// Advanced MTA settings
mtaGlobalOptions?: IMtaOptions;
}
/**
* Domain rule interface for pattern-based routing
*/
export interface IDomainRule {
// Domain pattern (e.g., "*@example.com", "*@*.example.net")
pattern: string;
// Handling mode for this pattern
mode: EmailProcessingMode;
// Forward mode configuration
target?: {
server: string;
port?: number;
useTls?: boolean;
authentication?: {
user?: string;
pass?: string;
};
};
// MTA mode configuration
mtaOptions?: IMtaOptions;
// Process mode configuration
contentScanning?: boolean;
scanners?: IContentScanner[];
transformations?: ITransformation[];
// Rate limits for this domain
rateLimits?: {
maxMessagesPerMinute?: number;
maxRecipientsPerMessage?: number;
};
}
/**
* MTA options interface
*/
export interface IMtaOptions {
domain?: string;
allowLocalDelivery?: boolean;
localDeliveryPath?: string;
dkimSign?: boolean;
dkimOptions?: {
domainName: string;
keySelector: string;
privateKey: string;
};
smtpBanner?: string;
maxConnections?: number;
connTimeout?: number;
spoolDir?: string;
}
/**
* Content scanner interface
*/
export interface IContentScanner {
type: 'spam' | 'virus' | 'attachment';
threshold?: number;
action: 'tag' | 'reject';
blockedExtensions?: string[];
}
/**
* Transformation interface
*/
export interface ITransformation {
type: string;
header?: string;
value?: string;
domains?: string[];
append?: boolean;
[key: string]: any;
}

View File

@ -0,0 +1,431 @@
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

@ -0,0 +1,311 @@
import * as plugins from '../plugins.js';
/**
* Configuration options for TLS in SMTP connections
*/
export interface ISmtpTlsOptions {
/** Enable TLS for this SMTP port */
enabled: boolean;
/** Whether to use STARTTLS (upgrade plain connection) or implicit TLS */
useStartTls?: boolean;
/** Required TLS protocol version (defaults to TLSv1.2) */
minTlsVersion?: 'TLSv1.0' | 'TLSv1.1' | 'TLSv1.2' | 'TLSv1.3';
/** TLS ciphers to allow (comma-separated list) */
allowedCiphers?: string;
/** Whether to require client certificate for authentication */
requireClientCert?: boolean;
/** Whether to verify client certificate if provided */
verifyClientCert?: boolean;
}
/**
* Rate limiting options for SMTP connections
*/
export interface ISmtpRateLimitOptions {
/** Maximum connections per minute from a single IP */
maxConnectionsPerMinute?: number;
/** Maximum concurrent connections from a single IP */
maxConcurrentConnections?: number;
/** Maximum emails per minute from a single IP */
maxEmailsPerMinute?: number;
/** Maximum recipients per email */
maxRecipientsPerEmail?: number;
/** Maximum email size in bytes */
maxEmailSize?: number;
/** Action to take when rate limit is exceeded (default: 'tempfail') */
rateLimitAction?: 'tempfail' | 'drop' | 'delay';
}
/**
* Configuration for a specific SMTP port
*/
export interface ISmtpPortSettings {
/** The port number to listen on */
port: number;
/** Whether this port is enabled */
enabled?: boolean;
/** Port description (e.g., "Submission Port") */
description?: string;
/** Whether to require authentication for this port */
requireAuth?: boolean;
/** TLS options for this port */
tls?: ISmtpTlsOptions;
/** Rate limiting settings for this port */
rateLimit?: ISmtpRateLimitOptions;
/** Maximum message size in bytes for this port */
maxMessageSize?: number;
/** Whether to enable SMTP extensions like PIPELINING, 8BITMIME, etc. */
smtpExtensions?: {
/** Enable PIPELINING extension */
pipelining?: boolean;
/** Enable 8BITMIME extension */
eightBitMime?: boolean;
/** Enable SIZE extension */
size?: boolean;
/** Enable ENHANCEDSTATUSCODES extension */
enhancedStatusCodes?: boolean;
/** Enable DSN extension */
dsn?: boolean;
};
/** Custom SMTP greeting banner */
banner?: string;
}
/**
* Configuration manager for SMTP ports
*/
export class SmtpPortConfig {
/** Port configurations */
private portConfigs: Map<number, ISmtpPortSettings> = new Map();
/** Default port configurations */
private static readonly DEFAULT_CONFIGS: Record<number, Partial<ISmtpPortSettings>> = {
// Port 25: Standard SMTP
25: {
description: 'Standard SMTP',
requireAuth: false,
tls: {
enabled: true,
useStartTls: true,
minTlsVersion: 'TLSv1.2'
},
rateLimit: {
maxConnectionsPerMinute: 60,
maxConcurrentConnections: 10,
maxEmailsPerMinute: 30
},
maxMessageSize: 20 * 1024 * 1024 // 20MB
},
// Port 587: Submission
587: {
description: 'Submission Port',
requireAuth: true,
tls: {
enabled: true,
useStartTls: true,
minTlsVersion: 'TLSv1.2'
},
rateLimit: {
maxConnectionsPerMinute: 100,
maxConcurrentConnections: 20,
maxEmailsPerMinute: 60
},
maxMessageSize: 50 * 1024 * 1024 // 50MB
},
// Port 465: SMTPS (Legacy Implicit TLS)
465: {
description: 'SMTPS (Implicit TLS)',
requireAuth: true,
tls: {
enabled: true,
useStartTls: false,
minTlsVersion: 'TLSv1.2'
},
rateLimit: {
maxConnectionsPerMinute: 100,
maxConcurrentConnections: 20,
maxEmailsPerMinute: 60
},
maxMessageSize: 50 * 1024 * 1024 // 50MB
}
};
/**
* Create a new SmtpPortConfig
* @param initialConfigs Optional initial port configurations
*/
constructor(initialConfigs?: ISmtpPortSettings[]) {
// Initialize with default configurations for standard SMTP ports
this.initializeDefaults();
// Apply custom configurations if provided
if (initialConfigs) {
for (const config of initialConfigs) {
this.setPortConfig(config);
}
}
}
/**
* Initialize port configurations with defaults
*/
private initializeDefaults(): void {
// Set up default configurations for standard SMTP ports: 25, 587, 465
Object.entries(SmtpPortConfig.DEFAULT_CONFIGS).forEach(([portStr, defaults]) => {
const port = parseInt(portStr, 10);
this.portConfigs.set(port, {
port,
enabled: true,
...defaults
});
});
}
/**
* Get configuration for a specific port
* @param port Port number
* @returns Port configuration or null if not found
*/
public getPortConfig(port: number): ISmtpPortSettings | null {
return this.portConfigs.get(port) || null;
}
/**
* Get all configured ports
* @returns Array of port configurations
*/
public getAllPortConfigs(): ISmtpPortSettings[] {
return Array.from(this.portConfigs.values());
}
/**
* Get only enabled port configurations
* @returns Array of enabled port configurations
*/
public getEnabledPortConfigs(): ISmtpPortSettings[] {
return this.getAllPortConfigs().filter(config => config.enabled !== false);
}
/**
* Set configuration for a specific port
* @param config Port configuration
*/
public setPortConfig(config: ISmtpPortSettings): void {
// Get existing config if any
const existingConfig = this.portConfigs.get(config.port) || { port: config.port };
// Merge with new configuration
this.portConfigs.set(config.port, {
...existingConfig,
...config
});
}
/**
* Remove configuration for a specific port
* @param port Port number
* @returns Whether the configuration was removed
*/
public removePortConfig(port: number): boolean {
return this.portConfigs.delete(port);
}
/**
* Disable a specific port
* @param port Port number
* @returns Whether the port was disabled
*/
public disablePort(port: number): boolean {
const config = this.portConfigs.get(port);
if (config) {
config.enabled = false;
return true;
}
return false;
}
/**
* Enable a specific port
* @param port Port number
* @returns Whether the port was enabled
*/
public enablePort(port: number): boolean {
const config = this.portConfigs.get(port);
if (config) {
config.enabled = true;
return true;
}
return false;
}
/**
* Apply port configurations to SmartProxy settings
* @param smartProxy SmartProxy instance
*/
public applyToSmartProxy(smartProxy: plugins.smartproxy.SmartProxy): void {
if (!smartProxy) return;
const enabledPorts = this.getEnabledPortConfigs();
const settings = smartProxy.settings;
// Initialize globalPortRanges if needed
if (!settings.globalPortRanges) {
settings.globalPortRanges = [];
}
// Add configured ports to globalPortRanges
for (const portConfig of enabledPorts) {
// Add port to global port ranges if not already present
if (!settings.globalPortRanges.some((r) => r.from <= portConfig.port && portConfig.port <= r.to)) {
settings.globalPortRanges.push({ from: portConfig.port, to: portConfig.port });
}
// Apply TLS settings at SmartProxy level
if (portConfig.port === 465 && portConfig.tls?.enabled) {
// For implicit TLS on port 465
settings.sniEnabled = true;
}
}
// Group ports by TLS configuration to log them
const starttlsPorts = enabledPorts
.filter(p => p.tls?.enabled && p.tls?.useStartTls)
.map(p => p.port);
const implicitTlsPorts = enabledPorts
.filter(p => p.tls?.enabled && !p.tls?.useStartTls)
.map(p => p.port);
const nonTlsPorts = enabledPorts
.filter(p => !p.tls?.enabled)
.map(p => p.port);
if (starttlsPorts.length > 0) {
console.log(`Configured STARTTLS SMTP ports: ${starttlsPorts.join(', ')}`);
}
if (implicitTlsPorts.length > 0) {
console.log(`Configured Implicit TLS SMTP ports: ${implicitTlsPorts.join(', ')}`);
}
if (nonTlsPorts.length > 0) {
console.log(`Configured Plain SMTP ports: ${nonTlsPorts.join(', ')}`);
}
// Setup connection listeners for different port types
smartProxy.on('connection', (connection) => {
const port = connection.localPort;
// Check which type of port this is
if (implicitTlsPorts.includes(port)) {
console.log(`Implicit TLS SMTP connection on port ${port} from ${connection.remoteIP}`);
} else if (starttlsPorts.includes(port)) {
console.log(`STARTTLS SMTP connection on port ${port} from ${connection.remoteIP}`);
} else if (nonTlsPorts.includes(port)) {
console.log(`Plain SMTP connection on port ${port} from ${connection.remoteIP}`);
}
});
console.log(`Applied SMTP port configurations to SmartProxy: ${enabledPorts.map(p => p.port).join(', ')}`);
}
}

View File

@ -1 +1,8 @@
// Core DcRouter components
export * from './classes.dcrouter.js'; 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

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

Binary file not shown.