254 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			254 lines
		
	
	
		
			7.4 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| /**
 | |
|  * SMTP Client TLS Handler
 | |
|  * TLS and STARTTLS client functionality
 | |
|  */
 | |
| 
 | |
| import * as tls from 'node:tls';
 | |
| import * as net from 'node:net';
 | |
| import { DEFAULTS } from './constants.ts';
 | |
| import type { 
 | |
|   ISmtpConnection, 
 | |
|   ISmtpClientOptions,
 | |
|   ConnectionState 
 | |
| } from './interfaces.ts';
 | |
| import { CONNECTION_STATES } from './constants.ts';
 | |
| import { logTLS, logDebug } from './utils/logging.ts';
 | |
| import { isSuccessCode } from './utils/helpers.ts';
 | |
| import type { CommandHandler } from './command-handler.ts';
 | |
| 
 | |
| export class TlsHandler {
 | |
|   private options: ISmtpClientOptions;
 | |
|   private commandHandler: CommandHandler;
 | |
|   
 | |
|   constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) {
 | |
|     this.options = options;
 | |
|     this.commandHandler = commandHandler;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Upgrade connection to TLS using STARTTLS
 | |
|    */
 | |
|   public async upgradeToTLS(connection: ISmtpConnection): Promise<void> {
 | |
|     if (connection.secure) {
 | |
|       logDebug('Connection already secure', this.options);
 | |
|       return;
 | |
|     }
 | |
|     
 | |
|     // Check if STARTTLS is supported
 | |
|     if (!connection.capabilities?.starttls) {
 | |
|       throw new Error('Server does not support STARTTLS');
 | |
|     }
 | |
|     
 | |
|     logTLS('starttls_start', this.options);
 | |
|     
 | |
|     try {
 | |
|       // Send STARTTLS command
 | |
|       const response = await this.commandHandler.sendStartTls(connection);
 | |
|       
 | |
|       if (!isSuccessCode(response.code)) {
 | |
|         throw new Error(`STARTTLS command failed: ${response.message}`);
 | |
|       }
 | |
|       
 | |
|       // Upgrade the socket to TLS
 | |
|       await this.performTLSUpgrade(connection);
 | |
|       
 | |
|       // Clear capabilities as they may have changed after TLS
 | |
|       connection.capabilities = undefined;
 | |
|       connection.secure = true;
 | |
|       
 | |
|       logTLS('starttls_success', this.options);
 | |
|       
 | |
|     } catch (error) {
 | |
|       logTLS('starttls_failure', this.options, { error });
 | |
|       throw error;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Create a direct TLS connection
 | |
|    */
 | |
|   public async createTLSConnection(host: string, port: number): Promise<tls.TLSSocket> {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
 | |
|       
 | |
|       const tlsOptions: tls.ConnectionOptions = {
 | |
|         host,
 | |
|         port,
 | |
|         ...this.options.tls,
 | |
|         // Default TLS options for email
 | |
|         secureProtocol: 'TLS_method',
 | |
|         ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
 | |
|         rejectUnauthorized: this.options.tls?.rejectUnauthorized !== false
 | |
|       };
 | |
|       
 | |
|       logTLS('tls_connected', this.options, { host, port });
 | |
|       
 | |
|       const socket = tls.connect(tlsOptions);
 | |
|       
 | |
|       const timeoutHandler = setTimeout(() => {
 | |
|         socket.destroy();
 | |
|         reject(new Error(`TLS connection timeout after ${timeout}ms`));
 | |
|       }, timeout);
 | |
|       
 | |
|       socket.once('secureConnect', () => {
 | |
|         clearTimeout(timeoutHandler);
 | |
|         
 | |
|         if (!socket.authorized && this.options.tls?.rejectUnauthorized !== false) {
 | |
|           socket.destroy();
 | |
|           reject(new Error(`TLS certificate verification failed: ${socket.authorizationError}`));
 | |
|           return;
 | |
|         }
 | |
|         
 | |
|         logDebug('TLS connection established', this.options, {
 | |
|           authorized: socket.authorized,
 | |
|           protocol: socket.getProtocol(),
 | |
|           cipher: socket.getCipher()
 | |
|         });
 | |
|         
 | |
|         resolve(socket);
 | |
|       });
 | |
|       
 | |
|       socket.once('error', (error) => {
 | |
|         clearTimeout(timeoutHandler);
 | |
|         reject(error);
 | |
|       });
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Validate TLS certificate
 | |
|    */
 | |
|   public validateCertificate(socket: tls.TLSSocket): boolean {
 | |
|     if (!socket.authorized) {
 | |
|       logDebug('TLS certificate not authorized', this.options, {
 | |
|         error: socket.authorizationError
 | |
|       });
 | |
|       
 | |
|       // Allow self-signed certificates if explicitly configured
 | |
|       if (this.options.tls?.rejectUnauthorized === false) {
 | |
|         logDebug('Accepting unauthorized certificate (rejectUnauthorized: false)', this.options);
 | |
|         return true;
 | |
|       }
 | |
|       
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     const cert = socket.getPeerCertificate();
 | |
|     if (!cert) {
 | |
|       logDebug('No peer certificate available', this.options);
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     // Additional certificate validation
 | |
|     const now = new Date();
 | |
|     if (cert.valid_from && new Date(cert.valid_from) > now) {
 | |
|       logDebug('Certificate not yet valid', this.options, { validFrom: cert.valid_from });
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     if (cert.valid_to && new Date(cert.valid_to) < now) {
 | |
|       logDebug('Certificate expired', this.options, { validTo: cert.valid_to });
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     logDebug('TLS certificate validated', this.options, {
 | |
|       subject: cert.subject,
 | |
|       issuer: cert.issuer,
 | |
|       validFrom: cert.valid_from,
 | |
|       validTo: cert.valid_to
 | |
|     });
 | |
|     
 | |
|     return true;
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Get TLS connection information
 | |
|    */
 | |
|   public getTLSInfo(socket: tls.TLSSocket): any {
 | |
|     if (!(socket instanceof tls.TLSSocket)) {
 | |
|       return null;
 | |
|     }
 | |
|     
 | |
|     return {
 | |
|       authorized: socket.authorized,
 | |
|       authorizationError: socket.authorizationError,
 | |
|       protocol: socket.getProtocol(),
 | |
|       cipher: socket.getCipher(),
 | |
|       peerCertificate: socket.getPeerCertificate(),
 | |
|       alpnProtocol: socket.alpnProtocol
 | |
|     };
 | |
|   }
 | |
|   
 | |
|   /**
 | |
|    * Check if TLS upgrade is required or recommended
 | |
|    */
 | |
|   public shouldUseTLS(connection: ISmtpConnection): boolean {
 | |
|     // Already secure
 | |
|     if (connection.secure) {
 | |
|       return false;
 | |
|     }
 | |
|     
 | |
|     // Direct TLS connection configured
 | |
|     if (this.options.secure) {
 | |
|       return false; // Already handled in connection establishment
 | |
|     }
 | |
|     
 | |
|     // STARTTLS available and not explicitly disabled
 | |
|     if (connection.capabilities?.starttls) {
 | |
|       return this.options.tls !== null && this.options.tls !== undefined; // Use TLS if configured
 | |
|     }
 | |
|     
 | |
|     return false;
 | |
|   }
 | |
|   
 | |
|   private async performTLSUpgrade(connection: ISmtpConnection): Promise<void> {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       const plainSocket = connection.socket as net.Socket;
 | |
|       const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
 | |
|       
 | |
|       const tlsOptions: tls.ConnectionOptions = {
 | |
|         socket: plainSocket,
 | |
|         host: this.options.host,
 | |
|         ...this.options.tls,
 | |
|         // Default TLS options for STARTTLS
 | |
|         secureProtocol: 'TLS_method',
 | |
|         ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
 | |
|         rejectUnauthorized: this.options.tls?.rejectUnauthorized !== false
 | |
|       };
 | |
|       
 | |
|       const timeoutHandler = setTimeout(() => {
 | |
|         reject(new Error(`TLS upgrade timeout after ${timeout}ms`));
 | |
|       }, timeout);
 | |
|       
 | |
|       // Create TLS socket from existing connection
 | |
|       const tlsSocket = tls.connect(tlsOptions);
 | |
|       
 | |
|       tlsSocket.once('secureConnect', () => {
 | |
|         clearTimeout(timeoutHandler);
 | |
|         
 | |
|         // Validate certificate if required
 | |
|         if (!this.validateCertificate(tlsSocket)) {
 | |
|           tlsSocket.destroy();
 | |
|           reject(new Error('TLS certificate validation failed'));
 | |
|           return;
 | |
|         }
 | |
|         
 | |
|         // Replace the socket in the connection
 | |
|         connection.socket = tlsSocket;
 | |
|         connection.secure = true;
 | |
|         
 | |
|         logDebug('STARTTLS upgrade completed', this.options, {
 | |
|           protocol: tlsSocket.getProtocol(),
 | |
|           cipher: tlsSocket.getCipher()
 | |
|         });
 | |
|         
 | |
|         resolve();
 | |
|       });
 | |
|       
 | |
|       tlsSocket.once('error', (error) => {
 | |
|         clearTimeout(timeoutHandler);
 | |
|         reject(error);
 | |
|       });
 | |
|     });
 | |
|   }
 | |
| } |