BREAKING CHANGE(mail): remove DMARC and DKIM verifier implementations and MTA error classes; introduce DkimManager and EmailActionExecutor; simplify SPF verifier and update routing exports and tests
This commit is contained in:
174
ts/mail/routing/classes.email.action.executor.ts
Normal file
174
ts/mail/routing/classes.email.action.executor.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
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;
|
||||
}) => 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user