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';
|
2026-02-10 20:30:43 +00:00
|
|
|
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 = {}));
|
|
|
|
|
/**
|
2026-02-10 20:30:43 +00:00
|
|
|
* 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 {
|
2026-02-10 20:30:43 +00:00
|
|
|
constructor(_dnsManager) {
|
|
|
|
|
// dnsManager is no longer needed — Rust handles DNS lookups
|
2026-02-10 15:54:09 +00:00
|
|
|
}
|
|
|
|
|
/**
|
2026-02-10 20:30:43 +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;
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
2026-02-10 20:30:43 +00:00
|
|
|
* 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();
|
2026-02-10 20:30:43 +00:00
|
|
|
const mailFrom = email.from || '';
|
|
|
|
|
const domain = mailFrom.split('@')[1] || '';
|
2026-02-10 15:54:09 +00:00
|
|
|
try {
|
2026-02-10 20:30:43 +00:00
|
|
|
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({
|
2026-02-10 20:30:43 +00:00
|
|
|
level: spfResult.result === 'pass' ? SecurityLogLevel.INFO :
|
|
|
|
|
(spfResult.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO),
|
2026-02-10 15:54:09 +00:00
|
|
|
type: SecurityEventType.SPF,
|
2026-02-10 20:30:43 +00:00
|
|
|
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
|
|
|
});
|
2026-02-10 20:30:43 +00:00
|
|
|
return spfResult;
|
2026-02-10 15:54:09 +00:00
|
|
|
}
|
|
|
|
|
catch (error) {
|
2026-02-10 20:30:43 +00:00
|
|
|
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,
|
2026-02-10 20:30:43 +00:00
|
|
|
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
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
/**
|
2026-02-10 20:30:43 +00:00
|
|
|
* 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);
|
2026-02-10 20:30:43 +00:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-10 20:30:43 +00:00
|
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xhc3Nlcy5zcGZ2ZXJpZmllci5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL21haWwvc2VjdXJpdHkvY2xhc3Nlcy5zcGZ2ZXJpZmllci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEtBQUssT0FBTyxNQUFNLGtCQUFrQixDQUFDO0FBQzVDLE9BQU8sRUFBRSxNQUFNLEVBQUUsTUFBTSxpQkFBaUIsQ0FBQztBQUN6QyxPQUFPLEVBQUUsY0FBYyxFQUFFLGdCQUFnQixFQUFFLGlCQUFpQixFQUFFLE1BQU0seUJBQXlCLENBQUM7QUFDOUYsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sOENBQThDLENBQUM7QUFHbEY7O0dBRUc7QUFDSCxNQUFNLENBQU4sSUFBWSxZQUtYO0FBTEQsV0FBWSxZQUFZO0lBQ3RCLDBCQUFVLENBQUE7SUFDViw2QkFBYSxDQUFBO0lBQ2IsOEJBQWMsQ0FBQTtJQUNkLDBCQUFVLENBQUE7QUFDWixDQUFDLEVBTFcsWUFBWSxLQUFaLFlBQVksUUFLdkI7QUFFRDs7R0FFRztBQUNILE1BQU0sQ0FBTixJQUFZLGdCQVVYO0FBVkQsV0FBWSxnQkFBZ0I7SUFDMUIsK0JBQVcsQ0FBQTtJQUNYLHVDQUFtQixDQUFBO0lBQ25CLDJCQUFPLENBQUE7SUFDUCw2QkFBUyxDQUFBO0lBQ1QsK0JBQVcsQ0FBQTtJQUNYLCtCQUFXLENBQUE7SUFDWCxxQ0FBaUIsQ0FBQTtJQUNqQix5Q0FBcUIsQ0FBQTtJQUNyQiwrQkFBVyxDQUFBO0FBQ2IsQ0FBQyxFQVZXLGdCQUFnQixLQUFoQixnQkFBZ0IsUUFVM0I7QUFnQ0Q7Ozs7R0FJRztBQUNILE1BQU0sT0FBTyxXQUFXO0lBQ3RCLFlBQVksV0FBaUI7UUFDM0IsNERBQTREO0lBQzlELENBQUM7SUFFRDs7T0FFRztJQUNJLGNBQWMsQ0FBQyxNQUFjO1FBQ2xDLElBQUksQ0FBQyxNQUFNLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7WUFDakMsT0FBTyxJQUFJLENBQUM7UUFDZCxDQUFDO1FBRUQsSUFBSSxDQUFDO1lBQ0gsTUFBTSxTQUFTLEdBQWM7Z0JBQzNCLE9BQU8sRUFBRSxNQUFNO2dCQUNmLFVBQVUsRUFBRSxFQUFFO2dCQUNkLFNBQVMsRUFBRSxFQUFFO2FBQ2QsQ0FBQztZQUVGLE1BQU0sS0FBSyxHQUFHLE1BQU0sQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUMsSUFBSSxDQUFDLE1BQU0sR0FBRyxDQUFDLENBQUMsQ0FBQztZQUVoRSxLQUFLLElBQUksQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLEdBQUcsS0FBSyxDQUFDLE1BQU0sRUFBRSxDQUFDLEVBQUUsRUFBRSxDQUFDO2dCQUN0QyxNQUFNLElBQUksR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7Z0JBRXRCLElBQUksSUFBSSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO29CQUN2QixNQUFNLENBQUMsSUFBSSxFQUFFLEtBQUssQ0FBQyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7b0JBQ3RDLFNBQVMsQ0FBQyxTQUFTLENBQUMsSUFBSSxDQUFDLEdBQUcsS0FBSyxDQUFDO29CQUNsQyxTQUFTO2dCQUNYLENBQUM7Z0JBRUQsSUFBSSxTQUFTLEdBQUcsWUFBWSxDQUFDLElBQUksQ0FBQztnQkFDbEMsSUFBSSxhQUFhLEdBQUcsSUFBSSxDQUFDO2dCQUV6QixJQUFJLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUM7b0JBQzVDLElBQUksQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLElBQUksSUFBSSxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDO29CQUNqRCxTQUFTLEdBQUcsSUFBSSxDQUFDLENBQUMsQ0FBaUIsQ0FBQztvQkFDcEMsYUFBYSxHQUFHLElBQUksQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUM7Z0JBQ3BDLENBQUM7Z0JBRUQsTUFBTSxVQUFVLEdBQUcsYUFBYSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQztnQkFDOUMsSUFBSSxJQUFzQixDQUFDO2dCQUMzQixJQUFJLEtBQXlCLENBQUM7Z0JBRTlCLElBQUksVUFBVSxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUM7b0JBQ3RCLElBQUksR0FBRyxhQUFhLENBQUMsU0FBUyxDQUFDLENBQUMsRUFBRSxVQUFVLENBQXFCLENBQUM7b0JBQ2xFLEtBQUssR0FBRyxhQUFhLENBQUMsU0FBUyxDQUFDLFVBQVUsR0FBRyxDQUFDLENBQUMsQ0FBQztnQkFDbEQsQ0FBQztxQkFBTSxDQUFDO29CQUNOLElBQUksR0FBRyxhQUFpQyxDQUFDO2dCQUMzQyxDQUFDO2dCQUVELFNBQVMsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLEVBQUUsU0FBUyxFQUFFLElBQUksRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO1lBQ3hELENBQUM7WUFFRCxPQUFPLFNBQVMsQ0FBQztRQUNuQixDQUFDO1FBQUMsT0FBTyxLQUFLLEVBQUUsQ0FBQztZQUNmLE1BQU0sQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFLDZCQUE2QixLQUFLLENBQUMsT0FBTyxFQUFFLEVBQUU7Z0JBQ2hFLE1BQU07Z0JBQ04sS0FBSyxFQUFFLEtBQUssQ0FBQyxPQUFPO2FBQ3JCLENBQUMsQ0FBQztZQUNILE9BQU8sSUFBSSxDQUFDO1FBQ2QsQ0FBQztJQUNILENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxNQUFNLENBQ2pCLEtBQVksRUFDWixFQUFVLEVBQ1YsVUFBa0I7UUFFbEIsTUFBTSxjQUFjLEdBQUcsY0FBYyxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ3BELE1BQU0sUUFBUSxHQUFHLEtBQUssQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDO1FBQ2xDLE1BQU0sTUFBTSxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksRUFBRSxDQUFDO1FBRTVDLElBQUksQ0FBQztZQUNILE1BQU0sTUFBTSxHQUFHLGtCQUFrQixDQUFDLFdBQVcsRUFBRSxDQUFDO1lBQ2hELE1BQU0sTUFBTSxHQUFHLE1BQU0sTUFBTSxDQUFDLFFBQVEsQ0FBQztnQkFDbkMsRUFBRTtnQkFDRixVQUFVO2dCQUNWLFFBQVEsRUFBRSxPQUFPLENBQUMsRUFBRSxDQUFDLFFBQVEsRUFBRTtnQkFDL0IsUUFBUTthQUNULENBQUMsQ0FBQztZQUVILE1BQU0sU0FBUyxHQUFjO2dCQUMzQixNQUFNLEVBQUUsTUFBTSxDQUFDLE1BQTZCO2dCQUM1QyxNQUFNLEVBQUUsTUFBTSxDQUFDLE1BQU07Z0JBQ3JCLEVBQUUsRUFBRSxNQUFNL
|