382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import * as plugins from '../../plugins.ts';
 | |
| // MtaService reference removed
 | |
| import { logger } from '../../logger.ts';
 | |
| import { SecurityLogger, SecurityLogLevel, SecurityEventType } from '../../security/index.ts';
 | |
| 
 | |
| /**
 | |
|  * Result of a DKIM verification
 | |
|  */
 | |
| export interface IDkimVerificationResult {
 | |
|   isValid: boolean;
 | |
|   domain?: string;
 | |
|   selector?: string;
 | |
|   status?: string;
 | |
|   details?: any;
 | |
|   errorMessage?: string;
 | |
|   signatureFields?: Record<string, string>;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Enhanced DKIM verifier using smartmail capabilities
 | |
|  */
 | |
| export class DKIMVerifier {
 | |
|   // MtaRef reference removed
 | |
|   
 | |
|   // Cache verified results to avoid repeated verification
 | |
|   private verificationCache: Map<string, { result: IDkimVerificationResult, timestamp: number }> = new Map();
 | |
|   private cacheTtl = 30 * 60 * 1000; // 30 minutes cache
 | |
| 
 | |
|   constructor() {
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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<IDkimVerificationResult> {
 | |
|     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}`);
 | |
|           
 | |
|           // Enhanced security logging
 | |
|           SecurityLogger.getInstance().logEvent({
 | |
|             level: isValid ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
 | |
|             type: SecurityEventType.DKIM,
 | |
|             message: `DKIM verification ${isValid ? 'passed' : 'failed'} for domain ${dkimResult.domain}`,
 | |
|             details: {
 | |
|               selector: dkimResult.selector,
 | |
|               signatureFields: dkimResult.signature,
 | |
|               result: dkimResult.status.result
 | |
|             },
 | |
|             domain: dkimResult.domain,
 | |
|             success: isValid
 | |
|           });
 | |
|           
 | |
|           return result;
 | |
|         }
 | |
|       } catch (mailauthError) {
 | |
|         logger.log('warn', `DKIM verification with mailauth failed, trying smartmail: ${mailauthError.message}`);
 | |
|         
 | |
|         // Enhanced security logging
 | |
|         SecurityLogger.getInstance().logEvent({
 | |
|           level: SecurityLogLevel.WARN,
 | |
|           type: SecurityEventType.DKIM,
 | |
|           message: `DKIM verification with mailauth failed, trying smartmail fallback`,
 | |
|           details: { error: mailauthError.message },
 | |
|           success: false
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       // 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<string, string> = {};
 | |
|         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}`);
 | |
|           
 | |
|           // Enhanced security logging
 | |
|           SecurityLogger.getInstance().logEvent({
 | |
|             level: SecurityLogLevel.INFO,
 | |
|             type: SecurityEventType.DKIM,
 | |
|             message: `DKIM verification passed for domain ${domain} using fallback verification`,
 | |
|             details: {
 | |
|               selector,
 | |
|               signatureFields
 | |
|             },
 | |
|             domain,
 | |
|             success: true
 | |
|           });
 | |
|           
 | |
|           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`);
 | |
|           
 | |
|           // Enhanced security logging
 | |
|           SecurityLogger.getInstance().logEvent({
 | |
|             level: SecurityLogLevel.WARN,
 | |
|             type: SecurityEventType.DKIM,
 | |
|             message: `DKIM verification failed: Missing domain or selector in signature`,
 | |
|             details: { domain, selector, signatureFields },
 | |
|             domain: domain || 'unknown',
 | |
|             success: false
 | |
|           });
 | |
|           
 | |
|           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}`);
 | |
|         
 | |
|         // Enhanced security logging
 | |
|         SecurityLogger.getInstance().logEvent({
 | |
|           level: SecurityLogLevel.ERROR,
 | |
|           type: SecurityEventType.DKIM,
 | |
|           message: `DKIM verification error during processing`,
 | |
|           details: { error: error.message },
 | |
|           success: false
 | |
|         });
 | |
|         
 | |
|         return result;
 | |
|       }
 | |
|     } catch (error) {
 | |
|       logger.log('error', `DKIM verification failed with unexpected error: ${error.message}`);
 | |
|       
 | |
|       // Enhanced security logging for unexpected errors
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR,
 | |
|         type: SecurityEventType.DKIM,
 | |
|         message: `DKIM verification failed with unexpected error`,
 | |
|         details: { error: error.message },
 | |
|         success: false
 | |
|       });
 | |
|       
 | |
|       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<string | null> {
 | |
|     try {
 | |
|       const dkimRecord = `${selector}._domainkey.${domain}`;
 | |
|       
 | |
|       // Use DNS lookup from plugins
 | |
|       const txtRecords = await new Promise<string[]>((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}`);
 | |
|         
 | |
|         // Security logging for missing DKIM record
 | |
|         SecurityLogger.getInstance().logEvent({
 | |
|           level: SecurityLogLevel.WARN,
 | |
|           type: SecurityEventType.DKIM,
 | |
|           message: `No DKIM TXT record found for ${dkimRecord}`,
 | |
|           domain,
 | |
|           success: false,
 | |
|           details: { selector }
 | |
|         });
 | |
|         
 | |
|         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}`);
 | |
|       
 | |
|       // Security logging for invalid DKIM key
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.WARN,
 | |
|         type: SecurityEventType.DKIM,
 | |
|         message: `No valid DKIM public key found in TXT records`,
 | |
|         domain,
 | |
|         success: false,
 | |
|         details: { dkimRecord, selector }
 | |
|       });
 | |
|       
 | |
|       return null;
 | |
|     } catch (error) {
 | |
|       logger.log('error', `Error fetching DKIM key: ${error.message}`);
 | |
|       
 | |
|       // Security logging for DKIM key fetch error
 | |
|       SecurityLogger.getInstance().logEvent({
 | |
|         level: SecurityLogLevel.ERROR,
 | |
|         type: SecurityEventType.DKIM,
 | |
|         message: `Error fetching DKIM key for domain`,
 | |
|         domain,
 | |
|         success: false,
 | |
|         details: { error: error.message, selector, dkimRecord: `${selector}._domainkey.${domain}` }
 | |
|       });
 | |
|       
 | |
|       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;
 | |
|   }
 | |
| } |