2025-05-08 01:13:54 +00:00
|
|
|
import * as plugins from './plugins.js';
|
|
|
|
import * as paths from './paths.js';
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-07 22:06:55 +00:00
|
|
|
// Certificate types are available via plugins.tsclass
|
2025-05-07 14:33:20 +00:00
|
|
|
|
2025-05-27 19:28:12 +00:00
|
|
|
// Import the email server and its configuration
|
|
|
|
import { UnifiedEmailServer, type IUnifiedEmailServerOptions } from './mail/routing/classes.unified.email.server.js';
|
2025-05-28 13:23:45 +00:00
|
|
|
import type { IEmailRoute } from './mail/routing/interfaces.js';
|
2025-05-08 01:13:54 +00:00
|
|
|
import { logger } from './logger.js';
|
2025-05-21 00:12:49 +00:00
|
|
|
// Import the email configuration helpers directly from mail/delivery
|
|
|
|
import { configureEmailStorage, configureEmailServer } from './mail/delivery/index.js';
|
2025-05-30 05:30:06 +00:00
|
|
|
// Import storage manager
|
|
|
|
import { StorageManager, type IStorageConfig } from './storage/index.js';
|
2025-05-07 23:04:54 +00:00
|
|
|
|
|
|
|
export interface IDcRouterOptions {
|
2025-05-07 23:45:19 +00:00
|
|
|
/**
|
|
|
|
* 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;
|
|
|
|
|
|
|
|
/**
|
2025-05-27 19:28:12 +00:00
|
|
|
* Email server configuration
|
2025-05-08 00:12:36 +00:00
|
|
|
* This enables all email handling with pattern-based routing
|
2025-05-07 23:45:19 +00:00
|
|
|
*/
|
2025-05-27 19:28:12 +00:00
|
|
|
emailConfig?: IUnifiedEmailServerOptions;
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-20 11:04:09 +00:00
|
|
|
/**
|
|
|
|
* Custom email port configuration
|
|
|
|
* Allows configuring specific ports for email handling
|
|
|
|
* This overrides the default port mapping in the emailConfig
|
|
|
|
*/
|
|
|
|
emailPortConfig?: {
|
|
|
|
/** External to internal port mapping */
|
|
|
|
portMapping?: Record<number, number>;
|
|
|
|
/** Custom port configuration for specific ports */
|
|
|
|
portSettings?: Record<number, any>;
|
|
|
|
/** Path to store received emails */
|
|
|
|
receivedEmailsPath?: string;
|
|
|
|
};
|
|
|
|
|
2025-05-07 23:04:54 +00:00
|
|
|
/** 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;
|
2025-05-21 00:12:49 +00:00
|
|
|
/** Path to CA certificate file (for custom CAs) */
|
|
|
|
caPath?: string;
|
2025-05-07 23:04:54 +00:00
|
|
|
};
|
|
|
|
|
2025-05-30 10:34:50 +00:00
|
|
|
/**
|
|
|
|
* DNS domain for automatic DNS server setup with DoH
|
|
|
|
* When set, DNS server will:
|
|
|
|
* - Always bind to UDP port 53 on the VM's IP address
|
|
|
|
* - Use socket-handler approach for DNS-over-HTTPS
|
|
|
|
* - Automatically handle NS delegation validation
|
|
|
|
*/
|
2025-05-29 16:26:19 +00:00
|
|
|
dnsDomain?: string;
|
2025-05-19 17:34:48 +00:00
|
|
|
|
2025-05-30 10:34:50 +00:00
|
|
|
/**
|
|
|
|
* DNS records to register when using dnsDomain
|
|
|
|
* These are in addition to auto-generated records from email domains with internal-dns mode
|
|
|
|
*/
|
|
|
|
dnsRecords?: Array<{
|
|
|
|
name: string;
|
|
|
|
type: 'A' | 'AAAA' | 'CNAME' | 'MX' | 'TXT' | 'NS' | 'SOA';
|
|
|
|
value: string;
|
|
|
|
ttl?: number;
|
|
|
|
}>;
|
|
|
|
|
2025-05-19 17:34:48 +00:00
|
|
|
/** DNS challenge configuration for ACME (optional) */
|
|
|
|
dnsChallenge?: {
|
|
|
|
/** Cloudflare API key for DNS challenges */
|
|
|
|
cloudflareApiKey?: string;
|
|
|
|
/** Other DNS providers can be added here */
|
|
|
|
};
|
2025-05-30 05:30:06 +00:00
|
|
|
|
|
|
|
/** Storage configuration */
|
|
|
|
storage?: IStorageConfig;
|
2025-05-04 10:10:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* DcRouter can be run on ingress and egress to and from a datacenter site.
|
|
|
|
*/
|
2025-05-07 14:33:20 +00:00
|
|
|
/**
|
|
|
|
* Context passed to HTTP routing rules
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* Context passed to port proxy (SmartProxy) routing rules
|
|
|
|
*/
|
|
|
|
export interface PortProxyRuleContext {
|
|
|
|
proxy: plugins.smartproxy.SmartProxy;
|
2025-05-16 15:50:46 +00:00
|
|
|
routes: plugins.smartproxy.IRouteConfig[];
|
2025-05-07 14:33:20 +00:00
|
|
|
}
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-04 10:10:07 +00:00
|
|
|
export class DcRouter {
|
2025-05-07 14:33:20 +00:00
|
|
|
public options: IDcRouterOptions;
|
2025-05-07 23:04:54 +00:00
|
|
|
|
|
|
|
// Core services
|
2025-05-07 14:33:20 +00:00
|
|
|
public smartProxy?: plugins.smartproxy.SmartProxy;
|
2025-05-29 16:26:19 +00:00
|
|
|
public dnsServer?: plugins.smartdns.dnsServerMod.DnsServer;
|
2025-05-27 14:06:22 +00:00
|
|
|
public emailServer?: UnifiedEmailServer;
|
2025-05-30 05:30:06 +00:00
|
|
|
public storageManager: StorageManager;
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-20 11:04:09 +00:00
|
|
|
|
2025-05-07 23:04:54 +00:00
|
|
|
// Environment access
|
|
|
|
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
|
|
|
|
|
2025-05-21 02:17:18 +00:00
|
|
|
constructor(optionsArg: IDcRouterOptions) {
|
2025-05-07 22:06:55 +00:00
|
|
|
// Set defaults in options
|
|
|
|
this.options = {
|
|
|
|
...optionsArg
|
|
|
|
};
|
2025-05-20 11:04:09 +00:00
|
|
|
|
2025-05-30 05:30:06 +00:00
|
|
|
// Initialize storage manager
|
|
|
|
this.storageManager = new StorageManager(this.options.storage);
|
2025-05-07 14:33:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public async start() {
|
2025-05-07 23:04:54 +00:00
|
|
|
console.log('Starting DcRouter services...');
|
|
|
|
|
|
|
|
try {
|
2025-05-16 15:50:46 +00:00
|
|
|
// Set up SmartProxy for HTTP/HTTPS and all traffic including email routes
|
|
|
|
await this.setupSmartProxy();
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-08 00:12:36 +00:00
|
|
|
// Set up unified email handling if configured
|
|
|
|
if (this.options.emailConfig) {
|
|
|
|
await this.setupUnifiedEmailHandling();
|
2025-05-20 11:04:09 +00:00
|
|
|
|
|
|
|
// Apply custom email storage configuration if available
|
2025-05-27 14:06:22 +00:00
|
|
|
if (this.emailServer && this.options.emailPortConfig?.receivedEmailsPath) {
|
2025-05-20 11:04:09 +00:00
|
|
|
logger.log('info', 'Applying custom email storage configuration');
|
2025-05-27 14:06:22 +00:00
|
|
|
configureEmailStorage(this.emailServer, this.options);
|
2025-05-20 11:04:09 +00:00
|
|
|
}
|
2025-05-07 23:04:54 +00:00
|
|
|
}
|
|
|
|
|
2025-05-29 16:26:19 +00:00
|
|
|
// Set up DNS server if configured by dnsDomain
|
|
|
|
if (this.options.dnsDomain) {
|
|
|
|
await this.setupDnsWithSocketHandler();
|
2025-05-07 23:04:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2025-05-16 15:50:46 +00:00
|
|
|
* Set up SmartProxy with direct configuration and automatic email routes
|
2025-05-07 23:04:54 +00:00
|
|
|
*/
|
2025-05-07 23:45:19 +00:00
|
|
|
private async setupSmartProxy(): Promise<void> {
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log('[DcRouter] Setting up SmartProxy...');
|
2025-05-16 15:50:46 +00:00
|
|
|
let routes: plugins.smartproxy.IRouteConfig[] = [];
|
|
|
|
let acmeConfig: plugins.smartproxy.IAcmeOptions | undefined;
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// If user provides full SmartProxy config, use it directly
|
|
|
|
if (this.options.smartProxyConfig) {
|
|
|
|
routes = this.options.smartProxyConfig.routes || [];
|
|
|
|
acmeConfig = this.options.smartProxyConfig.acme;
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log(`[DcRouter] Found ${routes.length} routes in config`);
|
|
|
|
console.log(`[DcRouter] ACME config present: ${!!acmeConfig}`);
|
2025-05-16 15:50:46 +00:00
|
|
|
}
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// If email config exists, automatically add email routes
|
|
|
|
if (this.options.emailConfig) {
|
|
|
|
const emailRoutes = this.generateEmailRoutes(this.options.emailConfig);
|
2025-05-20 19:46:59 +00:00
|
|
|
console.log(`Email Routes are:`)
|
|
|
|
console.log(emailRoutes)
|
2025-05-21 00:12:49 +00:00
|
|
|
routes = [...routes, ...emailRoutes]; // Enable email routing through SmartProxy
|
2025-05-16 15:50:46 +00:00
|
|
|
}
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-29 16:26:19 +00:00
|
|
|
// If DNS domain is configured, add DNS routes
|
|
|
|
if (this.options.dnsDomain) {
|
|
|
|
const dnsRoutes = this.generateDnsRoutes();
|
|
|
|
console.log(`DNS Routes for domain ${this.options.dnsDomain}:`, dnsRoutes);
|
|
|
|
routes = [...routes, ...dnsRoutes];
|
|
|
|
}
|
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// Merge TLS/ACME configuration if provided at root level
|
|
|
|
if (this.options.tls && !acmeConfig) {
|
|
|
|
acmeConfig = {
|
|
|
|
accountEmail: this.options.tls.contactEmail,
|
|
|
|
enabled: true,
|
|
|
|
useProduction: true,
|
|
|
|
autoRenew: true,
|
|
|
|
renewThresholdDays: 30
|
|
|
|
};
|
|
|
|
}
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-19 17:34:48 +00:00
|
|
|
// Configure DNS challenge if available
|
|
|
|
let challengeHandlers: any[] = [];
|
|
|
|
if (this.options.dnsChallenge?.cloudflareApiKey) {
|
|
|
|
console.log('Configuring Cloudflare DNS challenge for ACME');
|
|
|
|
const cloudflareAccount = new plugins.cloudflare.CloudflareAccount(this.options.dnsChallenge.cloudflareApiKey);
|
|
|
|
const dns01Handler = new plugins.smartacme.handlers.Dns01Handler(cloudflareAccount);
|
|
|
|
challengeHandlers.push(dns01Handler);
|
|
|
|
}
|
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// If we have routes or need a basic SmartProxy instance, create it
|
|
|
|
if (routes.length > 0 || this.options.smartProxyConfig) {
|
|
|
|
console.log('Setting up SmartProxy with combined configuration');
|
|
|
|
|
|
|
|
// Create SmartProxy configuration
|
|
|
|
const smartProxyConfig: plugins.smartproxy.ISmartProxyOptions = {
|
|
|
|
...this.options.smartProxyConfig,
|
|
|
|
routes,
|
|
|
|
acme: acmeConfig
|
|
|
|
};
|
|
|
|
|
2025-05-19 17:34:48 +00:00
|
|
|
// If we have DNS challenge handlers, enhance the config
|
|
|
|
if (challengeHandlers.length > 0) {
|
|
|
|
// We'll need to pass this to SmartProxy somehow
|
|
|
|
// For now, we'll set it as a property
|
|
|
|
(smartProxyConfig as any).acmeChallengeHandlers = challengeHandlers;
|
|
|
|
(smartProxyConfig as any).acmeChallengePriority = ['dns-01', 'http-01'];
|
|
|
|
}
|
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// Create SmartProxy instance
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log('[DcRouter] Creating SmartProxy instance with config:', JSON.stringify({
|
|
|
|
routeCount: smartProxyConfig.routes?.length,
|
|
|
|
acmeEnabled: smartProxyConfig.acme?.enabled,
|
|
|
|
acmeEmail: smartProxyConfig.acme?.email,
|
|
|
|
certProvisionFunction: !!smartProxyConfig.certProvisionFunction
|
|
|
|
}, null, 2));
|
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// Set up event listeners
|
|
|
|
this.smartProxy.on('error', (err) => {
|
2025-05-19 17:34:48 +00:00
|
|
|
console.error('[DcRouter] SmartProxy error:', err);
|
|
|
|
console.error('[DcRouter] Error stack:', err.stack);
|
2025-05-07 23:45:19 +00:00
|
|
|
});
|
2025-05-16 15:50:46 +00:00
|
|
|
|
|
|
|
if (acmeConfig) {
|
|
|
|
this.smartProxy.on('certificate-issued', (event) => {
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log(`[DcRouter] Certificate issued for ${event.domain}, expires ${event.expiryDate}`);
|
2025-05-16 15:50:46 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
this.smartProxy.on('certificate-renewed', (event) => {
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log(`[DcRouter] Certificate renewed for ${event.domain}, expires ${event.expiryDate}`);
|
|
|
|
});
|
|
|
|
|
|
|
|
this.smartProxy.on('certificate-failed', (event) => {
|
|
|
|
console.error(`[DcRouter] Certificate failed for ${event.domain}:`, event.error);
|
2025-05-16 15:50:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Start SmartProxy
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log('[DcRouter] Starting SmartProxy...');
|
2025-05-16 15:50:46 +00:00
|
|
|
await this.smartProxy.start();
|
2025-05-19 17:34:48 +00:00
|
|
|
console.log('[DcRouter] SmartProxy started successfully');
|
2025-05-16 15:50:46 +00:00
|
|
|
|
|
|
|
console.log(`SmartProxy started with ${routes.length} routes`);
|
2025-05-07 23:45:19 +00:00
|
|
|
}
|
2025-05-07 23:04:54 +00:00
|
|
|
}
|
|
|
|
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-04 10:10:07 +00:00
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
/**
|
|
|
|
* Generate SmartProxy routes for email configuration
|
|
|
|
*/
|
2025-05-27 19:28:12 +00:00
|
|
|
private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
|
2025-05-16 15:50:46 +00:00
|
|
|
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
|
|
|
|
|
|
// Create routes for each email port
|
|
|
|
for (const port of emailConfig.ports) {
|
2025-05-20 11:04:09 +00:00
|
|
|
// Create a descriptive name for the route based on the port
|
|
|
|
let routeName = 'email-route';
|
|
|
|
let tlsMode = 'passthrough';
|
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// Handle different email ports differently
|
|
|
|
switch (port) {
|
|
|
|
case 25: // SMTP
|
2025-05-20 11:04:09 +00:00
|
|
|
routeName = 'smtp-route';
|
|
|
|
tlsMode = 'passthrough'; // STARTTLS handled by email server
|
2025-05-16 15:50:46 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 587: // Submission
|
2025-05-20 11:04:09 +00:00
|
|
|
routeName = 'submission-route';
|
|
|
|
tlsMode = 'passthrough'; // STARTTLS handled by email server
|
2025-05-16 15:50:46 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case 465: // SMTPS
|
2025-05-20 11:04:09 +00:00
|
|
|
routeName = 'smtps-route';
|
|
|
|
tlsMode = 'terminate'; // Terminate TLS and re-encrypt to email server
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
routeName = `email-port-${port}-route`;
|
|
|
|
tlsMode = 'passthrough';
|
|
|
|
|
|
|
|
// Check if we have specific settings for this port
|
|
|
|
if (this.options.emailPortConfig?.portSettings &&
|
|
|
|
this.options.emailPortConfig.portSettings[port]) {
|
|
|
|
const portSettings = this.options.emailPortConfig.portSettings[port];
|
|
|
|
|
|
|
|
// If this port requires TLS termination, set the mode accordingly
|
|
|
|
if (portSettings.terminateTls) {
|
|
|
|
tlsMode = 'terminate';
|
2025-05-16 15:50:46 +00:00
|
|
|
}
|
2025-05-20 11:04:09 +00:00
|
|
|
|
|
|
|
// Override the route name if specified
|
|
|
|
if (portSettings.routeName) {
|
|
|
|
routeName = portSettings.routeName;
|
|
|
|
}
|
|
|
|
}
|
2025-05-16 15:50:46 +00:00
|
|
|
break;
|
|
|
|
}
|
2025-05-20 11:04:09 +00:00
|
|
|
|
2025-05-29 16:26:19 +00:00
|
|
|
// Create action based on mode
|
|
|
|
let action: any;
|
|
|
|
|
|
|
|
if (emailConfig.useSocketHandler) {
|
|
|
|
// Socket-handler mode
|
|
|
|
action = {
|
|
|
|
type: 'socket-handler' as any,
|
|
|
|
socketHandler: this.createMailSocketHandler(port)
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
// Traditional forwarding mode
|
|
|
|
const defaultPortMapping = {
|
|
|
|
25: 10025, // SMTP
|
|
|
|
587: 10587, // Submission
|
|
|
|
465: 10465 // SMTPS
|
|
|
|
};
|
|
|
|
|
|
|
|
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
|
|
|
|
const internalPort = portMapping[port] || port + 10000;
|
|
|
|
|
|
|
|
action = {
|
2025-05-20 11:04:09 +00:00
|
|
|
type: 'forward',
|
|
|
|
target: {
|
|
|
|
host: 'localhost', // Forward to internal email server
|
|
|
|
port: internalPort
|
|
|
|
},
|
|
|
|
tls: {
|
|
|
|
mode: tlsMode as any
|
|
|
|
}
|
2025-05-29 16:26:19 +00:00
|
|
|
};
|
|
|
|
}
|
2025-05-20 11:04:09 +00:00
|
|
|
|
|
|
|
// For TLS terminate mode, add certificate info
|
2025-05-29 16:26:19 +00:00
|
|
|
if (tlsMode === 'terminate' && action.tls) {
|
|
|
|
action.tls.certificate = 'auto';
|
2025-05-20 11:04:09 +00:00
|
|
|
}
|
|
|
|
|
2025-05-29 16:26:19 +00:00
|
|
|
// Create the route configuration
|
|
|
|
const routeConfig: plugins.smartproxy.IRouteConfig = {
|
|
|
|
name: routeName,
|
|
|
|
match: {
|
|
|
|
ports: [port]
|
|
|
|
},
|
|
|
|
action: action
|
|
|
|
};
|
|
|
|
|
2025-05-20 11:04:09 +00:00
|
|
|
// Add the route to our list
|
|
|
|
emailRoutes.push(routeConfig);
|
2025-05-16 15:50:46 +00:00
|
|
|
}
|
|
|
|
|
2025-05-29 16:26:19 +00:00
|
|
|
// Add email domain-based routes if configured
|
2025-05-28 13:23:45 +00:00
|
|
|
if (emailConfig.routes) {
|
|
|
|
for (const route of emailConfig.routes) {
|
|
|
|
emailRoutes.push({
|
|
|
|
name: route.name,
|
|
|
|
match: {
|
|
|
|
ports: emailConfig.ports,
|
|
|
|
domains: route.match.recipients ? [route.match.recipients.toString().split('@')[1]] : []
|
|
|
|
},
|
|
|
|
action: {
|
|
|
|
type: 'forward',
|
|
|
|
target: route.action.type === 'forward' && route.action.forward ? {
|
|
|
|
host: route.action.forward.host,
|
|
|
|
port: route.action.forward.port || 25
|
|
|
|
} : undefined,
|
|
|
|
tls: {
|
|
|
|
mode: 'passthrough'
|
2025-05-16 15:50:46 +00:00
|
|
|
}
|
2025-05-28 13:23:45 +00:00
|
|
|
}
|
|
|
|
});
|
2025-05-16 15:50:46 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return emailRoutes;
|
|
|
|
}
|
2025-05-29 16:26:19 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Generate SmartProxy routes for DNS configuration
|
|
|
|
*/
|
|
|
|
private generateDnsRoutes(): plugins.smartproxy.IRouteConfig[] {
|
|
|
|
if (!this.options.dnsDomain) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const dnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
|
|
|
|
|
|
|
|
// Create routes for DNS-over-HTTPS paths
|
|
|
|
const dohPaths = ['/dns-query', '/resolve'];
|
|
|
|
|
|
|
|
for (const path of dohPaths) {
|
|
|
|
const dohRoute: plugins.smartproxy.IRouteConfig = {
|
|
|
|
name: `dns-over-https-${path.replace('/', '')}`,
|
|
|
|
match: {
|
|
|
|
ports: [443], // HTTPS port for DoH
|
|
|
|
domains: [this.options.dnsDomain],
|
|
|
|
path: path
|
|
|
|
},
|
|
|
|
action: {
|
|
|
|
type: 'socket-handler' as any,
|
|
|
|
socketHandler: this.createDnsSocketHandler()
|
|
|
|
} as any
|
|
|
|
};
|
|
|
|
|
|
|
|
dnsRoutes.push(dohRoute);
|
|
|
|
}
|
|
|
|
|
|
|
|
return dnsRoutes;
|
|
|
|
}
|
2025-05-16 15:50:46 +00:00
|
|
|
|
2025-05-07 22:06:55 +00:00
|
|
|
/**
|
2025-05-07 23:04:54 +00:00
|
|
|
* 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
|
2025-05-07 22:06:55 +00:00
|
|
|
*/
|
2025-05-07 23:04:54 +00:00
|
|
|
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;
|
|
|
|
}
|
2025-05-07 22:06:55 +00:00
|
|
|
|
2025-05-07 23:04:54 +00:00
|
|
|
public async stop() {
|
|
|
|
console.log('Stopping DcRouter services...');
|
|
|
|
|
2025-05-07 22:06:55 +00:00
|
|
|
try {
|
2025-05-07 23:04:54 +00:00
|
|
|
// Stop all services in parallel for faster shutdown
|
|
|
|
await Promise.all([
|
2025-05-27 14:06:22 +00:00
|
|
|
// Stop unified email server if running
|
|
|
|
this.emailServer ? this.emailServer.stop().catch(err => console.error('Error stopping email server:', err)) : Promise.resolve(),
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-07 23:45:19 +00:00
|
|
|
// Stop HTTP SmartProxy if running
|
|
|
|
this.smartProxy ? this.smartProxy.stop().catch(err => console.error('Error stopping SmartProxy:', err)) : Promise.resolve(),
|
2025-05-07 23:04:54 +00:00
|
|
|
|
|
|
|
// 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');
|
2025-05-07 22:06:55 +00:00
|
|
|
} catch (error) {
|
2025-05-07 23:04:54 +00:00
|
|
|
console.error('Error during DcRouter shutdown:', error);
|
|
|
|
throw error;
|
2025-05-07 22:06:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-07 23:04:54 +00:00
|
|
|
/**
|
2025-05-07 23:45:19 +00:00
|
|
|
* Update SmartProxy configuration
|
|
|
|
* @param config New SmartProxy configuration
|
2025-05-07 23:04:54 +00:00
|
|
|
*/
|
2025-05-07 23:45:19 +00:00
|
|
|
public async updateSmartProxyConfig(config: plugins.smartproxy.ISmartProxyOptions): Promise<void> {
|
|
|
|
// Stop existing SmartProxy if running
|
2025-05-04 10:10:07 +00:00
|
|
|
if (this.smartProxy) {
|
|
|
|
await this.smartProxy.stop();
|
2025-05-07 23:04:54 +00:00
|
|
|
this.smartProxy = undefined;
|
2025-05-04 10:10:07 +00:00
|
|
|
}
|
2025-05-07 22:06:55 +00:00
|
|
|
|
2025-05-07 23:45:19 +00:00
|
|
|
// Update configuration
|
|
|
|
this.options.smartProxyConfig = config;
|
|
|
|
|
2025-05-16 15:50:46 +00:00
|
|
|
// Start new SmartProxy with updated configuration (will include email routes if configured)
|
2025-05-07 23:45:19 +00:00
|
|
|
await this.setupSmartProxy();
|
|
|
|
|
|
|
|
console.log('SmartProxy configuration updated');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2025-05-08 00:12:36 +00:00
|
|
|
|
2025-05-07 23:45:19 +00:00
|
|
|
/**
|
2025-05-08 00:12:36 +00:00
|
|
|
* Set up unified email handling with pattern-based routing
|
|
|
|
* This implements the consolidated emailConfig approach
|
2025-05-07 23:45:19 +00:00
|
|
|
*/
|
2025-05-08 00:12:36 +00:00
|
|
|
private async setupUnifiedEmailHandling(): Promise<void> {
|
|
|
|
if (!this.options.emailConfig) {
|
|
|
|
throw new Error('Email configuration is required for unified email handling');
|
|
|
|
}
|
2025-05-16 15:50:46 +00:00
|
|
|
|
2025-05-27 19:28:12 +00:00
|
|
|
// Apply port mapping if behind SmartProxy
|
2025-05-27 18:00:14 +00:00
|
|
|
const portMapping = this.options.emailPortConfig?.portMapping || {
|
2025-05-16 15:50:46 +00:00
|
|
|
25: 10025, // SMTP
|
|
|
|
587: 10587, // Submission
|
|
|
|
465: 10465 // SMTPS
|
|
|
|
};
|
|
|
|
|
2025-05-27 19:28:12 +00:00
|
|
|
// Create config with mapped ports
|
|
|
|
const emailConfig: IUnifiedEmailServerOptions = {
|
|
|
|
...this.options.emailConfig,
|
|
|
|
ports: this.options.emailConfig.ports.map(port => portMapping[port] || port + 10000),
|
2025-05-16 15:50:46 +00:00
|
|
|
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
|
2025-05-27 19:28:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Create unified email server
|
|
|
|
this.emailServer = new UnifiedEmailServer(this, emailConfig);
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-27 18:00:14 +00:00
|
|
|
// Set up error handling
|
|
|
|
this.emailServer.on('error', (err: Error) => {
|
|
|
|
logger.log('error', `UnifiedEmailServer error: ${err.message}`);
|
|
|
|
});
|
2025-05-20 11:04:09 +00:00
|
|
|
|
2025-05-27 18:00:14 +00:00
|
|
|
// Start the server
|
|
|
|
await this.emailServer.start();
|
2025-05-20 11:04:09 +00:00
|
|
|
|
2025-05-27 19:28:12 +00:00
|
|
|
logger.log('info', `Email server started on ports: ${emailConfig.ports.join(', ')}`);
|
2025-05-07 14:33:20 +00:00
|
|
|
}
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-07 14:33:20 +00:00
|
|
|
/**
|
2025-05-08 00:12:36 +00:00
|
|
|
* Update the unified email configuration
|
|
|
|
* @param config New email configuration
|
2025-05-07 14:33:20 +00:00
|
|
|
*/
|
2025-05-27 19:28:12 +00:00
|
|
|
public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise<void> {
|
2025-05-08 00:12:36 +00:00
|
|
|
// Stop existing email components
|
|
|
|
await this.stopUnifiedEmailComponents();
|
2025-05-07 23:04:54 +00:00
|
|
|
|
|
|
|
// Update configuration
|
2025-05-08 00:12:36 +00:00
|
|
|
this.options.emailConfig = config;
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-08 00:12:36 +00:00
|
|
|
// Start email handling with new configuration
|
|
|
|
await this.setupUnifiedEmailHandling();
|
2025-05-07 23:04:54 +00:00
|
|
|
|
2025-05-08 00:12:36 +00:00
|
|
|
console.log('Unified email configuration updated');
|
2025-05-04 10:10:07 +00:00
|
|
|
}
|
2025-05-07 23:45:19 +00:00
|
|
|
|
|
|
|
/**
|
2025-05-08 00:12:36 +00:00
|
|
|
* Stop all unified email components
|
2025-05-07 23:45:19 +00:00
|
|
|
*/
|
2025-05-08 00:12:36 +00:00
|
|
|
private async stopUnifiedEmailComponents(): Promise<void> {
|
2025-05-08 00:39:43 +00:00
|
|
|
try {
|
2025-05-27 14:06:22 +00:00
|
|
|
// Stop the unified email server which contains all components
|
|
|
|
if (this.emailServer) {
|
|
|
|
await this.emailServer.stop();
|
2025-05-08 00:39:43 +00:00
|
|
|
logger.log('info', 'Unified email server stopped');
|
2025-05-27 14:06:22 +00:00
|
|
|
this.emailServer = undefined;
|
2025-05-08 00:39:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
logger.log('info', 'All unified email components stopped');
|
|
|
|
} catch (error) {
|
|
|
|
logger.log('error', `Error stopping unified email components: ${error.message}`);
|
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update domain rules for email routing
|
|
|
|
* @param rules New domain rules to apply
|
|
|
|
*/
|
2025-05-28 13:23:45 +00:00
|
|
|
public async updateEmailRoutes(routes: IEmailRoute[]): Promise<void> {
|
2025-05-08 00:39:43 +00:00
|
|
|
// Validate that email config exists
|
|
|
|
if (!this.options.emailConfig) {
|
2025-05-28 13:23:45 +00:00
|
|
|
throw new Error('Email configuration is required before updating routes');
|
2025-05-08 00:39:43 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update the configuration
|
2025-05-28 13:23:45 +00:00
|
|
|
this.options.emailConfig.routes = routes;
|
2025-05-08 00:39:43 +00:00
|
|
|
|
|
|
|
// Update the unified email server if it exists
|
2025-05-27 14:06:22 +00:00
|
|
|
if (this.emailServer) {
|
2025-05-28 13:23:45 +00:00
|
|
|
this.emailServer.updateRoutes(routes);
|
2025-05-08 00:39:43 +00:00
|
|
|
}
|
2025-05-07 23:45:19 +00:00
|
|
|
|
2025-05-28 13:23:45 +00:00
|
|
|
console.log(`Email routes updated with ${routes.length} routes`);
|
2025-05-07 23:45:19 +00:00
|
|
|
}
|
|
|
|
|
2025-05-08 00:39:43 +00:00
|
|
|
/**
|
|
|
|
* Get statistics from all components
|
|
|
|
*/
|
|
|
|
public getStats(): any {
|
|
|
|
const stats: any = {
|
2025-05-27 14:06:22 +00:00
|
|
|
emailServer: this.emailServer?.getStats()
|
2025-05-08 00:39:43 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return stats;
|
|
|
|
}
|
2025-05-20 19:46:59 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Configure MTA for email handling with custom port and storage settings
|
|
|
|
* @param config Configuration for the MTA service
|
|
|
|
*/
|
|
|
|
public async configureEmailMta(config: {
|
|
|
|
internalPort: number;
|
|
|
|
host?: string;
|
|
|
|
secure?: boolean;
|
|
|
|
storagePath?: string;
|
|
|
|
portMapping?: Record<number, number>;
|
|
|
|
}): Promise<boolean> {
|
|
|
|
logger.log('info', 'Configuring MTA service with custom settings');
|
|
|
|
|
|
|
|
|
|
|
|
// Update email port configuration
|
|
|
|
if (!this.options.emailPortConfig) {
|
|
|
|
this.options.emailPortConfig = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
// Configure storage paths for received emails
|
|
|
|
if (config.storagePath) {
|
|
|
|
// Set the storage path for received emails
|
|
|
|
this.options.emailPortConfig.receivedEmailsPath = config.storagePath;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply port mapping if provided
|
|
|
|
if (config.portMapping) {
|
|
|
|
this.options.emailPortConfig.portMapping = {
|
|
|
|
...this.options.emailPortConfig.portMapping,
|
|
|
|
...config.portMapping
|
|
|
|
};
|
|
|
|
|
|
|
|
logger.log('info', `Updated MTA port mappings: ${JSON.stringify(this.options.emailPortConfig.portMapping)}`);
|
|
|
|
}
|
|
|
|
|
2025-05-21 00:12:49 +00:00
|
|
|
// Use the dedicated helper to configure the email server
|
|
|
|
// Pass through the options specified by the implementation
|
2025-05-27 14:06:22 +00:00
|
|
|
if (this.emailServer) {
|
|
|
|
configureEmailServer(this.emailServer, {
|
2025-05-21 00:12:49 +00:00
|
|
|
ports: [config.internalPort], // Use whatever port the implementation specifies
|
|
|
|
hostname: config.host,
|
|
|
|
tls: config.secure ? {
|
|
|
|
// Basic TLS settings if secure mode is enabled
|
|
|
|
certPath: this.options.tls?.certPath,
|
|
|
|
keyPath: this.options.tls?.keyPath,
|
|
|
|
caPath: this.options.tls?.caPath
|
|
|
|
} : undefined,
|
|
|
|
storagePath: config.storagePath
|
|
|
|
});
|
|
|
|
}
|
2025-05-20 19:46:59 +00:00
|
|
|
|
|
|
|
// If email handling is already set up, restart it to apply changes
|
2025-05-27 14:06:22 +00:00
|
|
|
if (this.emailServer) {
|
2025-05-20 19:46:59 +00:00
|
|
|
logger.log('info', 'Restarting unified email handling to apply MTA configuration changes');
|
|
|
|
await this.stopUnifiedEmailComponents();
|
|
|
|
await this.setupUnifiedEmailHandling();
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2025-05-28 18:07:07 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Register DNS records with the DNS server
|
|
|
|
* @param records Array of DNS records to register
|
|
|
|
*/
|
|
|
|
private registerDnsRecords(records: Array<{name: string; type: string; value: string; ttl?: number}>): void {
|
|
|
|
if (!this.dnsServer) return;
|
|
|
|
|
|
|
|
// Group records by domain pattern
|
|
|
|
const recordsByDomain = new Map<string, typeof records>();
|
|
|
|
|
|
|
|
for (const record of records) {
|
|
|
|
const pattern = record.name.includes('*') ? record.name : `*.${record.name}`;
|
|
|
|
if (!recordsByDomain.has(pattern)) {
|
|
|
|
recordsByDomain.set(pattern, []);
|
|
|
|
}
|
|
|
|
recordsByDomain.get(pattern)!.push(record);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Register handlers for each domain pattern
|
|
|
|
for (const [domainPattern, domainRecords] of recordsByDomain) {
|
|
|
|
const recordTypes = [...new Set(domainRecords.map(r => r.type))];
|
|
|
|
|
|
|
|
this.dnsServer.registerHandler(domainPattern, recordTypes, (question) => {
|
|
|
|
const matchingRecord = domainRecords.find(
|
|
|
|
r => r.name === question.name && r.type === question.type
|
|
|
|
);
|
|
|
|
|
|
|
|
if (matchingRecord) {
|
|
|
|
return {
|
|
|
|
name: matchingRecord.name,
|
|
|
|
type: matchingRecord.type,
|
|
|
|
class: 'IN',
|
|
|
|
ttl: matchingRecord.ttl || 300,
|
|
|
|
data: this.parseDnsRecordData(matchingRecord.type, matchingRecord.value)
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse DNS record data based on record type
|
|
|
|
* @param type DNS record type
|
|
|
|
* @param value DNS record value
|
|
|
|
* @returns Parsed data for the DNS response
|
|
|
|
*/
|
|
|
|
private parseDnsRecordData(type: string, value: string): any {
|
|
|
|
switch (type) {
|
|
|
|
case 'A':
|
|
|
|
return value; // IP address as string
|
|
|
|
case 'MX':
|
|
|
|
const [priority, exchange] = value.split(' ');
|
|
|
|
return { priority: parseInt(priority), exchange };
|
|
|
|
case 'TXT':
|
|
|
|
return value;
|
|
|
|
case 'NS':
|
|
|
|
return value;
|
|
|
|
default:
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
}
|
2025-05-29 16:26:19 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Set up DNS server with socket handler for DoH
|
|
|
|
*/
|
|
|
|
private async setupDnsWithSocketHandler(): Promise<void> {
|
|
|
|
if (!this.options.dnsDomain) {
|
|
|
|
throw new Error('dnsDomain is required for DNS socket handler setup');
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.log('info', `Setting up DNS server with socket handler for domain: ${this.options.dnsDomain}`);
|
|
|
|
|
|
|
|
// Get VM IP address for UDP binding
|
|
|
|
const networkInterfaces = plugins.os.networkInterfaces();
|
|
|
|
let vmIpAddress = '0.0.0.0'; // Default to all interfaces
|
|
|
|
|
|
|
|
// Try to find the VM's internal IP address
|
|
|
|
for (const [name, interfaces] of Object.entries(networkInterfaces)) {
|
|
|
|
if (interfaces) {
|
|
|
|
for (const iface of interfaces) {
|
|
|
|
if (!iface.internal && iface.family === 'IPv4') {
|
|
|
|
vmIpAddress = iface.address;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create DNS server instance with manual HTTPS mode
|
|
|
|
this.dnsServer = new plugins.smartdns.dnsServerMod.DnsServer({
|
|
|
|
udpPort: 53,
|
|
|
|
udpBindInterface: vmIpAddress,
|
|
|
|
httpsPort: 443, // Required but won't bind due to manual mode
|
|
|
|
manualHttpsMode: true, // Enable manual HTTPS socket handling
|
|
|
|
dnssecZone: this.options.dnsDomain,
|
|
|
|
// For now, use self-signed cert until we integrate with Let's Encrypt
|
|
|
|
httpsKey: '',
|
|
|
|
httpsCert: ''
|
|
|
|
});
|
|
|
|
|
|
|
|
// Start the DNS server (UDP only)
|
|
|
|
await this.dnsServer.start();
|
|
|
|
logger.log('info', `DNS server started on UDP ${vmIpAddress}:53`);
|
2025-05-30 10:34:50 +00:00
|
|
|
|
|
|
|
// Register DNS records if provided
|
|
|
|
if (this.options.dnsRecords && this.options.dnsRecords.length > 0) {
|
|
|
|
this.registerDnsRecords(this.options.dnsRecords);
|
|
|
|
logger.log('info', `Registered ${this.options.dnsRecords.length} DNS records`);
|
|
|
|
}
|
2025-05-29 16:26:19 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create DNS socket handler for DoH
|
|
|
|
*/
|
|
|
|
private createDnsSocketHandler(): (socket: plugins.net.Socket) => Promise<void> {
|
|
|
|
return async (socket: plugins.net.Socket) => {
|
|
|
|
if (!this.dnsServer) {
|
|
|
|
logger.log('error', 'DNS socket handler called but DNS server not initialized');
|
|
|
|
socket.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.log('debug', 'DNS socket handler: passing socket to DnsServer');
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Use the built-in socket handler from smartdns
|
|
|
|
// This handles HTTP/2, DoH protocol, etc.
|
|
|
|
await (this.dnsServer as any).handleHttpsSocket(socket);
|
|
|
|
} catch (error) {
|
|
|
|
logger.log('error', `DNS socket handler error: ${error.message}`);
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create mail socket handler for email traffic
|
|
|
|
*/
|
|
|
|
private createMailSocketHandler(port: number): (socket: plugins.net.Socket) => Promise<void> {
|
|
|
|
return async (socket: plugins.net.Socket) => {
|
|
|
|
if (!this.emailServer) {
|
|
|
|
logger.log('error', 'Mail socket handler called but email server not initialized');
|
|
|
|
socket.end();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.log('debug', `Mail socket handler: handling connection for port ${port}`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
// Port 465 requires immediate TLS
|
|
|
|
if (port === 465) {
|
|
|
|
// Wrap the socket in TLS
|
|
|
|
const tlsOptions = {
|
|
|
|
isServer: true,
|
|
|
|
key: this.options.tls?.keyPath ? plugins.fs.readFileSync(this.options.tls.keyPath, 'utf8') : undefined,
|
|
|
|
cert: this.options.tls?.certPath ? plugins.fs.readFileSync(this.options.tls.certPath, 'utf8') : undefined
|
|
|
|
};
|
|
|
|
|
|
|
|
const tlsSocket = new plugins.tls.TLSSocket(socket, tlsOptions);
|
|
|
|
|
|
|
|
tlsSocket.on('secure', () => {
|
|
|
|
// Pass the secure socket to the email server
|
|
|
|
this.emailServer!.handleSocket(tlsSocket, port);
|
|
|
|
});
|
|
|
|
|
|
|
|
tlsSocket.on('error', (err) => {
|
|
|
|
logger.log('error', `TLS handshake error on port ${port}: ${err.message}`);
|
|
|
|
socket.destroy();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// For ports 25 and 587, pass raw socket (STARTTLS handled by email server)
|
|
|
|
await this.emailServer.handleSocket(socket, port);
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.log('error', `Mail socket handler error on port ${port}: ${error.message}`);
|
|
|
|
socket.destroy();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2025-05-04 10:10:07 +00:00
|
|
|
}
|
|
|
|
|
2025-05-27 19:28:12 +00:00
|
|
|
// Re-export email server types for convenience
|
2025-05-28 14:12:50 +00:00
|
|
|
export type { IUnifiedEmailServerOptions };
|
2025-05-24 01:00:30 +00:00
|
|
|
|
2025-05-04 10:10:07 +00:00
|
|
|
export default DcRouter;
|