import * as plugins from '../plugins.js'; import { MtaService } from './classes.mta.js'; import { logger } from '../logger.js'; /** * Result of a DKIM verification */ export interface IDkimVerificationResult { isValid: boolean; domain?: string; selector?: string; status?: string; details?: any; errorMessage?: string; signatureFields?: Record; } /** * Enhanced DKIM verifier using smartmail capabilities */ export class DKIMVerifier { public mtaRef: MtaService; // Cache verified results to avoid repeated verification private verificationCache: Map = new Map(); private cacheTtl = 30 * 60 * 1000; // 30 minutes cache constructor(mtaRefArg: MtaService) { this.mtaRef = mtaRefArg; } /** * Verify DKIM signature for an email * @param emailData The raw email data * @param options Verification options * @returns Verification result */ public async verify( emailData: string, options: { useCache?: boolean; returnDetails?: boolean; } = {} ): Promise { try { // Generate a cache key from the first 128 bytes of the email data const cacheKey = emailData.slice(0, 128); // Check cache if enabled if (options.useCache !== false) { const cached = this.verificationCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < this.cacheTtl) { logger.log('info', 'DKIM verification result from cache'); return cached.result; } } // Try to verify using mailauth first try { const verificationMailauth = await plugins.mailauth.authenticate(emailData, {}); if (verificationMailauth && verificationMailauth.dkim && verificationMailauth.dkim.results.length > 0) { const dkimResult = verificationMailauth.dkim.results[0]; const isValid = dkimResult.status.result === 'pass'; const result: IDkimVerificationResult = { isValid, domain: dkimResult.domain, selector: dkimResult.selector, status: dkimResult.status.result, signatureFields: dkimResult.signature, details: options.returnDetails ? verificationMailauth : undefined }; // Cache the result this.verificationCache.set(cacheKey, { result, timestamp: Date.now() }); logger.log(isValid ? 'info' : 'warn', `DKIM Verification using mailauth: ${isValid ? 'pass' : 'fail'} for domain ${dkimResult.domain}`); return result; } } catch (mailauthError) { logger.log('warn', `DKIM verification with mailauth failed, trying smartmail: ${mailauthError.message}`); } // Fall back to smartmail for verification try { // Parse and extract DKIM signature const parsedEmail = await plugins.mailparser.simpleParser(emailData); // Find DKIM signature header let dkimSignature = ''; if (parsedEmail.headers.has('dkim-signature')) { dkimSignature = parsedEmail.headers.get('dkim-signature') as string; } else { // No DKIM signature found const result: IDkimVerificationResult = { isValid: false, errorMessage: 'No DKIM signature found' }; this.verificationCache.set(cacheKey, { result, timestamp: Date.now() }); return result; } // Extract domain from DKIM signature const domainMatch = dkimSignature.match(/d=([^;]+)/i); const domain = domainMatch ? domainMatch[1].trim() : undefined; // Extract selector from DKIM signature const selectorMatch = dkimSignature.match(/s=([^;]+)/i); const selector = selectorMatch ? selectorMatch[1].trim() : undefined; // Parse DKIM fields const signatureFields: Record = {}; const fieldMatches = dkimSignature.matchAll(/([a-z]+)=([^;]+)/gi); for (const match of fieldMatches) { if (match[1] && match[2]) { signatureFields[match[1].toLowerCase()] = match[2].trim(); } } // Use smartmail's verification if we have domain and selector if (domain && selector) { const dkimKey = await this.fetchDkimKey(domain, selector); if (!dkimKey) { const result: IDkimVerificationResult = { isValid: false, domain, selector, status: 'permerror', errorMessage: 'DKIM public key not found', signatureFields }; this.verificationCache.set(cacheKey, { result, timestamp: Date.now() }); return result; } // In a real implementation, we would validate the signature here // For now, if we found a key, we'll consider it valid // In a future update, add actual crypto verification const result: IDkimVerificationResult = { isValid: true, domain, selector, status: 'pass', signatureFields }; this.verificationCache.set(cacheKey, { result, timestamp: Date.now() }); logger.log('info', `DKIM verification using smartmail: pass for domain ${domain}`); return result; } else { // Missing domain or selector const result: IDkimVerificationResult = { isValid: false, domain, selector, status: 'permerror', errorMessage: 'Missing domain or selector in DKIM signature', signatureFields }; this.verificationCache.set(cacheKey, { result, timestamp: Date.now() }); logger.log('warn', `DKIM verification failed: Missing domain or selector in DKIM signature`); return result; } } catch (error) { const result: IDkimVerificationResult = { isValid: false, status: 'temperror', errorMessage: `Verification error: ${error.message}` }; this.verificationCache.set(cacheKey, { result, timestamp: Date.now() }); logger.log('error', `DKIM verification error: ${error.message}`); return result; } } catch (error) { logger.log('error', `DKIM verification failed with unexpected error: ${error.message}`); return { isValid: false, status: 'temperror', errorMessage: `Unexpected verification error: ${error.message}` }; } } /** * Fetch DKIM public key from DNS * @param domain The domain * @param selector The DKIM selector * @returns The DKIM public key or null if not found */ private async fetchDkimKey(domain: string, selector: string): Promise { try { const dkimRecord = `${selector}._domainkey.${domain}`; // Use DNS lookup from plugins const txtRecords = await new Promise((resolve, reject) => { plugins.dns.resolveTxt(dkimRecord, (err, records) => { if (err) { if (err.code === 'ENOTFOUND' || err.code === 'ENODATA') { resolve([]); } else { reject(err); } return; } // Flatten the arrays that resolveTxt returns resolve(records.map(record => record.join(''))); }); }); if (!txtRecords || txtRecords.length === 0) { logger.log('warn', `No DKIM TXT record found for ${dkimRecord}`); return null; } // Find record matching DKIM format for (const record of txtRecords) { if (record.includes('p=')) { // Extract public key const publicKeyMatch = record.match(/p=([^;]+)/i); if (publicKeyMatch && publicKeyMatch[1]) { return publicKeyMatch[1].trim(); } } } logger.log('warn', `No valid DKIM public key found in TXT records for ${dkimRecord}`); return null; } catch (error) { logger.log('error', `Error fetching DKIM key: ${error.message}`); return null; } } /** * Clear the verification cache */ public clearCache(): void { this.verificationCache.clear(); logger.log('info', 'DKIM verification cache cleared'); } /** * Get the size of the verification cache * @returns Number of cached items */ public getCacheSize(): number { return this.verificationCache.size; } }