update
This commit is contained in:
254
ts/mail/delivery/smtpclient/tls-handler.ts
Normal file
254
ts/mail/delivery/smtpclient/tls-handler.ts
Normal file
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* 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.js';
|
||||
import type {
|
||||
ISmtpConnection,
|
||||
ISmtpClientOptions,
|
||||
ConnectionState
|
||||
} from './interfaces.js';
|
||||
import { CONNECTION_STATES } from './constants.js';
|
||||
import { logTLS, logDebug } from './utils/logging.js';
|
||||
import { isSuccessCode } from './utils/helpers.js';
|
||||
import type { CommandHandler } from './command-handler.js';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user