feat: Implement Deno-native STARTTLS handler and connection wrapper

- Refactored STARTTLS implementation to use Deno's native TLS via Deno.startTls().
- Introduced ConnectionWrapper to provide a Node.js net.Socket-compatible interface for Deno.Conn and Deno.TlsConn.
- Updated TlsHandler to utilize the new STARTTLS implementation.
- Added comprehensive SMTP authentication tests for PLAIN and LOGIN mechanisms.
- Implemented rate limiting tests for SMTP server connections and commands.
- Enhanced error handling and logging throughout the STARTTLS and connection upgrade processes.
This commit is contained in:
2025-10-28 18:51:33 +00:00
parent 9cd15342e0
commit 6523c55516
14 changed files with 1328 additions and 429 deletions

View File

@@ -12,7 +12,10 @@ export interface ITestServerConfig {
port: number;
hostname?: string;
tlsEnabled?: boolean;
secure?: boolean; // Direct TLS server (like SMTPS on port 465)
authRequired?: boolean;
authMethods?: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
requireTLS?: boolean; // Whether to require TLS for AUTH (default: true)
timeout?: number;
testCertPath?: string;
testKeyPath?: string;
@@ -176,7 +179,8 @@ export async function startTestServer(config: ITestServerConfig): Promise<ITestS
auth: serverConfig.authRequired
? ({
required: true,
methods: ['PLAIN', 'LOGIN'] as ('PLAIN' | 'LOGIN' | 'OAUTH2')[],
methods: (serverConfig.authMethods || ['PLAIN', 'LOGIN']) as ('PLAIN' | 'LOGIN' | 'OAUTH2')[],
requireTLS: serverConfig.requireTLS !== undefined ? serverConfig.requireTLS : true, // Default: require TLS for AUTH
validateUser: async (username: string, password: string) => {
// Test server accepts these credentials
return username === 'testuser' && password === 'testpass';

View File

@@ -359,3 +359,36 @@ export async function retryOperation<T>(
throw lastError!;
}
/**
* Upgrade SMTP connection to TLS using STARTTLS command
* @param conn - Active SMTP connection
* @param hostname - Server hostname for TLS verification
* @returns Upgraded TLS connection
*/
export async function upgradeToTls(conn: Deno.Conn, hostname: string = 'localhost'): Promise<Deno.TlsConn> {
const encoder = new TextEncoder();
// Send STARTTLS command
await conn.write(encoder.encode('STARTTLS\r\n'));
// Read response
const response = await readSmtpResponse(conn);
// Check for 220 Ready to start TLS
if (!response.startsWith('220')) {
throw new Error(`STARTTLS failed: ${response}`);
}
// Read test certificate for self-signed cert validation
const certPath = new URL('../../test/fixtures/test-cert.pem', import.meta.url).pathname;
const certPem = await Deno.readTextFile(certPath);
// Upgrade connection to TLS with certificate options
const tlsConn = await Deno.startTls(conn, {
hostname,
caCerts: [certPem], // Accept self-signed test certificate
});
return tlsConn;
}