Files
smartmta/dist_ts/mail/security/classes.spfverifier.js

171 lines
14 KiB
JavaScript
Raw Normal View History

2026-02-10 15:54:09 +00:00
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';
2026-02-10 15:54:09 +00:00
/**
* 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 = {}));
/**
* Class for verifying SPF records.
* Delegates actual SPF evaluation to the Rust security bridge.
* Retains parseSpfRecord() for lightweight local parsing.
2026-02-10 15:54:09 +00:00
*/
export class SpfVerifier {
constructor(_dnsManager) {
// dnsManager is no longer needed — Rust handles DNS lookups
2026-02-10 15:54:09 +00:00
}
/**
* Parse SPF record from TXT record (pure string parsing, no DNS)
2026-02-10 15:54:09 +00:00
*/
parseSpfRecord(record) {
if (!record.startsWith('v=spf1')) {
return null;
}
try {
const spfRecord = {
version: 'spf1',
mechanisms: [],
modifiers: {}
};
const terms = record.split(' ').filter(term => term.length > 0);
for (let i = 1; i < terms.length; i++) {
const term = terms[i];
if (term.includes('=')) {
const [name, value] = term.split('=');
spfRecord.modifiers[name] = value;
continue;
}
let qualifier = SpfQualifier.PASS;
2026-02-10 15:54:09 +00:00
let mechanismText = term;
if (term.startsWith('+') || term.startsWith('-') ||
term.startsWith('~') || term.startsWith('?')) {
qualifier = term[0];
mechanismText = term.substring(1);
}
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;
}
}
/**
* Verify SPF for a given email delegates to Rust bridge
2026-02-10 15:54:09 +00:00
*/
async verify(email, ip, heloDomain) {
const securityLogger = SecurityLogger.getInstance();
const mailFrom = email.from || '';
const domain = mailFrom.split('@')[1] || '';
2026-02-10 15:54:09 +00:00
try {
const bridge = RustSecurityBridge.getInstance();
const result = await bridge.checkSpf({
ip,
heloDomain,
hostname: plugins.os.hostname(),
mailFrom,
});
const spfResult = {
result: result.result,
domain: result.domain,
ip: result.ip,
explanation: result.explanation ?? undefined,
};
2026-02-10 15:54:09 +00:00
securityLogger.logEvent({
level: spfResult.result === 'pass' ? SecurityLogLevel.INFO :
(spfResult.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO),
2026-02-10 15:54:09 +00:00
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'
2026-02-10 15:54:09 +00:00
});
return spfResult;
2026-02-10 15:54:09 +00:00
}
catch (error) {
logger.log('error', `SPF verification error: ${error.message}`, { domain, ip, error: error.message });
2026-02-10 15:54:09 +00:00
securityLogger.logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.SPF,
message: `SPF verification error for ${domain}`,
domain,
details: { ip, error: error.message },
2026-02-10 15:54:09 +00:00
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
2026-02-10 15:54:09 +00:00
*/
async verifyAndApply(email, ip, heloDomain) {
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};`;
2026-02-10 15:54:09 +00:00
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;
}
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zcGZ2ZXJpZmllci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvc2VjdXJpdHkvY2xhc3Nlcy5zcGZ2ZXJpZmllci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDOUYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOENBQThDLENBQUM7QUFHbEY7O0dBRUc7QUFDSCxNQUFNLENBQU4sSUFBWSxZQUtYO0FBTEQsV0FBWSxZQUFZO0lBQ3RCLDBCQUFVLENBQUE7SUFDViw2QkFBYSxDQUFBO0lBQ2IsOEJBQWMsQ0FBQTtJQUNkLDBCQUFVLENBQUE7QUFDWixDQUFDLEVBTFcsWUFBWSxLQUFaLFlBQVksUUFLdkI7QUFFRDs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLGdCQVVYO0FBVkQsV0FBWSxnQkFBZ0I7SUFDMUIsK0JBQVcsQ0FBQTtJQUNYLHVDQUFtQixDQUFBO0lBQ25CLDJCQUFPLENBQUE7SUFDUCw2QkFBUyxDQUFBO0lBQ1QsK0JBQVcsQ0FBQTtJQUNYLCtCQUFXLENBQUE7SUFDWCxxQ0FBaUIsQ0FBQTtJQUNqQix5Q0FBcUIsQ0FBQTtJQUNyQiwrQkFBVyxDQUFBO0FBQ2IsQ0FBQyxFQVZXLGdCQUFnQixLQUFoQixnQkFBZ0IsUUFVM0I7QUFnQ0Q7Ozs7R0FJRztBQUNILE1BQU0sT0FBTyxXQUFXO0lBQ3RCLFlBQVksV0FBaUI7UUFDM0IsNERBQTREO0lBQzlELENBQUM7SUFFRDs7T0FFRztJQUNJLGNBQWMsQ0FBQyxNQUFjO1FBQ2xDLElBQUksQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7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