204 lines
16 KiB
JavaScript
204 lines
16 KiB
JavaScript
/**
|
|
* 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 { CONNECTION_STATES } from './constants.js';
|
|
import { logTLS, logDebug } from './utils/logging.js';
|
|
import { isSuccessCode } from './utils/helpers.js';
|
|
export class TlsHandler {
|
|
options;
|
|
commandHandler;
|
|
constructor(options, commandHandler) {
|
|
this.options = options;
|
|
this.commandHandler = commandHandler;
|
|
}
|
|
/**
|
|
* Upgrade connection to TLS using STARTTLS
|
|
*/
|
|
async upgradeToTLS(connection) {
|
|
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
|
|
*/
|
|
async createTLSConnection(host, port) {
|
|
return new Promise((resolve, reject) => {
|
|
const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
|
|
const tlsOptions = {
|
|
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
|
|
*/
|
|
validateCertificate(socket) {
|
|
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
|
|
*/
|
|
getTLSInfo(socket) {
|
|
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
|
|
*/
|
|
shouldUseTLS(connection) {
|
|
// 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;
|
|
}
|
|
async performTLSUpgrade(connection) {
|
|
return new Promise((resolve, reject) => {
|
|
const plainSocket = connection.socket;
|
|
const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
|
|
const tlsOptions = {
|
|
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);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGxzLWhhbmRsZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBjbGllbnQvdGxzLWhhbmRsZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxLQUFLLEdBQUcsTUFBTSxVQUFVLENBQUM7QUFDaEMsT0FBTyxLQUFLLEdBQUcsTUFBTSxVQUFVLENBQUM7QUFDaEMsT0FBTyxFQUFFLFFBQVEsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBTTFDLE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLGdCQUFnQixDQUFDO0FBQ25ELE9BQU8sRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDdEQsT0FBTyxFQUFFLGFBQWEsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBR25ELE1BQU0sT0FBTyxVQUFVO0lBQ2IsT0FBTyxDQUFxQjtJQUM1QixjQUFjLENBQWlCO0lBRXZDLFlBQVksT0FBMkIsRUFBRSxjQUE4QjtRQUNyRSxJQUFJLENBQUMsT0FBTyxHQUFHLE9BQU8sQ0FBQztRQUN2QixJQUFJLENBQUMsY0FBYyxHQUFHLGNBQWMsQ0FBQztJQUN2QyxDQUFDO0lBRUQ7O09BRUc7SUFDSSxLQUFLLENBQUMsWUFBWSxDQUFDLFVBQTJCO1FBQ25ELElBQUksVUFBVSxDQUFDLE1BQU0sRUFBRSxDQUFDO1lBQ3RCLFFBQVEsQ0FBQywyQkFBMkIsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLENBQUM7WUFDcEQsT0FBTztRQUNULENBQUM7UUFFRCxpQ0FBaUM7UUFDakMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsUUFBUSxFQUFFLENBQUM7WUFDdkMsTUFBTSxJQUFJLEtBQUssQ0FBQyxrQ0FBa0MsQ0FBQyxDQUFDO1FBQ3RELENBQUM7UUFFRCxNQUFNLENBQUMsZ0JBQWdCLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxDQUFDO1FB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
|