Files
smartmta/dist_ts/mail/security/classes.dmarcverifier.js
Juergen Kunz b82468ab1e
Some checks failed
CI / Build Test (Current Platform) (push) Failing after 4s
CI / Type Check & Lint (push) Failing after 6s
CI / Build All Platforms (push) Failing after 4s
BREAKING CHANGE(rust-bridge): make Rust the primary security backend, remove all TS fallbacks
Phase 3 of the Rust migration: the Rust security bridge is now mandatory
and all TypeScript security fallback implementations have been removed.

- UnifiedEmailServer.start() throws if Rust bridge fails to start
- SpfVerifier gutted to thin wrapper (parseSpfRecord stays in TS)
- DKIMVerifier gutted to thin wrapper delegating to bridge.verifyDkim()
- IPReputationChecker delegates to bridge.checkIpReputation(), keeps LRU cache
- DmarcVerifier keeps alignment logic (works with pre-computed results)
- DKIM signing via bridge.signDkim() in all 4 locations
- Removed mailauth and ip packages from plugins.ts (~1,200 lines deleted)
2026-02-10 20:30:43 +00:00

366 lines
28 KiB
JavaScript

import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* DMARC policy types
*/
export var DmarcPolicy;
(function (DmarcPolicy) {
DmarcPolicy["NONE"] = "none";
DmarcPolicy["QUARANTINE"] = "quarantine";
DmarcPolicy["REJECT"] = "reject";
})(DmarcPolicy || (DmarcPolicy = {}));
/**
* DMARC alignment modes
*/
export var DmarcAlignment;
(function (DmarcAlignment) {
DmarcAlignment["RELAXED"] = "r";
DmarcAlignment["STRICT"] = "s";
})(DmarcAlignment || (DmarcAlignment = {}));
/**
* Class for verifying and enforcing DMARC policies
*/
export class DmarcVerifier {
// DNS Manager reference for verifying records
dnsManager;
constructor(dnsManager) {
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
*/
parseDmarcRecord(record) {
if (!record.startsWith('v=DMARC1')) {
return null;
}
try {
// Initialize record with default values
const 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;
break;
case 'sp':
dmarcRecord.subdomainPolicy = value;
break;
case 'pct':
const pctValue = parseInt(value, 10);
if (!isNaN(pctValue) && pctValue >= 0 && pctValue <= 100) {
dmarcRecord.pct = pctValue;
}
break;
case 'adkim':
dmarcRecord.adkim = value;
break;
case 'aspf':
dmarcRecord.aspf = value;
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
*/
isDomainAligned(headerDomain, authDomain, alignment) {
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
*/
getDomainFromEmail(email) {
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
*/
shouldApplyDmarc(record) {
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
*/
determineAction(policy) {
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
*/
async verify(email, spfResult, dkimResult) {
const securityLogger = SecurityLogger.getInstance();
// Initialize result
const result = {
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
*/
applyPolicy(email, dmarcResult) {
// 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
*/
async verifyAndApply(email, spfResult, dkimResult) {
// Verify DMARC
const dmarcResult = await this.verify(email, spfResult, dkimResult);
// Apply DMARC policy
return this.applyPolicy(email, dmarcResult);
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5kbWFyY3ZlcmlmaWVyLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vdHMvbWFpbC9zZWN1cml0eS9jbGFzc2VzLmRtYXJjdmVyaWZpZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLE1BQU0sRUFBRSxNQUFNLGlCQUFpQixDQUFDO0FBQ3pDLE9BQU8sRUFBRSxjQUFjLEVBQUUsZ0JBQWdCLEVBQUUsaUJBQWlCLEVBQUUsTUFBTSx5QkFBeUIsQ0FBQztBQUc5Rjs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLFdBSVg7QUFKRCxXQUFZLFdBQVc7SUFDckIsNEJBQWEsQ0FBQTtJQUNiLHdDQUF5QixDQUFBO0lBQ3pCLGdDQUFpQixDQUFBO0FBQ25CLENBQUMsRUFKVyxXQUFXLEtBQVgsV0FBVyxRQUl0QjtBQUVEOztHQUVHO0FBQ0gsTUFBTSxDQUFOLElBQVksY0FHWDtBQUhELFdBQVksY0FBYztJQUN4QiwrQkFBYSxDQUFBO0lBQ2IsOEJBQVksQ0FBQTtBQUNkLENBQUMsRUFIVyxjQUFjLEtBQWQsY0FBYyxRQUd6QjtBQXVDRDs7R0FFRztBQUNILE1BQU0sT0FBTyxhQUFhO0lBQ3hCLDhDQUE4QztJQUN0QyxVQUFVLENBQU87SUFFekIsWUFBWSxVQUFnQjtRQUMxQixJQUFJLENBQUMsVUFBVSxHQUFHLFVBQVUsQ0FBQztJQUMvQixDQUFDO0lBRUQ7Ozs7T0FJRztJQUNJLGdCQUFnQixDQUFDLE1BQWM7UUFDcEMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxVQUFVLENBQUMsVUFBVSxDQUFDLEVBQUUsQ0FBQztZQUNuQyxPQUFPLElBQUksQ0FBQztRQUNkLENBQUM7UUFFRCxJQUFJLENBQUM7WUFDSCx3Q0FBd0M7WUFDeEMsTUFBTSxXQUFXLEdBQWdCO2dCQUMvQixPQUFPLEVBQUUsUUFBUTtnQkFDakIsTUFBTSxFQUFFLFdBQVcsQ0FBQyxJQUFJO2dCQUN4QixHQUFHLEVBQUUsR0FBRztnQkFDUixL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