feat(mailer-smtp): add SCRAM-SHA-256 auth, Ed25519 DKIM, opportunistic TLS, SNI cert selection, pipelining and delivery/bridge improvements
This commit is contained in:
@@ -7,10 +7,12 @@ import {
|
||||
SecurityEventType
|
||||
} from '../../security/index.js';
|
||||
import { UnifiedDeliveryQueue, type IQueueItem } from './classes.delivery.queue.js';
|
||||
import type { Email } from '../core/classes.email.js';
|
||||
import { Email } from '../core/classes.email.js';
|
||||
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
|
||||
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
|
||||
|
||||
const dns = plugins.dns;
|
||||
|
||||
/**
|
||||
* Delivery status enumeration
|
||||
*/
|
||||
@@ -480,39 +482,119 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve MX records for a domain, sorted by priority (lowest first).
|
||||
* Falls back to the domain itself as an A record per RFC 5321.
|
||||
*/
|
||||
private async resolveMxForDomain(domain: string): Promise<Array<{ exchange: string; priority: number }>> {
|
||||
const resolver = new dns.promises.Resolver();
|
||||
try {
|
||||
const mxRecords = await resolver.resolveMx(domain);
|
||||
return mxRecords.sort((a, b) => a.priority - b.priority);
|
||||
} catch (err) {
|
||||
logger.log('warn', `No MX records for ${domain}, falling back to A record`);
|
||||
return [{ exchange: domain, priority: 0 }];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group recipient addresses by their domain part.
|
||||
*/
|
||||
private groupRecipientsByDomain(recipients: string[]): Map<string, string[]> {
|
||||
const groups = new Map<string, string[]>();
|
||||
for (const rcpt of recipients) {
|
||||
const domain = rcpt.split('@')[1]?.toLowerCase();
|
||||
if (!domain) continue;
|
||||
const list = groups.get(domain) || [];
|
||||
list.push(rcpt);
|
||||
groups.set(domain, list);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default handler for MTA mode delivery
|
||||
* @param item Queue item
|
||||
*/
|
||||
private async handleMtaDelivery(item: IQueueItem): Promise<any> {
|
||||
logger.log('info', `MTA delivery for item ${item.id}`);
|
||||
|
||||
|
||||
const email = item.processingResult as Email;
|
||||
const route = item.route;
|
||||
|
||||
try {
|
||||
// Apply DKIM signing if configured in the route
|
||||
if (item.route?.action.options?.mtaOptions?.dkimSign) {
|
||||
await this.applyDkimSigning(email, item.route.action.options.mtaOptions);
|
||||
}
|
||||
|
||||
// In a full implementation, this would use the MTA service
|
||||
// For now, we'll simulate a successful delivery
|
||||
|
||||
logger.log('info', `Email processed by MTA: ${email.subject} to ${email.getAllRecipients().join(', ')}`);
|
||||
|
||||
// Note: The MTA implementation would handle actual local delivery
|
||||
|
||||
// Simulate successful delivery
|
||||
return {
|
||||
recipients: email.getAllRecipients().length,
|
||||
subject: email.subject,
|
||||
dkimSigned: !!item.route?.action.options?.mtaOptions?.dkimSign
|
||||
};
|
||||
} catch (error: any) {
|
||||
logger.log('error', `Failed to process email in MTA mode: ${error.message}`);
|
||||
throw error;
|
||||
|
||||
if (!this.emailServer) {
|
||||
throw new Error('No email server available for MTA delivery');
|
||||
}
|
||||
|
||||
// Build DKIM options from route config
|
||||
const dkimDomain = item.route?.action.options?.mtaOptions?.dkimSign
|
||||
? (item.route.action.options.mtaOptions.dkimOptions?.domainName || email.from.split('@')[1])
|
||||
: undefined;
|
||||
const dkimSelector = item.route?.action.options?.mtaOptions?.dkimOptions?.keySelector || 'default';
|
||||
|
||||
const allRecipients = email.getAllRecipients();
|
||||
if (allRecipients.length === 0) {
|
||||
throw new Error('No recipients specified for MTA delivery');
|
||||
}
|
||||
|
||||
const domainGroups = this.groupRecipientsByDomain(allRecipients);
|
||||
const results: Array<{ domain: string; success: boolean; error?: string; accepted?: string[]; rejected?: string[] }> = [];
|
||||
|
||||
for (const [domain, recipients] of domainGroups) {
|
||||
const mxHosts = await this.resolveMxForDomain(domain);
|
||||
let delivered = false;
|
||||
let lastError: string | undefined;
|
||||
|
||||
for (const mx of mxHosts) {
|
||||
try {
|
||||
logger.log('info', `MTA: trying MX ${mx.exchange}:25 for domain ${domain} (priority ${mx.priority})`);
|
||||
|
||||
// Create a temporary Email scoped to this domain's recipients
|
||||
const domainEmail = new Email({
|
||||
from: email.from,
|
||||
to: recipients.filter(r => email.to.includes(r)),
|
||||
cc: recipients.filter(r => (email.cc || []).includes(r)),
|
||||
bcc: recipients.filter(r => (email.bcc || []).includes(r)),
|
||||
subject: email.subject,
|
||||
text: email.text,
|
||||
html: email.html,
|
||||
});
|
||||
|
||||
const result = await this.emailServer.sendOutboundEmail(mx.exchange, 25, domainEmail, {
|
||||
dkimDomain,
|
||||
dkimSelector,
|
||||
});
|
||||
|
||||
results.push({
|
||||
domain,
|
||||
success: true,
|
||||
accepted: result.accepted,
|
||||
rejected: result.rejected,
|
||||
});
|
||||
delivered = true;
|
||||
logger.log('info', `MTA: delivered to ${domain} via ${mx.exchange}`);
|
||||
break;
|
||||
} catch (err: any) {
|
||||
lastError = err.message;
|
||||
logger.log('warn', `MTA: MX ${mx.exchange} failed for ${domain}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!delivered) {
|
||||
results.push({ domain, success: false, error: lastError });
|
||||
logger.log('error', `MTA: all MX hosts failed for ${domain}`);
|
||||
}
|
||||
}
|
||||
|
||||
const allFailed = results.every(r => !r.success);
|
||||
if (allFailed) {
|
||||
const summary = results.map(r => `${r.domain}: ${r.error}`).join('; ');
|
||||
throw new Error(`MTA delivery failed for all domains: ${summary}`);
|
||||
}
|
||||
|
||||
return {
|
||||
recipients: allRecipients.length,
|
||||
domainResults: results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -584,16 +666,10 @@ export class MultiModeDeliverySystem extends EventEmitter {
|
||||
await this.applyDkimSigning(email, item.route.action.options?.mtaOptions || {});
|
||||
}
|
||||
|
||||
logger.log('info', `Email successfully processed in store-and-forward mode`);
|
||||
|
||||
// Simulate successful delivery
|
||||
return {
|
||||
recipients: email.getAllRecipients().length,
|
||||
subject: email.subject,
|
||||
scanned: !!route?.action.options?.contentScanning,
|
||||
transformed: !!(route?.action.options?.transformations && route?.action.options?.transformations.length > 0),
|
||||
dkimSigned: !!(item.route?.action.options?.mtaOptions?.dkimSign || item.route?.action.process?.dkim)
|
||||
};
|
||||
logger.log('info', `Email successfully processed in store-and-forward mode, delivering via MTA`);
|
||||
|
||||
// After scanning + transformations, deliver via MTA
|
||||
return await this.handleMtaDelivery(item);
|
||||
} catch (error: any) {
|
||||
logger.log('error', `Failed to process email: ${error.message}`);
|
||||
throw error;
|
||||
|
||||
Reference in New Issue
Block a user