This commit is contained in:
2025-05-20 11:04:09 +00:00
parent 07f03eb834
commit f3f06ed06d
4 changed files with 418 additions and 54 deletions

View File

@ -11,6 +11,7 @@ import { UnifiedDeliveryQueue, type IQueueOptions } from './mail/delivery/classe
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from './mail/delivery/classes.delivery.system.js';
import { UnifiedRateLimiter, type IHierarchicalRateLimits } from './mail/delivery/classes.unified.rate.limiter.js';
import { logger } from './logger.js';
import { applyCustomEmailStorage } from './mail/delivery/classes.mta.patch.js';
export interface IDcRouterOptions {
/**
@ -25,6 +26,20 @@ export interface IDcRouterOptions {
*/
emailConfig?: IEmailConfig;
/**
* 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;
};
/** TLS/certificate configuration */
tls?: {
/** Contact email for ACME certificates */
@ -76,14 +91,20 @@ export class DcRouter {
public deliverySystem?: MultiModeDeliverySystem;
public rateLimiter?: UnifiedRateLimiter;
// Reference to the platform service for accessing MTA and other services
public platformServiceRef?: any;
// Environment access
private qenv = new plugins.qenv.Qenv('./', '.nogit/');
constructor(optionsArg: IDcRouterOptions) {
constructor(optionsArg: IDcRouterOptions, platformServiceRef?: any) {
// Set defaults in options
this.options = {
...optionsArg
};
// Store reference to platform service if provided
this.platformServiceRef = platformServiceRef;
}
public async start() {
@ -96,6 +117,12 @@ export class DcRouter {
// Set up unified email handling if configured
if (this.options.emailConfig) {
await this.setupUnifiedEmailHandling();
// Apply custom email storage configuration if available
if (this.platformServiceRef && this.options.emailPortConfig?.receivedEmailsPath) {
logger.log('info', 'Applying custom email storage configuration');
applyCustomEmailStorage(this.platformServiceRef, this.options);
}
}
// Set up DNS server if configured
@ -222,69 +249,90 @@ export class DcRouter {
private generateEmailRoutes(emailConfig: IEmailConfig): plugins.smartproxy.IRouteConfig[] {
const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
// Get the custom port mapping if available, otherwise use defaults
const defaultPortMapping = {
25: 10025, // SMTP
587: 10587, // Submission
465: 10465 // SMTPS
};
// Use custom port mapping if provided, otherwise fall back to defaults
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
// Create routes for each email port
for (const port of emailConfig.ports) {
// Calculate the internal port using the mapping
const internalPort = portMapping[port] || port + 10000;
// Create a descriptive name for the route based on the port
let routeName = 'email-route';
let tlsMode = 'passthrough';
// Handle different email ports differently
switch (port) {
case 25: // SMTP
emailRoutes.push({
name: 'smtp-route',
match: {
ports: [25]
},
action: {
type: 'forward',
target: {
host: 'localhost', // Forward to internal email server
port: 10025 // Internal email server port
},
// No TLS termination for port 25 (STARTTLS handled by email server)
tls: {
mode: 'passthrough'
}
}
});
routeName = 'smtp-route';
tlsMode = 'passthrough'; // STARTTLS handled by email server
break;
case 587: // Submission
emailRoutes.push({
name: 'submission-route',
match: {
ports: [587]
},
action: {
type: 'forward',
target: {
host: 'localhost',
port: 10587
},
tls: {
mode: 'passthrough' // STARTTLS handled by email server
}
}
});
routeName = 'submission-route';
tlsMode = 'passthrough'; // STARTTLS handled by email server
break;
case 465: // SMTPS
emailRoutes.push({
name: 'smtps-route',
match: {
ports: [465]
},
action: {
type: 'forward',
target: {
host: 'localhost',
port: 10465
},
tls: {
mode: 'terminate', // Terminate TLS and re-encrypt to email server
certificate: 'auto'
}
routeName = 'smtps-route';
tlsMode = 'terminate'; // Terminate TLS and re-encrypt to email server
break;
default:
routeName = `email-port-${port}-route`;
// For unknown ports, assume passthrough by default
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';
}
});
// Override the route name if specified
if (portSettings.routeName) {
routeName = portSettings.routeName;
}
}
break;
}
// Create the route configuration
const routeConfig: plugins.smartproxy.IRouteConfig = {
name: routeName,
match: {
ports: [port]
},
action: {
type: 'forward',
target: {
host: 'localhost', // Forward to internal email server
port: internalPort
},
tls: {
mode: tlsMode as any
}
}
};
// For TLS terminate mode, add certificate info
if (tlsMode === 'terminate') {
routeConfig.action.tls.certificate = 'auto';
}
// Add the route to our list
emailRoutes.push(routeConfig);
}
// Add domain-specific email routes if configured
@ -406,13 +454,16 @@ export class DcRouter {
const emailConfig = this.options.emailConfig;
// Map external ports to internal ports
const portMapping = {
// Map external ports to internal ports with support for custom port mapping
const defaultPortMapping = {
25: 10025, // SMTP
587: 10587, // Submission
465: 10465 // SMTPS
};
// Use custom port mapping if provided, otherwise fall back to defaults
const portMapping = this.options.emailPortConfig?.portMapping || defaultPortMapping;
// Create internal email server configuration
const internalEmailConfig: IEmailConfig = {
...emailConfig,
@ -420,6 +471,17 @@ export class DcRouter {
hostname: 'localhost' // Listen on localhost for SmartProxy forwarding
};
// If custom MTA options are provided, merge them
if (this.options.emailPortConfig?.portSettings) {
// Will be used in MTA configuration
logger.log('info', 'Custom port settings detected for email configuration');
}
// Configure custom email storage path if specified
if (this.options.emailPortConfig?.receivedEmailsPath) {
logger.log('info', `Custom email storage path configured: ${this.options.emailPortConfig.receivedEmailsPath}`);
}
try {
// Create domain router for pattern matching
this.domainRouter = new DomainRouter({

View File

@ -80,7 +80,7 @@ export class SmtpPortConfig {
/** Default port configurations */
private static readonly DEFAULT_CONFIGS: Record<number, Partial<ISmtpPortSettings>> = {
// Port 25: Standard SMTP
// Port 25: Standard SMTP (modified from port 25)
25: {
description: 'Standard SMTP',
requireAuth: false,
@ -258,7 +258,7 @@ export class SmtpPortConfig {
type: 'forward',
target: {
host: 'localhost',
port: portConfig.port + 10000 // Map to internal port (e.g., 25 -> 10025)
port: portConfig.port === 29200 ? 39200 : portConfig.port + 10000 // Map to internal port (29200 -> 39200, 587 -> 10587, etc.)
}
}
};

View File

@ -0,0 +1,72 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import type { SzPlatformService } from '../../platformservice.js';
/**
* Extension to the MTA service to handle custom email storage
*/
export function configureEmailStorage(mtaService: any, options: any = {}) {
const originalSaveToLocalMailbox = mtaService.saveToLocalMailbox;
// Override the saveToLocalMailbox method to use custom path
mtaService.saveToLocalMailbox = async function(email: any): Promise<void> {
try {
// Use the custom received emails path if configured
const customPath = options?.receivedEmailsPath;
if (customPath) {
// Ensure the directory exists
plugins.smartfile.fs.ensureDirSync(customPath);
// Check if this is a bounce notification
const isBounceNotification = this.isBounceNotification(email);
if (isBounceNotification) {
await this.processBounceNotification(email);
return;
}
// Store the email in the custom path
const emailContent = email.toRFC822String();
const filename = `${Date.now()}_${email.to[0].replace(/[^a-zA-Z0-9]/g, '_')}.eml`;
plugins.smartfile.memory.toFsSync(
emailContent,
plugins.path.join(customPath, filename)
);
console.log(`Email saved to custom mailbox location: ${customPath}/${filename}`);
} else {
// Use the original implementation
await originalSaveToLocalMailbox.call(this, email);
}
} catch (error) {
console.error('Error saving email to local mailbox:', error);
throw error;
}
};
return mtaService;
}
/**
* Apply the email storage configuration to the platform service
*/
export function applyCustomEmailStorage(platformService: SzPlatformService, options: any): void {
// Extract the receivedEmailsPath if available
if (options?.emailPortConfig?.receivedEmailsPath) {
const receivedEmailsPath = options.emailPortConfig.receivedEmailsPath;
// Ensure the directory exists
plugins.smartfile.fs.ensureDirSync(receivedEmailsPath);
// Apply configuration to MTA service if available
if (platformService.mtaService) {
configureEmailStorage(platformService.mtaService, {
receivedEmailsPath
});
console.log(`Configured MTA to store received emails to: ${receivedEmailsPath}`);
}
}
}