Phase 3 of the Rust migration: the Rust security bridge is now mandatory and all TypeScript security fallback implementations have been removed. - UnifiedEmailServer.start() throws if Rust bridge fails to start - SpfVerifier gutted to thin wrapper (parseSpfRecord stays in TS) - DKIMVerifier gutted to thin wrapper delegating to bridge.verifyDkim() - IPReputationChecker delegates to bridge.checkIpReputation(), keeps LRU cache - DmarcVerifier keeps alignment logic (works with pre-computed results) - DKIM signing via bridge.signDkim() in all 4 locations - Removed mailauth and ip packages from plugins.ts (~1,200 lines deleted)
234 lines
6.2 KiB
TypeScript
234 lines
6.2 KiB
TypeScript
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<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.
|
|
*/
|
|
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<SpfResult> {
|
|
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<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};`;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|