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:
2026-02-11 07:55:28 +00:00
parent cf8fcb6efa
commit 526dcb4dac
15 changed files with 619 additions and 1870 deletions

View File

@@ -1,86 +0,0 @@
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
/**
* Result of a DKIM verification
*/
export interface IDkimVerificationResult {
isValid: boolean;
domain?: string;
selector?: string;
status?: string;
details?: any;
errorMessage?: string;
signatureFields?: Record<string, string>;
}
/**
* DKIM verifier — delegates to the Rust security bridge.
*/
export class DKIMVerifier {
constructor() {}
/**
* Verify DKIM signature for an email via Rust bridge
*/
public async verify(
emailData: string,
options: {
useCache?: boolean;
returnDetails?: boolean;
} = {}
): Promise<IDkimVerificationResult> {
try {
const bridge = RustSecurityBridge.getInstance();
const results = await bridge.verifyDkim(emailData);
const first = results[0];
const result: IDkimVerificationResult = {
isValid: first?.is_valid ?? false,
domain: first?.domain ?? undefined,
selector: first?.selector ?? undefined,
status: first?.status ?? 'none',
details: options.returnDetails ? results : undefined,
};
SecurityLogger.getInstance().logEvent({
level: result.isValid ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
type: SecurityEventType.DKIM,
message: `DKIM verification ${result.isValid ? 'passed' : 'failed'} for domain ${result.domain || 'unknown'}`,
details: { selector: result.selector, status: result.status },
domain: result.domain || 'unknown',
success: result.isValid
});
logger.log(result.isValid ? 'info' : 'warn',
`DKIM verification: ${result.status} for domain ${result.domain || 'unknown'}`);
return result;
} catch (error) {
logger.log('error', `DKIM verification failed: ${error.message}`);
SecurityLogger.getInstance().logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DKIM,
message: `DKIM verification error`,
details: { error: error.message },
success: false
});
return {
isValid: false,
status: 'temperror',
errorMessage: `Verification error: ${error.message}`
};
}
}
/** No-op — Rust bridge handles its own caching */
public clearCache(): void {}
/** Always 0 — cache is managed by the Rust side */
public getCacheSize(): number {
return 0;
}
}

View File

@@ -1,475 +0,0 @@
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import type { Email } from '../core/classes.email.js';
/**
* DMARC policy types
*/
export enum DmarcPolicy {
NONE = 'none',
QUARANTINE = 'quarantine',
REJECT = 'reject'
}
/**
* DMARC alignment modes
*/
export enum DmarcAlignment {
RELAXED = 'r',
STRICT = 's'
}
/**
* DMARC record fields
*/
export interface DmarcRecord {
// Required fields
version: string;
policy: DmarcPolicy;
// Optional fields
subdomainPolicy?: DmarcPolicy;
pct?: number;
adkim?: DmarcAlignment;
aspf?: DmarcAlignment;
reportInterval?: number;
failureOptions?: string;
reportUriAggregate?: string[];
reportUriForensic?: string[];
}
/**
* DMARC verification result
*/
export interface DmarcResult {
hasDmarc: boolean;
record?: DmarcRecord;
spfDomainAligned: boolean;
dkimDomainAligned: boolean;
spfPassed: boolean;
dkimPassed: boolean;
policyEvaluated: DmarcPolicy;
actualPolicy: DmarcPolicy;
appliedPercentage: number;
action: 'pass' | 'quarantine' | 'reject';
details: string;
error?: string;
}
/**
* Class for verifying and enforcing DMARC policies
*/
export class DmarcVerifier {
// DNS Manager reference for verifying records
private dnsManager?: any;
constructor(dnsManager?: any) {
this.dnsManager = dnsManager;
}
/**
* Parse a DMARC record from a TXT record string
* @param record DMARC TXT record string
* @returns Parsed DMARC record or null if invalid
*/
public parseDmarcRecord(record: string): DmarcRecord | null {
if (!record.startsWith('v=DMARC1')) {
return null;
}
try {
// Initialize record with default values
const dmarcRecord: DmarcRecord = {
version: 'DMARC1',
policy: DmarcPolicy.NONE,
pct: 100,
adkim: DmarcAlignment.RELAXED,
aspf: DmarcAlignment.RELAXED
};
// Split the record into tag/value pairs
const parts = record.split(';').map(part => part.trim());
for (const part of parts) {
if (!part || !part.includes('=')) continue;
const [tag, value] = part.split('=').map(p => p.trim());
// Process based on tag
switch (tag.toLowerCase()) {
case 'v':
dmarcRecord.version = value;
break;
case 'p':
dmarcRecord.policy = value as DmarcPolicy;
break;
case 'sp':
dmarcRecord.subdomainPolicy = value as DmarcPolicy;
break;
case 'pct':
const pctValue = parseInt(value, 10);
if (!isNaN(pctValue) && pctValue >= 0 && pctValue <= 100) {
dmarcRecord.pct = pctValue;
}
break;
case 'adkim':
dmarcRecord.adkim = value as DmarcAlignment;
break;
case 'aspf':
dmarcRecord.aspf = value as DmarcAlignment;
break;
case 'ri':
const interval = parseInt(value, 10);
if (!isNaN(interval) && interval > 0) {
dmarcRecord.reportInterval = interval;
}
break;
case 'fo':
dmarcRecord.failureOptions = value;
break;
case 'rua':
dmarcRecord.reportUriAggregate = value.split(',').map(uri => {
if (uri.startsWith('mailto:')) {
return uri.substring(7).trim();
}
return uri.trim();
});
break;
case 'ruf':
dmarcRecord.reportUriForensic = value.split(',').map(uri => {
if (uri.startsWith('mailto:')) {
return uri.substring(7).trim();
}
return uri.trim();
});
break;
}
}
// Ensure subdomain policy is set if not explicitly provided
if (!dmarcRecord.subdomainPolicy) {
dmarcRecord.subdomainPolicy = dmarcRecord.policy;
}
return dmarcRecord;
} catch (error) {
logger.log('error', `Error parsing DMARC record: ${error.message}`, {
record,
error: error.message
});
return null;
}
}
/**
* Check if domains are aligned according to DMARC policy
* @param headerDomain Domain from header (From)
* @param authDomain Domain from authentication (SPF, DKIM)
* @param alignment Alignment mode
* @returns Whether the domains are aligned
*/
private isDomainAligned(
headerDomain: string,
authDomain: string,
alignment: DmarcAlignment
): boolean {
if (!headerDomain || !authDomain) {
return false;
}
// For strict alignment, domains must match exactly
if (alignment === DmarcAlignment.STRICT) {
return headerDomain.toLowerCase() === authDomain.toLowerCase();
}
// For relaxed alignment, the authenticated domain must be a subdomain of the header domain
// or the same as the header domain
const headerParts = headerDomain.toLowerCase().split('.');
const authParts = authDomain.toLowerCase().split('.');
// Ensures we have at least two parts (domain and TLD)
if (headerParts.length < 2 || authParts.length < 2) {
return false;
}
// Get organizational domain (last two parts)
const headerOrgDomain = headerParts.slice(-2).join('.');
const authOrgDomain = authParts.slice(-2).join('.');
return headerOrgDomain === authOrgDomain;
}
/**
* Extract domain from an email address
* @param email Email address
* @returns Domain part of the email
*/
private getDomainFromEmail(email: string): string {
if (!email) return '';
// Handle name + email format: "John Doe <john@example.com>"
const matches = email.match(/<([^>]+)>/);
const address = matches ? matches[1] : email;
const parts = address.split('@');
return parts.length > 1 ? parts[1] : '';
}
/**
* Check if DMARC verification should be applied based on percentage
* @param record DMARC record
* @returns Whether DMARC verification should be applied
*/
private shouldApplyDmarc(record: DmarcRecord): boolean {
if (record.pct === undefined || record.pct === 100) {
return true;
}
// Apply DMARC randomly based on percentage
const random = Math.floor(Math.random() * 100) + 1;
return random <= record.pct;
}
/**
* Determine the action to take based on DMARC policy
* @param policy DMARC policy
* @returns Action to take
*/
private determineAction(policy: DmarcPolicy): 'pass' | 'quarantine' | 'reject' {
switch (policy) {
case DmarcPolicy.REJECT:
return 'reject';
case DmarcPolicy.QUARANTINE:
return 'quarantine';
case DmarcPolicy.NONE:
default:
return 'pass';
}
}
/**
* Verify DMARC for an incoming email
* @param email Email to verify
* @param spfResult SPF verification result
* @param dkimResult DKIM verification result
* @returns DMARC verification result
*/
public async verify(
email: Email,
spfResult: { domain: string; result: boolean },
dkimResult: { domain: string; result: boolean }
): Promise<DmarcResult> {
const securityLogger = SecurityLogger.getInstance();
// Initialize result
const result: DmarcResult = {
hasDmarc: false,
spfDomainAligned: false,
dkimDomainAligned: false,
spfPassed: spfResult.result,
dkimPassed: dkimResult.result,
policyEvaluated: DmarcPolicy.NONE,
actualPolicy: DmarcPolicy.NONE,
appliedPercentage: 100,
action: 'pass',
details: 'DMARC not configured'
};
try {
// Extract From domain
const fromHeader = email.getFromEmail();
const fromDomain = this.getDomainFromEmail(fromHeader);
if (!fromDomain) {
result.error = 'Invalid From domain';
return result;
}
// Check alignment
result.spfDomainAligned = this.isDomainAligned(
fromDomain,
spfResult.domain,
DmarcAlignment.RELAXED
);
result.dkimDomainAligned = this.isDomainAligned(
fromDomain,
dkimResult.domain,
DmarcAlignment.RELAXED
);
// Lookup DMARC record
const dmarcVerificationResult = this.dnsManager ?
await this.dnsManager.verifyDmarcRecord(fromDomain) :
{ found: false, valid: false, error: 'DNS Manager not available' };
// If DMARC record exists and is valid
if (dmarcVerificationResult.found && dmarcVerificationResult.valid) {
result.hasDmarc = true;
// Parse DMARC record
const parsedRecord = this.parseDmarcRecord(dmarcVerificationResult.value);
if (parsedRecord) {
result.record = parsedRecord;
result.actualPolicy = parsedRecord.policy;
result.appliedPercentage = parsedRecord.pct || 100;
// Override alignment modes if specified in record
if (parsedRecord.adkim) {
result.dkimDomainAligned = this.isDomainAligned(
fromDomain,
dkimResult.domain,
parsedRecord.adkim
);
}
if (parsedRecord.aspf) {
result.spfDomainAligned = this.isDomainAligned(
fromDomain,
spfResult.domain,
parsedRecord.aspf
);
}
// Determine DMARC compliance
const spfAligned = result.spfPassed && result.spfDomainAligned;
const dkimAligned = result.dkimPassed && result.dkimDomainAligned;
// Email passes DMARC if either SPF or DKIM passes with alignment
const dmarcPass = spfAligned || dkimAligned;
// Use record percentage to determine if policy should be applied
const applyPolicy = this.shouldApplyDmarc(parsedRecord);
if (!dmarcPass) {
// DMARC failed, apply policy
result.policyEvaluated = applyPolicy ? parsedRecord.policy : DmarcPolicy.NONE;
result.action = this.determineAction(result.policyEvaluated);
result.details = `DMARC failed: SPF aligned=${spfAligned}, DKIM aligned=${dkimAligned}, policy=${result.policyEvaluated}`;
} else {
result.policyEvaluated = DmarcPolicy.NONE;
result.action = 'pass';
result.details = `DMARC passed: SPF aligned=${spfAligned}, DKIM aligned=${dkimAligned}`;
}
} else {
result.error = 'Invalid DMARC record format';
result.details = 'DMARC record invalid';
}
} else {
// No DMARC record found or invalid
result.details = dmarcVerificationResult.error || 'No DMARC record found';
}
// Log the DMARC verification
securityLogger.logEvent({
level: result.action === 'pass' ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
type: SecurityEventType.DMARC,
message: result.details,
domain: fromDomain,
details: {
fromDomain,
spfDomain: spfResult.domain,
dkimDomain: dkimResult.domain,
spfPassed: result.spfPassed,
dkimPassed: result.dkimPassed,
spfAligned: result.spfDomainAligned,
dkimAligned: result.dkimDomainAligned,
dmarcPolicy: result.policyEvaluated,
action: result.action
},
success: result.action === 'pass'
});
return result;
} catch (error) {
logger.log('error', `Error verifying DMARC: ${error.message}`, {
error: error.message,
emailId: email.getMessageId()
});
result.error = `DMARC verification error: ${error.message}`;
// Log error
securityLogger.logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.DMARC,
message: `DMARC verification failed with error`,
details: {
error: error.message,
emailId: email.getMessageId()
},
success: false
});
return result;
}
}
/**
* Apply DMARC policy to an email
* @param email Email to apply policy to
* @param dmarcResult DMARC verification result
* @returns Whether the email should be accepted
*/
public applyPolicy(email: Email, dmarcResult: DmarcResult): boolean {
// Apply action based on DMARC verification result
switch (dmarcResult.action) {
case 'reject':
// Reject the email
email.mightBeSpam = true;
logger.log('warn', `Email rejected due to DMARC policy: ${dmarcResult.details}`, {
emailId: email.getMessageId(),
from: email.getFromEmail(),
subject: email.subject
});
return false;
case 'quarantine':
// Quarantine the email (mark as spam)
email.mightBeSpam = true;
// Add spam header
if (!email.headers['X-Spam-Flag']) {
email.headers['X-Spam-Flag'] = 'YES';
}
// Add DMARC reason header
email.headers['X-DMARC-Result'] = dmarcResult.details;
logger.log('warn', `Email quarantined due to DMARC policy: ${dmarcResult.details}`, {
emailId: email.getMessageId(),
from: email.getFromEmail(),
subject: email.subject
});
return true;
case 'pass':
default:
// Accept the email
// Add DMARC result header for information
email.headers['X-DMARC-Result'] = dmarcResult.details;
return true;
}
}
/**
* End-to-end DMARC verification and policy application
* This method should be called after SPF and DKIM verification
* @param email Email to verify
* @param spfResult SPF verification result
* @param dkimResult DKIM verification result
* @returns Whether the email should be accepted
*/
public async verifyAndApply(
email: Email,
spfResult: { domain: string; result: boolean },
dkimResult: { domain: string; result: boolean }
): Promise<boolean> {
// Verify DMARC
const dmarcResult = await this.verify(email, spfResult, dkimResult);
// Apply DMARC policy
return this.applyPolicy(email, dmarcResult);
}
}

View File

@@ -1,8 +1,4 @@
import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
import { RustSecurityBridge } from '../../security/classes.rustsecuritybridge.js';
import type { Email } from '../core/classes.email.js';
/**
* SPF result qualifiers
@@ -127,107 +123,4 @@ export class SpfVerifier {
return null;
}
}
/**
* Verify SPF for a given email — delegates to Rust bridge
*/
public async verify(
email: Email,
ip: string,
heloDomain: string
): Promise<SpfResult> {
const securityLogger = SecurityLogger.getInstance();
const mailFrom = email.from || '';
const domain = mailFrom.split('@')[1] || '';
try {
const bridge = RustSecurityBridge.getInstance();
const result = await bridge.checkSpf({
ip,
heloDomain,
hostname: plugins.os.hostname(),
mailFrom,
});
const spfResult: SpfResult = {
result: result.result as SpfResult['result'],
domain: result.domain,
ip: result.ip,
explanation: result.explanation ?? undefined,
};
securityLogger.logEvent({
level: spfResult.result === 'pass' ? SecurityLogLevel.INFO :
(spfResult.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO),
type: SecurityEventType.SPF,
message: `SPF ${spfResult.result} for ${spfResult.domain} from IP ${ip}`,
domain: spfResult.domain,
details: { ip, heloDomain, result: spfResult.result, explanation: spfResult.explanation },
success: spfResult.result === 'pass'
});
return spfResult;
} catch (error) {
logger.log('error', `SPF verification error: ${error.message}`, { domain, ip, error: error.message });
securityLogger.logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.SPF,
message: `SPF verification error for ${domain}`,
domain,
details: { ip, error: error.message },
success: false
});
return {
result: 'temperror',
explanation: `Error verifying SPF: ${error.message}`,
domain,
ip,
error: error.message
};
}
}
/**
* Check if email passes SPF verification and apply headers
*/
public async verifyAndApply(
email: Email,
ip: string,
heloDomain: string
): Promise<boolean> {
const result = await this.verify(email, ip, heloDomain);
email.headers['Received-SPF'] = `${result.result} (${result.domain}: ${result.explanation || ''}) client-ip=${ip}; envelope-from=${email.getEnvelopeFrom()}; helo=${heloDomain};`;
switch (result.result) {
case 'fail':
email.mightBeSpam = true;
logger.log('warn', `SPF failed for ${result.domain} from ${ip}: ${result.explanation}`);
return false;
case 'softfail':
email.mightBeSpam = true;
logger.log('info', `SPF softfailed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'neutral':
case 'none':
logger.log('info', `SPF ${result.result} for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'pass':
logger.log('info', `SPF passed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'temperror':
case 'permerror':
logger.log('error', `SPF error for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
default:
return true;
}
}
}

View File

@@ -1,5 +1,3 @@
// Email security components
export * from './classes.dkimcreator.js';
export * from './classes.dkimverifier.js';
export * from './classes.dmarcverifier.js';
export * from './classes.spfverifier.js';