Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
076aac27ce | |||
7f84405279 | |||
13ef31c13f | |||
5cf4c0f150 | |||
04b7552b34 |
30
changelog.md
30
changelog.md
@ -1,5 +1,35 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-05-08 - 2.7.0 - feat(dcrouter)
|
||||
Implement unified email configuration with pattern‐based routing and consolidated email processing. Migrate SMTP forwarding and store‐and‐forward 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 store‐and‐forward 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 store‐and‐forward 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
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/platformservice",
|
||||
"private": true,
|
||||
"version": "2.5.0",
|
||||
"version": "2.7.0",
|
||||
"description": "A multifaceted platform service handling mail, SMS, letter delivery, and AI services.",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
|
1388
readme.plan.md
1388
readme.plan.md
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,10 @@ import { tap, expect } from '@push.rocks/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import {
|
||||
DcRouter,
|
||||
type IDcRouterOptions,
|
||||
type ISmtpForwardingConfig,
|
||||
type IDomainRoutingConfig
|
||||
type IDcRouterOptions,
|
||||
type IEmailConfig,
|
||||
type EmailProcessingMode,
|
||||
type IDomainRule
|
||||
} from '../ts/dcrouter/index.js';
|
||||
|
||||
tap.test('DcRouter class - basic functionality', async () => {
|
||||
@ -21,71 +22,97 @@ tap.test('DcRouter class - basic functionality', async () => {
|
||||
expect(router.options.tls.contactEmail).toEqual('test@example.com');
|
||||
});
|
||||
|
||||
tap.test('DcRouter class - HTTP routing configuration', async () => {
|
||||
// Create HTTP routing configuration
|
||||
const httpRoutes: IDomainRoutingConfig[] = [
|
||||
{
|
||||
domain: 'example.com',
|
||||
targetServer: '192.168.1.10',
|
||||
targetPort: 8080,
|
||||
useTls: true
|
||||
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'
|
||||
},
|
||||
{
|
||||
domain: '*.example.org',
|
||||
targetServer: '192.168.1.20',
|
||||
targetPort: 9000,
|
||||
useTls: false
|
||||
}
|
||||
];
|
||||
|
||||
const options: IDcRouterOptions = {
|
||||
httpDomainRoutes: httpRoutes,
|
||||
tls: {
|
||||
contactEmail: 'test@example.com'
|
||||
}
|
||||
};
|
||||
|
||||
const router = new DcRouter(options);
|
||||
expect(router.options.httpDomainRoutes.length).toEqual(2);
|
||||
expect(router.options.httpDomainRoutes[0].domain).toEqual('example.com');
|
||||
expect(router.options.httpDomainRoutes[1].domain).toEqual('*.example.org');
|
||||
});
|
||||
|
||||
tap.test('DcRouter class - SMTP forwarding configuration', async () => {
|
||||
// Create SMTP forwarding configuration
|
||||
const smtpForwarding: ISmtpForwardingConfig = {
|
||||
enabled: true,
|
||||
ports: [25, 587, 465],
|
||||
defaultServer: 'mail.example.com',
|
||||
defaultPort: 25,
|
||||
useTls: true,
|
||||
preserveSourceIp: true,
|
||||
domainRoutes: [
|
||||
globalPortRanges: [
|
||||
{ from: 80, to: 80 },
|
||||
{ from: 443, to: 443 }
|
||||
],
|
||||
domainConfigs: [
|
||||
{
|
||||
domain: 'example.com',
|
||||
server: 'mail1.example.com',
|
||||
port: 25
|
||||
},
|
||||
{
|
||||
domain: 'example.org',
|
||||
server: 'mail2.example.org',
|
||||
port: 587
|
||||
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 = {
|
||||
smtpForwarding,
|
||||
smartProxyConfig,
|
||||
tls: {
|
||||
contactEmail: 'test@example.com'
|
||||
}
|
||||
};
|
||||
|
||||
const router = new DcRouter(options);
|
||||
expect(router.options.smtpForwarding.enabled).toEqual(true);
|
||||
expect(router.options.smtpForwarding.ports.length).toEqual(3);
|
||||
expect(router.options.smtpForwarding.domainRoutes.length).toEqual(2);
|
||||
expect(router.options.smtpForwarding.domainRoutes[0].domain).toEqual('example.com');
|
||||
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 () => {
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/platformservice',
|
||||
version: '2.5.0',
|
||||
version: '2.7.0',
|
||||
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
|
||||
}
|
||||
|
@ -3,62 +3,24 @@ import * as paths from '../paths.js';
|
||||
import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js';
|
||||
import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js';
|
||||
|
||||
import { type IMtaConfig, MtaService } from '../mta/classes.mta.js';
|
||||
|
||||
// Certificate types are available via plugins.tsclass
|
||||
|
||||
/**
|
||||
* Configuration for SMTP forwarding functionality
|
||||
*/
|
||||
export interface ISmtpForwardingConfig {
|
||||
/** Whether SMTP forwarding is enabled */
|
||||
enabled?: boolean;
|
||||
/** SMTP ports to listen on */
|
||||
ports?: number[];
|
||||
/** Default SMTP server hostname */
|
||||
defaultServer: string;
|
||||
/** Default SMTP server port */
|
||||
defaultPort?: number;
|
||||
/** Whether to use TLS when connecting to the default server */
|
||||
useTls?: boolean;
|
||||
/** Preserve source IP address when forwarding */
|
||||
preserveSourceIp?: boolean;
|
||||
/** Domain-specific routing rules */
|
||||
domainRoutes?: Array<{
|
||||
domain: string;
|
||||
server: string;
|
||||
port?: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple domain-based routing configuration
|
||||
*/
|
||||
export interface IDomainRoutingConfig {
|
||||
/** The domain name or pattern (e.g., example.com or *.example.com) */
|
||||
domain: string;
|
||||
/** Target server hostname or IP */
|
||||
targetServer: string;
|
||||
/** Target port */
|
||||
targetPort: number;
|
||||
/** Enable HTTPS/TLS for this route */
|
||||
useTls?: boolean;
|
||||
/** Allow incoming connections from these IP ranges (default: all) */
|
||||
allowedIps?: string[];
|
||||
}
|
||||
// Import the consolidated email config
|
||||
import type { IEmailConfig } from './classes.email.config.js';
|
||||
import { DomainRouter } from './classes.domain.router.js';
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
/** HTTP/HTTPS domain-based routing */
|
||||
httpDomainRoutes?: IDomainRoutingConfig[];
|
||||
/**
|
||||
* Direct SmartProxy configuration - gives full control over HTTP/HTTPS and TCP/SNI traffic
|
||||
* This is the preferred way to configure HTTP/HTTPS and general TCP/SNI traffic
|
||||
*/
|
||||
smartProxyConfig?: plugins.smartproxy.ISmartProxyOptions;
|
||||
|
||||
/** SMTP forwarding configuration */
|
||||
smtpForwarding?: ISmtpForwardingConfig;
|
||||
|
||||
/** MTA service configuration (if not using SMTP forwarding) */
|
||||
mtaConfig?: IMtaConfig;
|
||||
|
||||
/** Existing MTA service instance to use (if not using SMTP forwarding) */
|
||||
mtaServiceInstance?: MtaService;
|
||||
/**
|
||||
* Consolidated email configuration
|
||||
* This enables all email handling with pattern-based routing
|
||||
*/
|
||||
emailConfig?: IEmailConfig;
|
||||
|
||||
/** TLS/certificate configuration */
|
||||
tls?: {
|
||||
@ -89,15 +51,17 @@ export interface PortProxyRuleContext {
|
||||
proxy: plugins.smartproxy.SmartProxy;
|
||||
configs: plugins.smartproxy.IPortProxySettings['domainConfigs'];
|
||||
}
|
||||
|
||||
export class DcRouter {
|
||||
public options: IDcRouterOptions;
|
||||
|
||||
// Core services
|
||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||
public smtpProxy?: plugins.smartproxy.SmartProxy;
|
||||
public mta?: MtaService;
|
||||
public dnsServer?: plugins.smartdns.DnsServer;
|
||||
|
||||
// Unified email components
|
||||
public domainRouter?: DomainRouter;
|
||||
|
||||
// Environment access
|
||||
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
||||
|
||||
@ -112,14 +76,14 @@ export class DcRouter {
|
||||
console.log('Starting DcRouter services...');
|
||||
|
||||
try {
|
||||
// 1. Set up HTTP/HTTPS traffic handling with SmartProxy
|
||||
await this.setupHttpProxy();
|
||||
// Set up SmartProxy for HTTP/HTTPS and general TCP/SNI traffic
|
||||
if (this.options.smartProxyConfig) {
|
||||
await this.setupSmartProxy();
|
||||
}
|
||||
|
||||
// 2. Set up MTA or SMTP forwarding
|
||||
if (this.options.smtpForwarding?.enabled) {
|
||||
await this.setupSmtpForwarding();
|
||||
} else {
|
||||
await this.setupMtaService();
|
||||
// Set up unified email handling if configured
|
||||
if (this.options.emailConfig) {
|
||||
await this.setupUnifiedEmailHandling();
|
||||
}
|
||||
|
||||
// 3. Set up DNS server if configured
|
||||
@ -139,119 +103,40 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up SmartProxy for HTTP/HTTPS traffic
|
||||
* Set up SmartProxy with direct configuration
|
||||
*/
|
||||
private async setupHttpProxy() {
|
||||
if (!this.options.httpDomainRoutes || this.options.httpDomainRoutes.length === 0) {
|
||||
console.log('No HTTP domain routes configured, skipping HTTP proxy setup');
|
||||
private async setupSmartProxy(): Promise<void> {
|
||||
if (!this.options.smartProxyConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Setting up SmartProxy for HTTP/HTTPS traffic');
|
||||
console.log('Setting up SmartProxy with direct configuration');
|
||||
|
||||
// Prepare SmartProxy configuration
|
||||
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
||||
fromPort: 443,
|
||||
toPort: this.options.httpDomainRoutes[0].targetPort,
|
||||
targetIP: this.options.httpDomainRoutes[0].targetServer,
|
||||
sniEnabled: true,
|
||||
acme: {
|
||||
port: 80,
|
||||
enabled: true,
|
||||
autoRenew: true,
|
||||
useProduction: true,
|
||||
renewThresholdDays: 30,
|
||||
accountEmail: this.options.tls?.contactEmail || 'admin@example.com' // ACME requires an email
|
||||
},
|
||||
globalPortRanges: [{ from: 443, to: 443 }],
|
||||
domainConfigs: []
|
||||
};
|
||||
// Create SmartProxy instance with full configuration
|
||||
this.smartProxy = new plugins.smartproxy.SmartProxy(this.options.smartProxyConfig);
|
||||
|
||||
// Create domain configs from the HTTP routes
|
||||
smartProxyConfig.domainConfigs = this.options.httpDomainRoutes.map(route => ({
|
||||
domains: [route.domain],
|
||||
targetIPs: [route.targetServer],
|
||||
allowedIPs: route.allowedIps || ['0.0.0.0/0'],
|
||||
// Skip certificate management for wildcard domains as it's not supported by HTTP-01 challenges
|
||||
certificateManagement: !route.domain.includes('*')
|
||||
}));
|
||||
|
||||
// Create and start the SmartProxy instance
|
||||
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
||||
|
||||
// Listen for certificate events
|
||||
this.smartProxy.on('certificate-issued', event => {
|
||||
console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
|
||||
// Set up event listeners
|
||||
this.smartProxy.on('error', (err) => {
|
||||
console.error('SmartProxy error:', err);
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-renewed', event => {
|
||||
console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
if (this.options.smartProxyConfig.acme) {
|
||||
this.smartProxy.on('certificate-issued', (event) => {
|
||||
console.log(`Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
|
||||
this.smartProxy.on('certificate-renewed', (event) => {
|
||||
console.log(`Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Start SmartProxy
|
||||
await this.smartProxy.start();
|
||||
|
||||
console.log(`HTTP/HTTPS proxy configured with ${smartProxyConfig.domainConfigs.length} domain routes`);
|
||||
console.log('SmartProxy started successfully');
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the MTA service
|
||||
*/
|
||||
private async setupMtaService() {
|
||||
// Use existing MTA service if provided
|
||||
if (this.options.mtaServiceInstance) {
|
||||
this.mta = this.options.mtaServiceInstance;
|
||||
console.log('Using provided MTA service instance');
|
||||
} 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');
|
||||
|
||||
// Start the MTA service
|
||||
await this.mta.start();
|
||||
console.log('MTA service started');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up SMTP forwarding with SmartProxy
|
||||
*/
|
||||
private async setupSmtpForwarding() {
|
||||
if (!this.options.smtpForwarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
const forwarding = this.options.smtpForwarding;
|
||||
console.log('Setting up SMTP forwarding');
|
||||
|
||||
// Determine which ports to listen on
|
||||
const smtpPorts = forwarding.ports || [25, 587, 465];
|
||||
|
||||
// Create SmartProxy instance for SMTP forwarding
|
||||
this.smtpProxy = new plugins.smartproxy.SmartProxy({
|
||||
// Listen on the first SMTP port
|
||||
fromPort: smtpPorts[0],
|
||||
// Forward to the default server
|
||||
toPort: forwarding.defaultPort || 25,
|
||||
targetIP: forwarding.defaultServer,
|
||||
// Enable SNI if port 465 is included (implicit TLS)
|
||||
sniEnabled: smtpPorts.includes(465),
|
||||
// Preserve source IP if requested
|
||||
preserveSourceIP: forwarding.preserveSourceIp || false,
|
||||
// Create domain configs for SMTP routing
|
||||
domainConfigs: forwarding.domainRoutes?.map(route => ({
|
||||
domains: [route.domain],
|
||||
allowedIPs: ['0.0.0.0/0'], // Allow from anywhere by default
|
||||
targetIPs: [route.server]
|
||||
})) || [],
|
||||
// Include all SMTP ports in the global port ranges
|
||||
globalPortRanges: smtpPorts.map(port => ({ from: port, to: port }))
|
||||
});
|
||||
|
||||
// Start the SMTP proxy
|
||||
await this.smtpProxy.start();
|
||||
|
||||
console.log(`SMTP forwarding configured on ports ${smtpPorts.join(', ')}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a domain matches a pattern (including wildcard support)
|
||||
@ -287,16 +172,11 @@ export class DcRouter {
|
||||
try {
|
||||
// Stop all services in parallel for faster shutdown
|
||||
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 HTTP SmartProxy if running
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping HTTP SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop SMTP SmartProxy if running
|
||||
this.smtpProxy ? this.smtpProxy.stop().catch(err => console.error('Error stopping SMTP SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop MTA service if it's our own (not an external instance)
|
||||
(this.mta && !this.options.mtaServiceInstance) ?
|
||||
this.mta.stop().catch(err => console.error('Error stopping MTA service:', err)) :
|
||||
Promise.resolve(),
|
||||
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
// Stop DNS server if running
|
||||
this.dnsServer ?
|
||||
@ -312,46 +192,84 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update HTTP domain routes
|
||||
* @param routes New HTTP domain routes
|
||||
* Update SmartProxy configuration
|
||||
* @param config New SmartProxy configuration
|
||||
*/
|
||||
public async updateHttpRoutes(routes: IDomainRoutingConfig[]): Promise<void> {
|
||||
this.options.httpDomainRoutes = routes;
|
||||
|
||||
// If SmartProxy is already running, we need to restart it with the new configuration
|
||||
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
|
||||
// Stop existing SmartProxy if running
|
||||
if (this.smartProxy) {
|
||||
// Stop the existing SmartProxy
|
||||
await this.smartProxy.stop();
|
||||
this.smartProxy = undefined;
|
||||
|
||||
// Start a new SmartProxy with the updated configuration
|
||||
await this.setupHttpProxy();
|
||||
}
|
||||
|
||||
console.log(`Updated HTTP routes with ${routes.length} domains`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update SMTP forwarding configuration
|
||||
* @param config New SMTP forwarding configuration
|
||||
*/
|
||||
public async updateSmtpForwarding(config: ISmtpForwardingConfig): Promise<void> {
|
||||
// Stop existing SMTP proxy if running
|
||||
if (this.smtpProxy) {
|
||||
await this.smtpProxy.stop();
|
||||
this.smtpProxy = undefined;
|
||||
}
|
||||
|
||||
// Update configuration
|
||||
this.options.smtpForwarding = config;
|
||||
this.options.smartProxyConfig = config;
|
||||
|
||||
// Restart SMTP forwarding if enabled
|
||||
if (config.enabled) {
|
||||
await this.setupSmtpForwarding();
|
||||
// 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');
|
||||
}
|
||||
|
||||
console.log('SMTP forwarding configuration updated');
|
||||
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;
|
||||
|
351
ts/dcrouter/classes.domain.router.ts
Normal file
351
ts/dcrouter/classes.domain.router.ts
Normal 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');
|
||||
}
|
||||
}
|
129
ts/dcrouter/classes.email.config.ts
Normal file
129
ts/dcrouter/classes.email.config.ts
Normal 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;
|
||||
}
|
@ -1,3 +1,8 @@
|
||||
// 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';
|
||||
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@serve.zone/platformservice',
|
||||
version: '2.5.0',
|
||||
version: '2.7.0',
|
||||
description: 'A multifaceted platform service handling mail, SMS, letter delivery, and AI services.'
|
||||
}
|
||||
|
Binary file not shown.
Reference in New Issue
Block a user