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'; /** * 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; } /** * 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. */ export class SpfVerifier { constructor(_dnsManager?: any) { // dnsManager is no longer needed — Rust handles DNS lookups } /** * Parse SPF record from TXT record (pure string parsing, no DNS) */ public parseSpfRecord(record: string): SpfRecord | null { if (!record.startsWith('v=spf1')) { return null; } try { const spfRecord: 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; let mechanismText = term; if (term.startsWith('+') || term.startsWith('-') || term.startsWith('~') || term.startsWith('?')) { qualifier = term[0] as SpfQualifier; mechanismText = term.substring(1); } const colonIndex = mechanismText.indexOf(':'); let type: SpfMechanismType; let value: string | undefined; if (colonIndex !== -1) { type = mechanismText.substring(0, colonIndex) as SpfMechanismType; value = mechanismText.substring(colonIndex + 1); } else { type = mechanismText as SpfMechanismType; } 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 */ public async verify( email: Email, ip: string, heloDomain: string ): Promise { const securityLogger = SecurityLogger.getInstance(); const mailFrom = email.from || ''; const domain = mailFrom.split('@')[1] || ''; 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, }; securityLogger.logEvent({ level: spfResult.result === 'pass' ? SecurityLogLevel.INFO : (spfResult.result === 'fail' ? SecurityLogLevel.WARN : SecurityLogLevel.INFO), 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' }); return spfResult; } catch (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 if email passes SPF verification and apply headers */ public async verifyAndApply( email: Email, ip: string, heloDomain: string ): Promise { 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};`; 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; } } }