Files
smartmta/ts/mail/security/classes.spfverifier.ts

234 lines
6.2 KiB
TypeScript
Raw Normal View History

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';
2025-10-24 08:09:29 +00:00
/**
* SPF result qualifiers
*/
export enum SpfQualifier {
PASS = '+',
NEUTRAL = '?',
SOFTFAIL = '~',
FAIL = '-'
}
/**
* SPF mechanism types
*/
export enum SpfMechanismType {
ALL = 'all',
INCLUDE = 'include',
A = 'a',
MX = 'mx',
IP4 = 'ip4',
IP6 = 'ip6',
EXISTS = 'exists',
REDIRECT = 'redirect',
EXP = 'exp'
}
/**
* SPF mechanism definition
*/
export interface SpfMechanism {
qualifier: SpfQualifier;
type: SpfMechanismType;
value?: string;
}
/**
* SPF record parsed data
*/
export interface SpfRecord {
version: string;
mechanisms: SpfMechanism[];
modifiers: Record<string, string>;
}
/**
* SPF verification result
*/
export interface SpfResult {
result: 'pass' | 'neutral' | 'softfail' | 'fail' | 'temperror' | 'permerror' | 'none';
explanation?: string;
domain: string;
ip: string;
record?: string;
error?: string;
}
/**
* Class for verifying SPF records.
* Delegates actual SPF evaluation to the Rust security bridge.
* Retains parseSpfRecord() for lightweight local parsing.
2025-10-24 08:09:29 +00:00
*/
export class SpfVerifier {
constructor(_dnsManager?: any) {
// dnsManager is no longer needed — Rust handles DNS lookups
2025-10-24 08:09:29 +00:00
}
2025-10-24 08:09:29 +00:00
/**
* Parse SPF record from TXT record (pure string parsing, no DNS)
2025-10-24 08:09:29 +00:00
*/
public parseSpfRecord(record: string): SpfRecord | null {
if (!record.startsWith('v=spf1')) {
return null;
}
2025-10-24 08:09:29 +00:00
try {
const spfRecord: SpfRecord = {
version: 'spf1',
mechanisms: [],
modifiers: {}
};
2025-10-24 08:09:29 +00:00
const terms = record.split(' ').filter(term => term.length > 0);
2025-10-24 08:09:29 +00:00
for (let i = 1; i < terms.length; i++) {
const term = terms[i];
2025-10-24 08:09:29 +00:00
if (term.includes('=')) {
const [name, value] = term.split('=');
spfRecord.modifiers[name] = value;
continue;
}
let qualifier = SpfQualifier.PASS;
2025-10-24 08:09:29 +00:00
let mechanismText = term;
if (term.startsWith('+') || term.startsWith('-') ||
2025-10-24 08:09:29 +00:00
term.startsWith('~') || term.startsWith('?')) {
qualifier = term[0] as SpfQualifier;
mechanismText = term.substring(1);
}
2025-10-24 08:09:29 +00:00
const colonIndex = mechanismText.indexOf(':');
let type: SpfMechanismType;
let value: string | undefined;
2025-10-24 08:09:29 +00:00
if (colonIndex !== -1) {
type = mechanismText.substring(0, colonIndex) as SpfMechanismType;
value = mechanismText.substring(colonIndex + 1);
} else {
type = mechanismText as SpfMechanismType;
}
2025-10-24 08:09:29 +00:00
spfRecord.mechanisms.push({ qualifier, type, value });
}
2025-10-24 08:09:29 +00:00
return spfRecord;
} catch (error) {
logger.log('error', `Error parsing SPF record: ${error.message}`, {
record,
error: error.message
});
return null;
}
}
2025-10-24 08:09:29 +00:00
/**
* Verify SPF for a given email delegates to Rust bridge
2025-10-24 08:09:29 +00:00
*/
public async verify(
email: Email,
ip: string,
heloDomain: string
): Promise<SpfResult> {
const securityLogger = SecurityLogger.getInstance();
const mailFrom = email.from || '';
const domain = mailFrom.split('@')[1] || '';
2025-10-24 08:09:29 +00:00
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,
};
2025-10-24 08:09:29 +00:00
securityLogger.logEvent({
level: spfResult.result === 'pass' ? SecurityLogLevel.INFO :
(spfResult.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO),
2025-10-24 08:09:29 +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'
2025-10-24 08:09:29 +00:00
});
return spfResult;
2025-10-24 08:09:29 +00:00
} catch (error) {
logger.log('error', `SPF verification error: ${error.message}`, { domain, ip, error: error.message });
2025-10-24 08:09:29 +00:00
securityLogger.logEvent({
level: SecurityLogLevel.ERROR,
type: SecurityEventType.SPF,
message: `SPF verification error for ${domain}`,
domain,
details: { ip, error: error.message },
2025-10-24 08:09:29 +00:00
success: false
});
2025-10-24 08:09:29 +00:00
return {
result: 'temperror',
explanation: `Error verifying SPF: ${error.message}`,
domain,
ip,
error: error.message
};
}
}
2025-10-24 08:09:29 +00:00
/**
* Check if email passes SPF verification and apply headers
2025-10-24 08:09:29 +00:00
*/
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};`;
2025-10-24 08:09:29 +00:00
switch (result.result) {
case 'fail':
email.mightBeSpam = true;
logger.log('warn', `SPF failed for ${result.domain} from ${ip}: ${result.explanation}`);
return false;
2025-10-24 08:09:29 +00:00
case 'softfail':
email.mightBeSpam = true;
logger.log('info', `SPF softfailed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
2025-10-24 08:09:29 +00:00
case 'neutral':
case 'none':
logger.log('info', `SPF ${result.result} for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
2025-10-24 08:09:29 +00:00
case 'pass':
logger.log('info', `SPF passed for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
2025-10-24 08:09:29 +00:00
case 'temperror':
case 'permerror':
logger.log('error', `SPF error for ${result.domain} from ${ip}: ${result.explanation}`);
return true;
2025-10-24 08:09:29 +00:00
default:
return true;
}
}
}