Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
076aac27ce | |||
7f84405279 | |||
13ef31c13f | |||
5cf4c0f150 | |||
04b7552b34 | |||
1528d29b0d | |||
9d895898b1 |
39
changelog.md
39
changelog.md
@ -1,5 +1,44 @@
|
||||
# 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
|
||||
|
||||
- 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)
|
||||
Update test assertions and singleton instance references in DMARC, integration, and IP warmup manager tests
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@serve.zone/platformservice",
|
||||
"private": true,
|
||||
"version": "2.4.2",
|
||||
"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",
|
||||
|
@ -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)
|
1324
readme.plan.md
1324
readme.plan.md
File diff suppressed because it is too large
Load Diff
146
test/test.dcrouter.ts
Normal file
146
test/test.dcrouter.ts
Normal 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();
|
@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
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.'
|
||||
}
|
||||
|
@ -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 type DcRouter from './classes.dcrouter.js';
|
||||
|
||||
export class SzDcRouterConnector {
|
||||
public qenv: plugins.qenv.Qenv;
|
||||
public dcRouterRef: DcRouter;
|
||||
|
||||
constructor(dcRouterRef: DcRouter) {
|
||||
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> {
|
||||
|
@ -1,23 +1,39 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import { SzDcRouterConnector } from './classes.dcr.sz.connector.js';
|
||||
|
||||
import type { SzPlatformService } from '../platformservice.js';
|
||||
import { type IMtaConfig, MtaService } from '../mta/classes.mta.js';
|
||||
import { SmtpPortConfig, type ISmtpPortSettings } from './classes.smtp.portconfig.js';
|
||||
import { EmailDomainRouter, type IEmailDomainRoutingConfig } from './classes.email.domainrouter.js';
|
||||
|
||||
// Certificate types are available via plugins.tsclass
|
||||
|
||||
export interface IDcRouterOptions {
|
||||
platformServiceInstance?: SzPlatformService;
|
||||
// Import the consolidated email config
|
||||
import type { IEmailConfig } from './classes.email.config.js';
|
||||
import { DomainRouter } from './classes.domain.router.js';
|
||||
|
||||
/** 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;
|
||||
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;
|
||||
};
|
||||
|
||||
/** DNS server configuration */
|
||||
dnsServerConfig?: plugins.smartdns.IDnsServerOptions;
|
||||
}
|
||||
@ -35,14 +51,20 @@ export interface PortProxyRuleContext {
|
||||
proxy: plugins.smartproxy.SmartProxy;
|
||||
configs: plugins.smartproxy.IPortProxySettings['domainConfigs'];
|
||||
}
|
||||
|
||||
export class DcRouter {
|
||||
public szDcRouterConnector = new SzDcRouterConnector(this);
|
||||
public options: IDcRouterOptions;
|
||||
|
||||
// Core services
|
||||
public smartProxy?: plugins.smartproxy.SmartProxy;
|
||||
public mta?: MtaService;
|
||||
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) {
|
||||
// Set defaults in options
|
||||
this.options = {
|
||||
@ -51,194 +73,203 @@ export class DcRouter {
|
||||
}
|
||||
|
||||
public async start() {
|
||||
// Set up MTA service - use existing instance if provided
|
||||
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 {
|
||||
const domainSupported = await smartAcmeInstance.challengeHandlers[0].checkWetherDomainIsSupported(domainArg);
|
||||
if (!domainSupported) {
|
||||
return null; // Let SmartProxy handle with default mechanism
|
||||
}
|
||||
// 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
|
||||
const result: plugins.tsclass.network.ICert = {
|
||||
id: `cert-${domainArg}`,
|
||||
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
|
||||
}
|
||||
};
|
||||
console.log('Starting DcRouter services...');
|
||||
|
||||
try {
|
||||
// Set up SmartProxy for HTTP/HTTPS and general TCP/SNI traffic
|
||||
if (this.options.smartProxyConfig) {
|
||||
await this.setupSmartProxy();
|
||||
}
|
||||
|
||||
// Create the SmartProxy instance with the appropriate cert provisioning function
|
||||
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();
|
||||
// Set up unified email handling if configured
|
||||
if (this.options.emailConfig) {
|
||||
await this.setupUnifiedEmailHandling();
|
||||
}
|
||||
}
|
||||
|
||||
// DNS server
|
||||
if (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();
|
||||
|
||||
// 3. Set up DNS server if configured
|
||||
if (this.options.dnsServerConfig) {
|
||||
this.dnsServer = new plugins.smartdns.DnsServer(this.options.dnsServerConfig);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up SmartProxy with direct configuration
|
||||
*/
|
||||
private async setupSmartProxy(): Promise<void> {
|
||||
if (!this.options.smartProxyConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Setting up SmartProxy with direct configuration');
|
||||
|
||||
// Create SmartProxy instance with full configuration
|
||||
this.smartProxy = new plugins.smartproxy.SmartProxy(this.options.smartProxyConfig);
|
||||
|
||||
// Set up event listeners
|
||||
this.smartProxy.on('error', (err) => {
|
||||
console.error('SmartProxy error:', err);
|
||||
});
|
||||
|
||||
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('SmartProxy started successfully');
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Configure SmartProxy for SMTP ports
|
||||
* 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
|
||||
*/
|
||||
public configureSmtpProxy(): void {
|
||||
if (!this.smartProxy || !this.mta) return;
|
||||
|
||||
const mtaPort = this.mta.config.smtp?.port || 25;
|
||||
try {
|
||||
// Configure SmartProxy to forward SMTP ports to the MTA service
|
||||
const settings = this.smartProxy.settings;
|
||||
// Ensure localhost target for MTA
|
||||
settings.targetIP = settings.targetIP || 'localhost';
|
||||
// Forward all SMTP ports to the MTA port
|
||||
settings.toPort = mtaPort;
|
||||
// Initialize globalPortRanges if needed
|
||||
if (!settings.globalPortRanges) {
|
||||
settings.globalPortRanges = [];
|
||||
}
|
||||
// Add SMTP ports 25, 587, 465 if not already present
|
||||
for (const port of [25, 587, 465]) {
|
||||
if (!settings.globalPortRanges.some((r) => r.from <= port && port <= r.to)) {
|
||||
settings.globalPortRanges.push({ from: port, to: port });
|
||||
}
|
||||
}
|
||||
console.log(`Configured SmartProxy for SMTP ports: 25, 587, 465 → localhost:${mtaPort}`);
|
||||
} catch (error) {
|
||||
console.error('Failed to configure SmartProxy for SMTP:', error);
|
||||
private isDomainMatch(domain: string, pattern: string): boolean {
|
||||
// Normalize inputs
|
||||
domain = domain.toLowerCase();
|
||||
pattern = pattern.toLowerCase();
|
||||
|
||||
// Check for exact match
|
||||
if (domain === pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for wildcard match (*.example.com)
|
||||
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() {
|
||||
// Stop SmartProxy
|
||||
if (this.smartProxy) {
|
||||
await this.smartProxy.stop();
|
||||
}
|
||||
console.log('Stopping DcRouter services...');
|
||||
|
||||
// Stop MTA service if it's our own (not an external instance)
|
||||
if (this.mta && !this.options.mtaServiceInstance) {
|
||||
await this.mta.stop();
|
||||
}
|
||||
|
||||
// Stop DNS server
|
||||
if (this.dnsServer) {
|
||||
await this.dnsServer.stop();
|
||||
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 SmartProxy:', err)) : Promise.resolve(),
|
||||
|
||||
// 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(
|
||||
priority: number,
|
||||
check: (email: any) => Promise<any>,
|
||||
action: (email: any) => Promise<any>
|
||||
): void {
|
||||
this.smtpRuleEngine?.createRule(priority, check, action);
|
||||
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
|
||||
// Stop existing SmartProxy if running
|
||||
if (this.smartProxy) {
|
||||
await this.smartProxy.stop();
|
||||
this.smartProxy = undefined;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
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;
|
||||
}
|
431
ts/dcrouter/classes.email.domainrouter.ts
Normal file
431
ts/dcrouter/classes.email.domainrouter.ts
Normal 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;
|
||||
}
|
||||
}
|
311
ts/dcrouter/classes.smtp.portconfig.ts
Normal file
311
ts/dcrouter/classes.smtp.portconfig.ts
Normal 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(', ')}`);
|
||||
}
|
||||
}
|
@ -1 +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.4.2',
|
||||
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