Files
smartmta/dist_ts/mail/security/classes.spfverifier.js
2026-02-10 15:54:09 +00:00

494 lines
35 KiB
JavaScript

import * as plugins from '../../plugins.js';
import { logger } from '../../logger.js';
import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.js';
/**
* SPF result qualifiers
*/
export var SpfQualifier;
(function (SpfQualifier) {
SpfQualifier["PASS"] = "+";
SpfQualifier["NEUTRAL"] = "?";
SpfQualifier["SOFTFAIL"] = "~";
SpfQualifier["FAIL"] = "-";
})(SpfQualifier || (SpfQualifier = {}));
/**
* SPF mechanism types
*/
export var SpfMechanismType;
(function (SpfMechanismType) {
SpfMechanismType["ALL"] = "all";
SpfMechanismType["INCLUDE"] = "include";
SpfMechanismType["A"] = "a";
SpfMechanismType["MX"] = "mx";
SpfMechanismType["IP4"] = "ip4";
SpfMechanismType["IP6"] = "ip6";
SpfMechanismType["EXISTS"] = "exists";
SpfMechanismType["REDIRECT"] = "redirect";
SpfMechanismType["EXP"] = "exp";
})(SpfMechanismType || (SpfMechanismType = {}));
/**
* Maximum lookup limit for SPF records (prevent infinite loops)
*/
const MAX_SPF_LOOKUPS = 10;
/**
* Class for verifying SPF records
*/
export class SpfVerifier {
// DNS Manager reference for verifying records
dnsManager;
lookupCount = 0;
constructor(dnsManager) {
this.dnsManager = dnsManager;
}
/**
* Parse SPF record from TXT record
* @param record SPF TXT record
* @returns Parsed SPF record or null if invalid
*/
parseSpfRecord(record) {
if (!record.startsWith('v=spf1')) {
return null;
}
try {
const spfRecord = {
version: 'spf1',
mechanisms: [],
modifiers: {}
};
// Split into terms
const terms = record.split(' ').filter(term => term.length > 0);
// Skip version term
for (let i = 1; i < terms.length; i++) {
const term = terms[i];
// Check if it's a modifier (name=value)
if (term.includes('=')) {
const [name, value] = term.split('=');
spfRecord.modifiers[name] = value;
continue;
}
// Parse as mechanism
let qualifier = SpfQualifier.PASS; // Default is +
let mechanismText = term;
// Check for qualifier
if (term.startsWith('+') || term.startsWith('-') ||
term.startsWith('~') || term.startsWith('?')) {
qualifier = term[0];
mechanismText = term.substring(1);
}
// Parse mechanism type and value
const colonIndex = mechanismText.indexOf(':');
let type;
let value;
if (colonIndex !== -1) {
type = mechanismText.substring(0, colonIndex);
value = mechanismText.substring(colonIndex + 1);
}
else {
type = mechanismText;
}
spfRecord.mechanisms.push({ qualifier, type, value });
}
return spfRecord;
}
catch (error) {
logger.log('error', `Error parsing SPF record: ${error.message}`, {
record,
error: error.message
});
return null;
}
}
/**
* Check if IP is in CIDR range
* @param ip IP address to check
* @param cidr CIDR range
* @returns Whether the IP is in the CIDR range
*/
isIpInCidr(ip, cidr) {
try {
const ipAddress = plugins.ip.Address4.parse(ip);
return ipAddress.isInSubnet(new plugins.ip.Address4(cidr));
}
catch (error) {
// Try IPv6
try {
const ipAddress = plugins.ip.Address6.parse(ip);
return ipAddress.isInSubnet(new plugins.ip.Address6(cidr));
}
catch (e) {
return false;
}
}
}
/**
* Check if a domain has the specified IP in its A or AAAA records
* @param domain Domain to check
* @param ip IP address to check
* @returns Whether the domain resolves to the IP
*/
async isDomainResolvingToIp(domain, ip) {
try {
// First try IPv4
const ipv4Addresses = await plugins.dns.promises.resolve4(domain);
if (ipv4Addresses.includes(ip)) {
return true;
}
// Then try IPv6
const ipv6Addresses = await plugins.dns.promises.resolve6(domain);
if (ipv6Addresses.includes(ip)) {
return true;
}
return false;
}
catch (error) {
return false;
}
}
/**
* Verify SPF for a given email with IP and helo domain
* @param email Email to verify
* @param ip Sender IP address
* @param heloDomain HELO/EHLO domain used by sender
* @returns SPF verification result
*/
async verify(email, ip, heloDomain) {
const securityLogger = SecurityLogger.getInstance();
// Reset lookup count
this.lookupCount = 0;
// Get domain from envelope from (return-path)
const domain = email.getEnvelopeFrom().split('@')[1] || '';
if (!domain) {
return {
result: 'permerror',
explanation: 'No envelope from domain',
domain: '',
ip
};
}
try {
// Look up SPF record
const spfVerificationResult = this.dnsManager ?
await this.dnsManager.verifySpfRecord(domain) :
{ found: false, valid: false, error: 'DNS Manager not available' };
if (!spfVerificationResult.found) {
return {
result: 'none',
explanation: 'No SPF record found',
domain,
ip
};
}
if (!spfVerificationResult.valid) {
return {
result: 'permerror',
explanation: 'Invalid SPF record',
domain,
ip,
record: spfVerificationResult.value
};
}
// Parse SPF record
const spfRecord = this.parseSpfRecord(spfVerificationResult.value);
if (!spfRecord) {
return {
result: 'permerror',
explanation: 'Failed to parse SPF record',
domain,
ip,
record: spfVerificationResult.value
};
}
// Check SPF record
const result = await this.checkSpfRecord(spfRecord, domain, ip);
// Log the result
const spfLogLevel = result.result === 'pass' ?
SecurityLogLevel.INFO :
(result.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO);
securityLogger.logEvent({
level: spfLogLevel,
type: SecurityEventType.SPF,
message: `SPF ${result.result} for ${domain} from IP ${ip}`,
domain,
details: {
ip,
heloDomain,
result: result.result,
explanation: result.explanation,
record: spfVerificationResult.value
},
success: result.result === 'pass'
});
return {
...result,
domain,
ip,
record: spfVerificationResult.value
};
}
catch (error) {
// Log 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 SPF record against IP address
* @param spfRecord Parsed SPF record
* @param domain Domain being checked
* @param ip IP address to check
* @returns SPF result
*/
async checkSpfRecord(spfRecord, domain, ip) {
// Check for 'redirect' modifier
if (spfRecord.modifiers.redirect) {
this.lookupCount++;
if (this.lookupCount > MAX_SPF_LOOKUPS) {
return {
result: 'permerror',
explanation: 'Too many DNS lookups',
domain,
ip
};
}
// Handle redirect
const redirectDomain = spfRecord.modifiers.redirect;
const redirectResult = this.dnsManager ?
await this.dnsManager.verifySpfRecord(redirectDomain) :
{ found: false, valid: false, error: 'DNS Manager not available' };
if (!redirectResult.found || !redirectResult.valid) {
return {
result: 'permerror',
explanation: `Invalid redirect to ${redirectDomain}`,
domain,
ip
};
}
const redirectRecord = this.parseSpfRecord(redirectResult.value);
if (!redirectRecord) {
return {
result: 'permerror',
explanation: `Failed to parse redirect record from ${redirectDomain}`,
domain,
ip
};
}
return this.checkSpfRecord(redirectRecord, redirectDomain, ip);
}
// Check each mechanism in order
for (const mechanism of spfRecord.mechanisms) {
let matched = false;
switch (mechanism.type) {
case SpfMechanismType.ALL:
matched = true;
break;
case SpfMechanismType.IP4:
if (mechanism.value) {
matched = this.isIpInCidr(ip, mechanism.value);
}
break;
case SpfMechanismType.IP6:
if (mechanism.value) {
matched = this.isIpInCidr(ip, mechanism.value);
}
break;
case SpfMechanismType.A:
this.lookupCount++;
if (this.lookupCount > MAX_SPF_LOOKUPS) {
return {
result: 'permerror',
explanation: 'Too many DNS lookups',
domain,
ip
};
}
// Check if domain has A/AAAA record matching IP
const checkDomain = mechanism.value || domain;
matched = await this.isDomainResolvingToIp(checkDomain, ip);
break;
case SpfMechanismType.MX:
this.lookupCount++;
if (this.lookupCount > MAX_SPF_LOOKUPS) {
return {
result: 'permerror',
explanation: 'Too many DNS lookups',
domain,
ip
};
}
// Check MX records
const mxDomain = mechanism.value || domain;
try {
const mxRecords = await plugins.dns.promises.resolveMx(mxDomain);
for (const mx of mxRecords) {
// Check if this MX record's IP matches
const mxMatches = await this.isDomainResolvingToIp(mx.exchange, ip);
if (mxMatches) {
matched = true;
break;
}
}
}
catch (error) {
// No MX records or error
matched = false;
}
break;
case SpfMechanismType.INCLUDE:
if (!mechanism.value) {
continue;
}
this.lookupCount++;
if (this.lookupCount > MAX_SPF_LOOKUPS) {
return {
result: 'permerror',
explanation: 'Too many DNS lookups',
domain,
ip
};
}
// Check included domain's SPF record
const includeDomain = mechanism.value;
const includeResult = this.dnsManager ?
await this.dnsManager.verifySpfRecord(includeDomain) :
{ found: false, valid: false, error: 'DNS Manager not available' };
if (!includeResult.found || !includeResult.valid) {
continue; // Skip this mechanism
}
const includeRecord = this.parseSpfRecord(includeResult.value);
if (!includeRecord) {
continue; // Skip this mechanism
}
// Recursively check the included SPF record
const includeCheck = await this.checkSpfRecord(includeRecord, includeDomain, ip);
// Include mechanism matches if the result is "pass"
matched = includeCheck.result === 'pass';
break;
case SpfMechanismType.EXISTS:
if (!mechanism.value) {
continue;
}
this.lookupCount++;
if (this.lookupCount > MAX_SPF_LOOKUPS) {
return {
result: 'permerror',
explanation: 'Too many DNS lookups',
domain,
ip
};
}
// Check if domain exists (has any A record)
try {
await plugins.dns.promises.resolve(mechanism.value, 'A');
matched = true;
}
catch (error) {
matched = false;
}
break;
}
// If this mechanism matched, return its result
if (matched) {
switch (mechanism.qualifier) {
case SpfQualifier.PASS:
return {
result: 'pass',
explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
domain,
ip
};
case SpfQualifier.FAIL:
return {
result: 'fail',
explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
domain,
ip
};
case SpfQualifier.SOFTFAIL:
return {
result: 'softfail',
explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
domain,
ip
};
case SpfQualifier.NEUTRAL:
return {
result: 'neutral',
explanation: `Matched ${mechanism.type}${mechanism.value ? ':' + mechanism.value : ''}`,
domain,
ip
};
}
}
}
// If no mechanism matched, default to neutral
return {
result: 'neutral',
explanation: 'No matching mechanism found',
domain,
ip
};
}
/**
* Check if email passes SPF verification
* @param email Email to verify
* @param ip Sender IP address
* @param heloDomain HELO/EHLO domain used by sender
* @returns Whether email passes SPF
*/
async verifyAndApply(email, ip, heloDomain) {
const result = await this.verify(email, ip, heloDomain);
// Add headers
email.headers['Received-SPF'] = `${result.result} (${result.domain}: ${result.explanation}) client-ip=${ip}; envelope-from=${email.getEnvelopeFrom()}; helo=${heloDomain};`;
// Apply policy based on result
switch (result.result) {
case 'fail':
// Fail - mark as spam
email.mightBeSpam = true;
logger.log('warn', `SPF failed for ${result.domain} from ${ip}: ${result.explanation}`);
return false;
case 'softfail':
// Soft fail - accept but mark as suspicious
email.mightBeSpam = true;
logger.log('info', `SPF softfailed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'neutral':
case 'none':
// Neutral or none - accept but note in headers
logger.log('info', `SPF ${result.result} for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'pass':
// Pass - accept
logger.log('info', `SPF passed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
case 'temperror':
case 'permerror':
// Temporary or permanent error - log but accept
logger.log('error', `SPF error for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
default:
return true;
}
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zcGZ2ZXJpZmllci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvc2VjdXJpdHkvY2xhc3Nlcy5zcGZ2ZXJpZmllci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFLOUY7O0dBRUc7QUFDSCxNQUFNLENBQU4sSUFBWSxZQUtYO0FBTEQsV0FBWSxZQUFZO0lBQ3RCLDBCQUFVLENBQUE7SUFDViw2QkFBYSxDQUFBO0lBQ2IsOEJBQWMsQ0FBQTtJQUNkLDBCQUFVLENBQUE7QUFDWixDQUFDLEVBTFcsWUFBWSxLQUFaLFlBQVksUUFLdkI7QUFFRDs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLGdCQVVYO0FBVkQsV0FBWSxnQkFBZ0I7SUFDMUIsK0JBQVcsQ0FBQTtJQUNYLHVDQUFtQixDQUFBO0lBQ25CLDJCQUFPLENBQUE7SUFDUCw2QkFBUyxDQUFBO0lBQ1QsK0JBQVcsQ0FBQTtJQUNYLCtCQUFXLENBQUE7SUFDWCxxQ0FBaUIsQ0FBQTtJQUNqQix5Q0FBcUIsQ0FBQTtJQUNyQiwrQkFBVyxDQUFBO0FBQ2IsQ0FBQyxFQVZXLGdCQUFnQixLQUFoQixnQkFBZ0IsUUFVM0I7QUFnQ0Q7O0dBRUc7QUFDSCxNQUFNLGVBQWUsR0FBRyxFQUFFLENBQUM7QUFFM0I7O0dBRUc7QUFDSCxNQUFNLE9BQU8sV0FBVztJQUN0Qiw4Q0FBOEM7SUFDdEMsVUFBVSxDQUFPO0lBQ2pCLFdBQVcsR0FBVyxDQUFDLENBQUM7SUFFaEMsWUFBWSxVQUFnQjtRQUMxQixJQUFJLENBQUMsVUFBVSxHQUFHLFVBQVUsQ0FBQztJQUMvQixDQUFDO0lBRUQ7Ozs7T0FJRztJ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