2026-02-11 07:55:28 +00:00
|
|
|
import { logger } from '../../logger.js';
|
|
|
|
|
import {
|
|
|
|
|
SecurityLogger,
|
|
|
|
|
SecurityLogLevel,
|
|
|
|
|
SecurityEventType
|
|
|
|
|
} from '../../security/index.js';
|
|
|
|
|
import type { IEmailAction, IEmailContext } from './interfaces.js';
|
|
|
|
|
import { Email } from '../core/classes.email.js';
|
|
|
|
|
import { BounceManager } from '../core/classes.bouncemanager.js';
|
|
|
|
|
import { UnifiedDeliveryQueue } from '../delivery/classes.delivery.queue.js';
|
|
|
|
|
import type { ISmtpSendResult } from '../../security/classes.rustsecuritybridge.js';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Dependencies injected from UnifiedEmailServer to avoid circular imports
|
|
|
|
|
*/
|
|
|
|
|
export interface IActionExecutorDeps {
|
|
|
|
|
sendOutboundEmail: (host: string, port: number, email: Email, options?: {
|
|
|
|
|
auth?: { user: string; pass: string };
|
|
|
|
|
dkimDomain?: string;
|
|
|
|
|
dkimSelector?: string;
|
2026-02-11 10:11:43 +00:00
|
|
|
tlsOpportunistic?: boolean;
|
2026-02-11 07:55:28 +00:00
|
|
|
}) => Promise<ISmtpSendResult>;
|
|
|
|
|
bounceManager: BounceManager;
|
|
|
|
|
deliveryQueue: UnifiedDeliveryQueue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Executes email routing actions (forward, process, deliver, reject)
|
|
|
|
|
*/
|
|
|
|
|
export class EmailActionExecutor {
|
|
|
|
|
constructor(private deps: IActionExecutorDeps) {}
|
|
|
|
|
|
|
|
|
|
async executeAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
|
|
|
|
|
switch (action.type) {
|
|
|
|
|
case 'forward':
|
|
|
|
|
await this.handleForwardAction(action, email, context);
|
|
|
|
|
break;
|
|
|
|
|
case 'process':
|
|
|
|
|
await this.handleProcessAction(action, email, context);
|
|
|
|
|
break;
|
|
|
|
|
case 'deliver':
|
|
|
|
|
await this.handleDeliverAction(action, email, context);
|
|
|
|
|
break;
|
|
|
|
|
case 'reject':
|
|
|
|
|
await this.handleRejectAction(action, email, context);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new Error(`Unknown action type: ${(action as any).type}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleForwardAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
|
|
|
|
|
if (!action.forward) {
|
|
|
|
|
throw new Error('Forward action requires forward configuration');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { host, port = 25, auth, addHeaders } = action.forward;
|
|
|
|
|
|
|
|
|
|
logger.log('info', `Forwarding email to ${host}:${port}`);
|
|
|
|
|
|
|
|
|
|
// Add forwarding headers
|
|
|
|
|
if (addHeaders) {
|
|
|
|
|
for (const [key, value] of Object.entries(addHeaders)) {
|
|
|
|
|
email.headers[key] = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add standard forwarding headers
|
|
|
|
|
email.headers['X-Forwarded-For'] = context.session.remoteAddress || 'unknown';
|
|
|
|
|
email.headers['X-Forwarded-To'] = email.to.join(', ');
|
|
|
|
|
email.headers['X-Forwarded-Date'] = new Date().toISOString();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Send email via Rust SMTP client
|
|
|
|
|
await this.deps.sendOutboundEmail(host, port, email, {
|
|
|
|
|
auth: auth as { user: string; pass: string } | undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
logger.log('info', `Successfully forwarded email to ${host}:${port}`);
|
|
|
|
|
|
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
|
|
|
level: SecurityLogLevel.INFO,
|
|
|
|
|
type: SecurityEventType.EMAIL_FORWARDING,
|
|
|
|
|
message: 'Email forwarded successfully',
|
|
|
|
|
ipAddress: context.session.remoteAddress,
|
|
|
|
|
details: {
|
|
|
|
|
sessionId: context.session.id,
|
|
|
|
|
routeName: context.session.matchedRoute?.name,
|
|
|
|
|
targetHost: host,
|
|
|
|
|
targetPort: port,
|
|
|
|
|
recipients: email.to
|
|
|
|
|
},
|
|
|
|
|
success: true
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logger.log('error', `Failed to forward email: ${error.message}`);
|
|
|
|
|
|
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
|
|
|
level: SecurityLogLevel.ERROR,
|
|
|
|
|
type: SecurityEventType.EMAIL_FORWARDING,
|
|
|
|
|
message: 'Email forwarding failed',
|
|
|
|
|
ipAddress: context.session.remoteAddress,
|
|
|
|
|
details: {
|
|
|
|
|
sessionId: context.session.id,
|
|
|
|
|
routeName: context.session.matchedRoute?.name,
|
|
|
|
|
targetHost: host,
|
|
|
|
|
targetPort: port,
|
|
|
|
|
error: error.message
|
|
|
|
|
},
|
|
|
|
|
success: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle as bounce
|
|
|
|
|
for (const recipient of email.getAllRecipients()) {
|
|
|
|
|
await this.deps.bounceManager.processSmtpFailure(recipient, error.message, {
|
|
|
|
|
sender: email.from,
|
|
|
|
|
originalEmailId: email.headers['Message-ID'] as string
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleProcessAction(action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
|
|
|
|
|
logger.log('info', `Processing email with action options`);
|
|
|
|
|
|
|
|
|
|
// Apply scanning if requested
|
|
|
|
|
if (action.process?.scan) {
|
|
|
|
|
logger.log('info', 'Content scanning requested');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Queue for delivery
|
|
|
|
|
const queue = action.process?.queue || 'normal';
|
|
|
|
|
await this.deps.deliveryQueue.enqueue(email, 'process', context.session.matchedRoute!);
|
|
|
|
|
|
|
|
|
|
logger.log('info', `Email queued for delivery in ${queue} queue`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleDeliverAction(_action: IEmailAction, email: Email, context: IEmailContext): Promise<void> {
|
|
|
|
|
logger.log('info', `Delivering email locally`);
|
|
|
|
|
|
|
|
|
|
// Queue for local delivery
|
|
|
|
|
await this.deps.deliveryQueue.enqueue(email, 'mta', context.session.matchedRoute!);
|
|
|
|
|
|
|
|
|
|
logger.log('info', 'Email queued for local delivery');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async handleRejectAction(action: IEmailAction, _email: Email, context: IEmailContext): Promise<void> {
|
|
|
|
|
const code = action.reject?.code || 550;
|
|
|
|
|
const message = action.reject?.message || 'Message rejected';
|
|
|
|
|
|
|
|
|
|
logger.log('info', `Rejecting email with code ${code}: ${message}`);
|
|
|
|
|
|
|
|
|
|
SecurityLogger.getInstance().logEvent({
|
|
|
|
|
level: SecurityLogLevel.WARN,
|
|
|
|
|
type: SecurityEventType.EMAIL_PROCESSING,
|
|
|
|
|
message: 'Email rejected by routing rule',
|
|
|
|
|
ipAddress: context.session.remoteAddress,
|
|
|
|
|
details: {
|
|
|
|
|
sessionId: context.session.id,
|
|
|
|
|
routeName: context.session.matchedRoute?.name,
|
|
|
|
|
rejectCode: code,
|
|
|
|
|
rejectMessage: message,
|
|
|
|
|
from: _email.from,
|
|
|
|
|
to: _email.to
|
|
|
|
|
},
|
|
|
|
|
success: false
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Throw error with SMTP code and message
|
|
|
|
|
const error = new Error(message);
|
|
|
|
|
(error as any).responseCode = code;
|
|
|
|
|
throw error;
|
|
|
|
|
}
|
|
|
|
|
}
|