2025-10-24 08:09:29 +00:00
|
|
|
/**
|
2025-10-28 18:51:33 +00:00
|
|
|
* STARTTLS Implementation using Deno Native TLS
|
|
|
|
|
* Uses Deno.startTls() for reliable TLS upgrades
|
2025-10-24 08:09:29 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import * as plugins from '../../../plugins.ts';
|
|
|
|
|
import { SmtpLogger } from './utils/logging.ts';
|
|
|
|
|
import { getSocketDetails } from './utils/helpers.ts';
|
2025-10-28 18:51:33 +00:00
|
|
|
import { ConnectionWrapper } from './utils/connection-wrapper.ts';
|
2025-10-24 08:09:29 +00:00
|
|
|
import type { ISmtpSession, ISessionManager, IConnectionManager } from './interfaces.ts';
|
|
|
|
|
import { SmtpState } from '../interfaces.ts';
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-28 18:51:33 +00:00
|
|
|
* Perform STARTTLS using Deno's native TLS implementation
|
|
|
|
|
* This replaces the broken Node.js TLS compatibility layer
|
2025-10-24 08:09:29 +00:00
|
|
|
*/
|
|
|
|
|
export async function performStartTLS(
|
|
|
|
|
socket: plugins.net.Socket,
|
|
|
|
|
options: {
|
|
|
|
|
key: string;
|
|
|
|
|
cert: string;
|
|
|
|
|
ca?: string;
|
|
|
|
|
session?: ISmtpSession;
|
|
|
|
|
sessionManager?: ISessionManager;
|
|
|
|
|
connectionManager?: IConnectionManager;
|
2025-10-28 18:51:33 +00:00
|
|
|
onSuccess?: (tlsSocket: plugins.tls.TLSSocket | ConnectionWrapper) => void;
|
2025-10-24 08:09:29 +00:00
|
|
|
onFailure?: (error: Error) => void;
|
|
|
|
|
updateSessionState?: (session: ISmtpSession, state: SmtpState) => void;
|
|
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
): Promise<plugins.tls.TLSSocket | ConnectionWrapper | undefined> {
|
|
|
|
|
return new Promise<plugins.tls.TLSSocket | ConnectionWrapper | undefined>(async (resolve) => {
|
2025-10-24 08:09:29 +00:00
|
|
|
try {
|
|
|
|
|
const socketDetails = getSocketDetails(socket);
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
SmtpLogger.info('Starting Deno-native STARTTLS upgrade process', {
|
2025-10-24 08:09:29 +00:00
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort
|
|
|
|
|
});
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
// Check if this is a ConnectionWrapper (Deno.Conn based)
|
|
|
|
|
if (socket instanceof ConnectionWrapper) {
|
|
|
|
|
SmtpLogger.info('Using Deno-native STARTTLS implementation for ConnectionWrapper');
|
|
|
|
|
|
|
|
|
|
// Get the underlying Deno.Conn
|
|
|
|
|
const denoConn = socket.getDenoConn();
|
|
|
|
|
|
|
|
|
|
// Set up timeout for TLS handshake
|
|
|
|
|
const handshakeTimeout = 30000; // 30 seconds
|
|
|
|
|
const timeoutId = setTimeout(() => {
|
|
|
|
|
const error = new Error('TLS handshake timed out');
|
|
|
|
|
SmtpLogger.error(error.message, {
|
|
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (options.onFailure) {
|
|
|
|
|
options.onFailure(error);
|
2025-10-24 08:09:29 +00:00
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
resolve(undefined);
|
|
|
|
|
}, handshakeTimeout);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
// Write cert and key to temporary files for Deno.startTls()
|
|
|
|
|
const tempDir = await Deno.makeTempDir();
|
|
|
|
|
const certFile = `${tempDir}/cert.pem`;
|
|
|
|
|
const keyFile = `${tempDir}/key.pem`;
|
|
|
|
|
|
2025-10-24 08:09:29 +00:00
|
|
|
try {
|
2025-10-28 18:51:33 +00:00
|
|
|
await Deno.writeTextFile(certFile, options.cert);
|
|
|
|
|
await Deno.writeTextFile(keyFile, options.key);
|
|
|
|
|
|
|
|
|
|
// Upgrade connection to TLS using Deno's native API
|
|
|
|
|
const tlsConn = await Deno.startTls(denoConn, {
|
|
|
|
|
hostname: 'localhost', // Server-side TLS doesn't need hostname validation
|
|
|
|
|
certFile,
|
|
|
|
|
keyFile,
|
|
|
|
|
alpnProtocols: ['smtp'],
|
2025-10-24 08:09:29 +00:00
|
|
|
});
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
SmtpLogger.info('TLS upgrade successful via Deno-native STARTTLS', {
|
2025-10-24 08:09:29 +00:00
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
2025-10-28 18:51:33 +00:00
|
|
|
remotePort: socketDetails.remotePort
|
2025-10-24 08:09:29 +00:00
|
|
|
});
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
// Replace the underlying connection in the wrapper
|
|
|
|
|
socket.replaceConnection(tlsConn);
|
|
|
|
|
|
|
|
|
|
// Update socket mapping in session manager
|
|
|
|
|
if (options.sessionManager) {
|
|
|
|
|
// Socket wrapper remains the same, just upgraded to TLS
|
|
|
|
|
const socketReplaced = options.sessionManager.replaceSocket(socket as any, socket as any);
|
|
|
|
|
if (!socketReplaced) {
|
|
|
|
|
SmtpLogger.warn('Socket already tracked in session manager', {
|
|
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Re-attach event handlers from connection manager if needed
|
|
|
|
|
if (options.connectionManager) {
|
|
|
|
|
try {
|
|
|
|
|
options.connectionManager.setupSocketEventHandlers(socket as any);
|
|
|
|
|
SmtpLogger.debug('Successfully re-attached connection manager event handlers to TLS socket', {
|
|
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort
|
|
|
|
|
});
|
|
|
|
|
} catch (handlerError) {
|
|
|
|
|
SmtpLogger.error('Failed to re-attach event handlers to TLS socket after STARTTLS', {
|
|
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort,
|
|
|
|
|
error: handlerError instanceof Error ? handlerError : new Error(String(handlerError))
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update session if provided
|
|
|
|
|
if (options.session) {
|
|
|
|
|
// Update session properties to indicate TLS is active
|
|
|
|
|
options.session.useTLS = true;
|
|
|
|
|
options.session.secure = true;
|
|
|
|
|
|
|
|
|
|
// Reset session state as required by RFC 3207
|
|
|
|
|
// After STARTTLS, client must issue a new EHLO
|
|
|
|
|
if (options.updateSessionState) {
|
|
|
|
|
options.updateSessionState(options.session, SmtpState.GREETING);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call success callback if provided
|
|
|
|
|
if (options.onSuccess) {
|
|
|
|
|
options.onSuccess(socket);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Success - return the wrapper with upgraded TLS connection
|
|
|
|
|
resolve(socket);
|
|
|
|
|
|
|
|
|
|
} finally {
|
|
|
|
|
// Clean up temporary files
|
|
|
|
|
try {
|
|
|
|
|
await Deno.remove(tempDir, { recursive: true });
|
|
|
|
|
} catch {
|
|
|
|
|
// Ignore cleanup errors
|
|
|
|
|
}
|
2025-10-24 08:09:29 +00:00
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
} catch (tlsError) {
|
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
|
|
|
|
|
|
const error = tlsError instanceof Error ? tlsError : new Error(String(tlsError));
|
|
|
|
|
SmtpLogger.error(`Deno TLS upgrade failed: ${error.message}`, {
|
|
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort,
|
|
|
|
|
error,
|
|
|
|
|
stack: error.stack
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (options.onFailure) {
|
|
|
|
|
options.onFailure(error);
|
2025-10-24 08:09:29 +00:00
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
resolve(undefined);
|
2025-10-24 08:09:29 +00:00
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
} else {
|
|
|
|
|
// Fallback: This should not happen since all connections are now ConnectionWrapper
|
|
|
|
|
SmtpLogger.error('STARTTLS called on non-ConnectionWrapper socket - this should not happen', {
|
|
|
|
|
socketType: socket.constructor.name,
|
|
|
|
|
remoteAddress: socketDetails.remoteAddress,
|
|
|
|
|
remotePort: socketDetails.remotePort
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const error = new Error('STARTTLS requires ConnectionWrapper (Deno.Conn based socket)');
|
|
|
|
|
if (options.onFailure) {
|
|
|
|
|
options.onFailure(error);
|
2025-10-24 08:09:29 +00:00
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
|
|
|
|
|
resolve(undefined);
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-24 08:09:29 +00:00
|
|
|
} catch (error) {
|
2025-10-28 18:51:33 +00:00
|
|
|
SmtpLogger.error(`Unexpected error in Deno-native STARTTLS: ${error instanceof Error ? error.message : String(error)}`, {
|
2025-10-24 08:09:29 +00:00
|
|
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
|
|
|
stack: error instanceof Error ? error.stack : 'No stack trace available'
|
|
|
|
|
});
|
2025-10-28 18:51:33 +00:00
|
|
|
|
2025-10-24 08:09:29 +00:00
|
|
|
if (options.onFailure) {
|
|
|
|
|
options.onFailure(error instanceof Error ? error : new Error(String(error)));
|
|
|
|
|
}
|
2025-10-28 18:51:33 +00:00
|
|
|
|
2025-10-24 08:09:29 +00:00
|
|
|
resolve(undefined);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-10-28 18:51:33 +00:00
|
|
|
}
|