/** * Connection Wrapper Utility * Wraps Deno.Conn to provide Node.js net.Socket-compatible interface * This allows the SMTP server to use Deno's native networking while maintaining * compatibility with existing Socket-based code */ import { EventEmitter } from '../../../../plugins.ts'; /** * Wraps a Deno.Conn or Deno.TlsConn to provide a Node.js Socket-compatible interface */ export class ConnectionWrapper extends EventEmitter { private conn: Deno.Conn | Deno.TlsConn; private _destroyed = false; private _reading = false; private _remoteAddr: Deno.NetAddr; private _localAddr: Deno.NetAddr; constructor(conn: Deno.Conn | Deno.TlsConn) { super(); this.conn = conn; this._remoteAddr = conn.remoteAddr as Deno.NetAddr; this._localAddr = conn.localAddr as Deno.NetAddr; // Start reading from the connection this._reading = true; this._startReading(); } /** * Get remote address (Node.js net.Socket compatible) */ get remoteAddress(): string { return this._remoteAddr.hostname; } /** * Get remote port (Node.js net.Socket compatible) */ get remotePort(): number { return this._remoteAddr.port; } /** * Get local address (Node.js net.Socket compatible) */ get localAddress(): string { return this._localAddr.hostname; } /** * Get local port (Node.js net.Socket compatible) */ get localPort(): number { return this._localAddr.port; } /** * Check if connection is destroyed */ get destroyed(): boolean { return this._destroyed; } /** * Check ready state (Node.js compatible) */ get readyState(): string { if (this._destroyed) { return 'closed'; } return 'open'; } /** * Check if writable (Node.js compatible) */ get writable(): boolean { return !this._destroyed; } /** * Check if this is a secure (TLS) connection */ get encrypted(): boolean { return 'handshake' in this.conn; // TlsConn has handshake property } /** * Write data to the connection (Node.js net.Socket compatible) */ write(data: string | Uint8Array, encoding?: string | ((err?: Error) => void), callback?: (err?: Error) => void): boolean { // Handle overloaded signatures (encoding is optional) if (typeof encoding === 'function') { callback = encoding; encoding = undefined; } if (this._destroyed) { const error = new Error('Connection is destroyed'); if (callback) { setTimeout(() => callback(error), 0); } return false; } const bytes = typeof data === 'string' ? new TextEncoder().encode(data) : data; // Use a promise-based approach that Node.js compatibility expects // Write happens async but we return true immediately (buffered) this.conn.write(bytes) .then(() => { if (callback) { callback(); } }) .catch((err) => { const error = err instanceof Error ? err : new Error(String(err)); if (callback) { callback(error); } else { this.emit('error', error); } }); return true; } /** * End the connection (Node.js net.Socket compatible) */ end(data?: string | Uint8Array, encoding?: string, callback?: () => void): void { if (data) { this.write(data, encoding, () => { this.destroy(); if (callback) callback(); }); } else { this.destroy(); if (callback) callback(); } } /** * Destroy the connection (Node.js net.Socket compatible) */ destroy(error?: Error): void { if (this._destroyed) { return; } this._destroyed = true; this._reading = false; try { this.conn.close(); } catch (closeError) { // Ignore close errors } if (error) { this.emit('error', error); } this.emit('close', !!error); } /** * Set TCP_NODELAY option (Node.js net.Socket compatible) */ setNoDelay(noDelay: boolean = true): this { try { // @ts-ignore - Deno.Conn has setNoDelay if (typeof this.conn.setNoDelay === 'function') { // @ts-ignore this.conn.setNoDelay(noDelay); } } catch { // Ignore if not supported } return this; } /** * Set keep-alive option (Node.js net.Socket compatible) */ setKeepAlive(enable: boolean = true, initialDelay?: number): this { try { // @ts-ignore - Deno.Conn has setKeepAlive if (typeof this.conn.setKeepAlive === 'function') { // @ts-ignore this.conn.setKeepAlive(enable); } } catch { // Ignore if not supported } return this; } /** * Set timeout (Node.js net.Socket compatible) */ setTimeout(timeout: number, callback?: () => void): this { // Deno doesn't have built-in socket timeout, but we can implement it // For now, just accept the call without error (most timeout handling is done elsewhere) if (callback) { // If callback provided, we could set up a timer, but for now just ignore // The SMTP server handles timeouts at a higher level } return this; } /** * Pause reading from the connection */ pause(): this { this._reading = false; return this; } /** * Resume reading from the connection */ resume(): this { if (!this._reading && !this._destroyed) { this._reading = true; this._startReading(); } return this; } /** * Get the underlying Deno.Conn */ getDenoConn(): Deno.Conn | Deno.TlsConn { return this.conn; } /** * Replace the underlying connection (for STARTTLS upgrade) */ replaceConnection(newConn: Deno.TlsConn): void { this.conn = newConn; this._remoteAddr = newConn.remoteAddr as Deno.NetAddr; this._localAddr = newConn.localAddr as Deno.NetAddr; // Restart reading from the new TLS connection if (!this._destroyed) { this._reading = true; this._startReading(); } } /** * Internal method to read data from the connection */ private async _startReading(): Promise { if (!this._reading || this._destroyed) { return; } try { const buffer = new Uint8Array(4096); while (this._reading && !this._destroyed) { const n = await this.conn.read(buffer); if (n === null) { // EOF this._destroyed = true; this.emit('end'); this.emit('close', false); break; } const data = buffer.subarray(0, n); this.emit('data', data); } } catch (error) { if (!this._destroyed) { this._destroyed = true; this.emit('error', error instanceof Error ? error : new Error(String(error))); this.emit('close', true); } } } /** * Remove all listeners (cleanup helper) */ removeAllListeners(event?: string): this { super.removeAllListeners(event); return this; } }