feat: Update Nodemailer transport to support optional host and apiKey parameters, enhance error handling, and improve example usage
This commit is contained in:
		
							parent
							
								
									26d11289ea
								
							
						
					
					
						commit
						79b8013b5e
					
				
					 4 changed files with 395 additions and 344 deletions
				
			
		
							
								
								
									
										35
									
								
								CLAUDE.md
									
										
									
									
									
								
							
							
						
						
									
										35
									
								
								CLAUDE.md
									
										
									
									
									
								
							|  | @ -8,27 +8,34 @@ This is **@siwatsystem/mxrelay-consumer**, a TypeScript client library for SMTP | |||
| 
 | ||||
| ## Development Commands | ||||
| 
 | ||||
| **This project uses Bun as the primary runtime and package manager. TypeScript files can be run directly without building.** | ||||
| 
 | ||||
| ### Build Commands | ||||
| - `npm run build` - Build all formats (CommonJS, ESM, and type declarations) | ||||
| - `npm run build:cjs` - Build CommonJS format only | ||||
| - `npm run build:esm` - Build ES modules format only   | ||||
| - `npm run build:types` - Build TypeScript declarations only | ||||
| - `npm run dev` - Watch mode for development | ||||
| - `npm run clean` - Remove build artifacts | ||||
| - `bun run build` - Build all formats (CommonJS, ESM, and type declarations) | ||||
| - `bun run build:cjs` - Build CommonJS format only | ||||
| - `bun run build:esm` - Build ES modules format only   | ||||
| - `bun run build:types` - Build TypeScript declarations only | ||||
| - `bun run dev` - Watch mode for development | ||||
| - `bun run clean` - Remove build artifacts | ||||
| 
 | ||||
| ### Testing Commands | ||||
| - `npm test` - Run all tests | ||||
| - `npm run test:watch` - Run tests in watch mode | ||||
| - `npm run test:coverage` - Run tests with coverage report | ||||
| - `bun test` - Run all tests | ||||
| - `bun run test:watch` - Run tests in watch mode | ||||
| - `bun run test:coverage` - Run tests with coverage report | ||||
| 
 | ||||
| ### Code Quality Commands | ||||
| - `npm run lint` - Lint TypeScript files | ||||
| - `npm run lint:fix` - Lint and auto-fix issues | ||||
| - `npm run format` - Format code with Prettier | ||||
| - `bun run lint` - Lint TypeScript files | ||||
| - `bun run lint:fix` - Lint and auto-fix issues | ||||
| - `bun run format` - Format code with Prettier | ||||
| 
 | ||||
| ### Example Commands | ||||
| - `npm run example:basic` - Run basic usage example | ||||
| - `npm run example:queue` - Run queue management example | ||||
| - `bun run examples/nodemailer-transport.ts` - Run Nodemailer transport example directly | ||||
| - `bun run example:basic` - Run basic usage example | ||||
| - `bun run example:queue` - Run queue management example | ||||
| 
 | ||||
| ### Running TypeScript Directly | ||||
| - `bun run src/index.ts` - Run source files directly without building | ||||
| - `bun run examples/any-example.ts` - Run any example file directly | ||||
| 
 | ||||
| ## Architecture | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										521
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										521
									
								
								README.md
									
										
									
									
									
								
							|  | @ -1,142 +1,169 @@ | |||
| # @siwatsystem/smtp-ws-relay-client | ||||
| # @siwatsystem/mxrelay-consumer | ||||
| 
 | ||||
| [](https://badge.fury.io/js/@siwatsystem%2Fsmtp-ws-relay-client) | ||||
| [](http://www.typescriptlang.org/) | ||||
| [](https://opensource.org/licenses/MIT) | ||||
| 
 | ||||
| A production-ready TypeScript client library for SMTP over WebSocket protocol with intelligent queue management, automatic reconnection, and comprehensive error handling. | ||||
| A production-ready TypeScript client library for SMTP over WebSocket protocol with intelligent queue management, automatic reconnection, and comprehensive error handling. Includes full Nodemailer transport compatibility. | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| 🚀 **Intelligent Queue Management** | ||||
| **Intelligent Queue Management** | ||||
| - Automatic WebSocket connection when messages are queued | ||||
| - Priority-based message processing | ||||
| - Configurable batch processing | ||||
| - Queue size limits and overflow protection | ||||
| - Priority-based message processing (CRITICAL > HIGH > NORMAL > LOW) | ||||
| - Configurable queue limits and overflow protection | ||||
| - Auto-disconnect when queue is empty | ||||
| 
 | ||||
| 🔄 **Robust Connection Handling**  | ||||
| **Robust Connection Handling**  | ||||
| - Automatic reconnection with exponential backoff | ||||
| - Connection state management | ||||
| - Heartbeat monitoring | ||||
| - Graceful connection lifecycle | ||||
| - Connection state management and lifecycle | ||||
| - Heartbeat monitoring and timeout handling | ||||
| - Graceful connection recovery | ||||
| 
 | ||||
| ⚡ **High Performance** | ||||
| - Efficient SMTP channel cycling | ||||
| - Concurrent message processing support   | ||||
| - Optimized resource usage | ||||
| - Minimal memory footprint | ||||
| **High Performance** | ||||
| - Efficient SMTP channel cycling per message | ||||
| - Minimal resource usage with smart connection management | ||||
| - Concurrent message processing support | ||||
| - Optimized WebSocket communication | ||||
| 
 | ||||
| 🛡️ **Enterprise-Grade Reliability** | ||||
| - Comprehensive error handling and classification | ||||
| **Enterprise-Grade Reliability** | ||||
| - Comprehensive SMTP error handling with meaningful messages | ||||
| - Timeout management for all operations | ||||
| - Retry logic with configurable attempts | ||||
| - Graceful degradation and recovery | ||||
| - Structured error classification | ||||
| 
 | ||||
| 📊 **Monitoring & Observability** | ||||
| - Real-time statistics and metrics | ||||
| - Structured logging with configurable levels | ||||
| - Event-driven architecture for monitoring | ||||
| - Performance tracking and analytics | ||||
| **Nodemailer Integration** | ||||
| - Full Nodemailer transport compatibility | ||||
| - Transparent bridge for all email features | ||||
| - Support for attachments, HTML, multipart messages | ||||
| - Standard Nodemailer API compatibility | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| ```bash | ||||
| npm install @siwatsystem/smtp-ws-relay-client | ||||
| npm install @siwatsystem/mxrelay-consumer | ||||
| # or | ||||
| bun add @siwatsystem/mxrelay-consumer | ||||
| ``` | ||||
| 
 | ||||
| ## Quick Start | ||||
| 
 | ||||
| ```typescript | ||||
| import { SMTPOverWSClient, MessagePriority } from '@siwatsystem/smtp-ws-relay-client'; | ||||
| ### Direct Client Usage | ||||
| 
 | ||||
| ```typescript | ||||
| import { SMTPOverWSClient } from '@siwatsystem/mxrelay-consumer'; | ||||
| 
 | ||||
| // Create client instance | ||||
| const client = new SMTPOverWSClient({ | ||||
|     url: 'ws://your-smtp-relay-server:3000/smtp', | ||||
|     url: 'wss://api.siwatsystem.com/smtp', | ||||
|     apiKey: 'your-api-key', | ||||
|     debug: true | ||||
| }); | ||||
| 
 | ||||
| // Send SMTP commands | ||||
| // Send SMTP commands directly | ||||
| try { | ||||
|     const response1 = await client.sendSMTPCommand('EHLO example.com\\r\\n'); | ||||
|     const response2 = await client.sendSMTPCommand('MAIL FROM: <test@example.com>\\r\\n'); | ||||
|     const response3 = await client.sendSMTPCommand('RCPT TO: <recipient@example.com>\\r\\n'); | ||||
|      | ||||
|     console.log('SMTP responses:', [response1, response2, response3]); | ||||
|     const response = await client.sendSMTPCommand(` | ||||
|         MAIL FROM: <sender@example.com> | ||||
|         RCPT TO: <recipient@example.com> | ||||
|         DATA | ||||
|         Subject: Test Email | ||||
|          | ||||
|         Hello from SMTP over WebSocket! | ||||
|         . | ||||
|         QUIT | ||||
|     `); | ||||
|     console.log('Email sent:', response); | ||||
| } catch (error) { | ||||
|     console.error('SMTP error:', error); | ||||
|     console.error('SMTP error:', error.message); | ||||
| } finally { | ||||
|     await client.shutdown(); | ||||
| } | ||||
| 
 | ||||
| // Graceful shutdown | ||||
| await client.shutdown(); | ||||
| ``` | ||||
| 
 | ||||
| ## Configuration Options | ||||
| ### Nodemailer Transport | ||||
| 
 | ||||
| ```typescript | ||||
| import nodemailer from 'nodemailer'; | ||||
| import { createTransport } from '@siwatsystem/mxrelay-consumer'; | ||||
| 
 | ||||
| // Create transport (uses defaults: api.siwatsystem.com:443 secure) | ||||
| const transport = createTransport('your-api-key'); | ||||
| 
 | ||||
| // Or with custom options | ||||
| const transport = createTransport('your-api-key', { | ||||
|     host: 'custom.server.com', | ||||
|     port: 80, | ||||
|     secure: false, | ||||
|     debug: true | ||||
| }); | ||||
| 
 | ||||
| const transporter = nodemailer.createTransporter(transport); | ||||
| 
 | ||||
| // Send email using standard Nodemailer API | ||||
| const info = await transporter.sendMail({ | ||||
|     from: 'sender@example.com', | ||||
|     to: 'recipient@example.com', | ||||
|     subject: 'Test Email via SMTP WebSocket', | ||||
|     text: 'Plain text version', | ||||
|     html: '<h1>HTML version</h1>', | ||||
|     attachments: [ | ||||
|         { | ||||
|             filename: 'document.pdf', | ||||
|             path: './document.pdf' | ||||
|         } | ||||
|     ] | ||||
| }); | ||||
| 
 | ||||
| console.log('Message sent:', info.messageId); | ||||
| await transport.close(); | ||||
| ``` | ||||
| 
 | ||||
| ## Configuration | ||||
| 
 | ||||
| ### Client Configuration | ||||
| 
 | ||||
| ```typescript | ||||
| interface SMTPClientConfig { | ||||
|     /** WebSocket server URL */ | ||||
|     url: string; | ||||
|      | ||||
|     /** API key for authentication */ | ||||
|     apiKey: string; | ||||
|      | ||||
|     /** Interval between reconnection attempts (default: 5000ms) */ | ||||
|     reconnectInterval?: number; | ||||
|      | ||||
|     /** Maximum reconnection attempts (default: 10) */ | ||||
|     maxReconnectAttempts?: number; | ||||
|      | ||||
|     /** Authentication timeout (default: 30000ms) */ | ||||
|     authTimeout?: number; | ||||
|      | ||||
|     /** Channel operation timeout (default: 10000ms) */ | ||||
|     channelTimeout?: number; | ||||
|      | ||||
|     /** Message timeout (default: 60000ms) */ | ||||
|     messageTimeout?: number; | ||||
|      | ||||
|     /** Maximum concurrent messages (default: 1) */ | ||||
|     maxConcurrentMessages?: number; | ||||
|      | ||||
|     /** Enable debug logging (default: false) */ | ||||
|     debug?: boolean; | ||||
|      | ||||
|     /** Custom logger implementation */ | ||||
|     logger?: Logger; | ||||
|      | ||||
|     /** Connection heartbeat interval (default: 30000ms) */ | ||||
|     heartbeatInterval?: number; | ||||
|      | ||||
|     /** Queue processing batch size (default: 10) */ | ||||
|     batchSize?: number; | ||||
|      | ||||
|     /** Maximum queue size (default: 1000) */ | ||||
|     maxQueueSize?: number; | ||||
|     url: string;                        // WebSocket server URL | ||||
|     apiKey: string;                     // Authentication API key | ||||
|     debug?: boolean;                    // Enable debug logging (default: false) | ||||
|     maxQueueSize?: number;              // Queue capacity limit (default: 1000) | ||||
|     reconnectInterval?: number;         // Reconnect delay (default: 5000ms) | ||||
|     maxReconnectAttempts?: number;      // Max retry attempts (default: 10) | ||||
|     authTimeout?: number;               // Auth timeout (default: 30000ms) | ||||
|     channelTimeout?: number;            // Channel timeout (default: 10000ms) | ||||
|     messageTimeout?: number;            // Message timeout (default: 60000ms) | ||||
|     heartbeatInterval?: number;         // Heartbeat interval (default: 30000ms) | ||||
|     maxConcurrentMessages?: number;     // Concurrent limit (default: 1) | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Transport Configuration | ||||
| 
 | ||||
| ```typescript | ||||
| interface TransportOptions { | ||||
|     host?: string;                      // Server host (default: 'api.siwatsystem.com') | ||||
|     port?: number;                      // Server port (default: 443) | ||||
|     secure?: boolean;                   // Use wss:// (default: true) | ||||
|     debug?: boolean;                    // Enable debug mode (default: false) | ||||
|     // ... other SMTPClientConfig options | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Advanced Usage | ||||
| 
 | ||||
| ### Priority-Based Message Queuing | ||||
| ### Priority-Based Messaging | ||||
| 
 | ||||
| ```typescript | ||||
| import { MessagePriority } from '@siwatsystem/smtp-ws-relay-client'; | ||||
| import { MessagePriority } from '@siwatsystem/mxrelay-consumer'; | ||||
| 
 | ||||
| // High priority message (processed first) | ||||
| await client.sendSMTPCommand('URGENT EMAIL DATA\\r\\n', { | ||||
| // High priority (processed first) | ||||
| await client.sendSMTPCommand('URGENT EMAIL DATA', { | ||||
|     priority: MessagePriority.HIGH, | ||||
|     timeout: 30000 | ||||
| }); | ||||
| 
 | ||||
| // Normal priority message | ||||
| await client.sendSMTPCommand('NORMAL EMAIL DATA\\r\\n', { | ||||
|     priority: MessagePriority.NORMAL | ||||
| }); | ||||
| 
 | ||||
| // Low priority message (processed last) | ||||
| await client.sendSMTPCommand('NEWSLETTER DATA\\r\\n', { | ||||
|     priority: MessagePriority.LOW | ||||
| // Critical priority (highest) | ||||
| await client.sendSMTPCommand('CRITICAL ALERT EMAIL', { | ||||
|     priority: MessagePriority.CRITICAL | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
|  | @ -144,17 +171,9 @@ await client.sendSMTPCommand('NEWSLETTER DATA\\r\\n', { | |||
| 
 | ||||
| ```typescript | ||||
| // Connection events | ||||
| client.on('connected', () => { | ||||
|     console.log('WebSocket connected'); | ||||
| }); | ||||
| 
 | ||||
| client.on('authenticated', () => { | ||||
|     console.log('Authentication successful'); | ||||
| }); | ||||
| 
 | ||||
| client.on('disconnected', (reason) => { | ||||
|     console.log('Disconnected:', reason); | ||||
| }); | ||||
| client.on('connected', () => console.log('WebSocket connected')); | ||||
| client.on('authenticated', () => console.log('Authentication successful')); | ||||
| client.on('disconnected', () => console.log('Connection lost')); | ||||
| 
 | ||||
| // Queue events | ||||
| client.on('messageQueued', (messageId, queueSize) => { | ||||
|  | @ -165,152 +184,140 @@ client.on('messageProcessed', (messageId, responseTime) => { | |||
|     console.log(`Message ${messageId} processed in ${responseTime}ms`); | ||||
| }); | ||||
| 
 | ||||
| client.on('queueProcessingStarted', (queueSize) => { | ||||
|     console.log(`Processing ${queueSize} queued messages`); | ||||
| }); | ||||
| 
 | ||||
| // Error events | ||||
| client.on('error', (error) => { | ||||
|     console.error('Client error:', error); | ||||
| }); | ||||
| 
 | ||||
| client.on('messageFailed', (messageId, error) => { | ||||
|     console.error(`Message ${messageId} failed:`, error); | ||||
| }); | ||||
| client.on('error', (error) => console.error('Client error:', error)); | ||||
| ``` | ||||
| 
 | ||||
| ### Statistics and Monitoring | ||||
| 
 | ||||
| ```typescript | ||||
| // Get real-time statistics | ||||
| const stats = client.getStats(); | ||||
| console.log('Client Statistics:', { | ||||
|     messagesQueued: stats.messagesQueued, | ||||
|     messagesProcessed: stats.messagesProcessed, | ||||
|     messagesFailed: stats.messagesFailed, | ||||
|     averageResponseTime: stats.averageResponseTime, | ||||
|     connectionUptime: stats.connectionUptime, | ||||
|     queueSize: stats.queueSize | ||||
| }); | ||||
| 
 | ||||
| // Monitor connection state | ||||
| console.log('Current state:', client.getConnectionState()); | ||||
| console.log('Queue size:', client.getQueueSize()); | ||||
| ``` | ||||
| 
 | ||||
| ### Custom Logger | ||||
| 
 | ||||
| ```typescript | ||||
| import { Logger } from '@siwatsystem/smtp-ws-relay-client'; | ||||
| 
 | ||||
| class CustomLogger implements Logger { | ||||
|     debug(message: string, ...args: any[]): void { | ||||
|         // Send to your logging service | ||||
|         console.debug(`[DEBUG] ${message}`, ...args); | ||||
|     } | ||||
|      | ||||
|     info(message: string, ...args: any[]): void { | ||||
|         console.info(`[INFO] ${message}`, ...args); | ||||
|     } | ||||
|      | ||||
|     warn(message: string, ...args: any[]): void { | ||||
|         console.warn(`[WARN] ${message}`, ...args); | ||||
|     } | ||||
|      | ||||
|     error(message: string, ...args: any[]): void { | ||||
|         console.error(`[ERROR] ${message}`, ...args); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const client = new SMTPOverWSClient({ | ||||
|     url: 'ws://localhost:3000/smtp', | ||||
|     apiKey: 'your-api-key', | ||||
|     logger: new CustomLogger() | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ## Error Handling | ||||
| 
 | ||||
| The library provides comprehensive error classification: | ||||
| ### SMTP Error Detection | ||||
| 
 | ||||
| The transport properly detects and categorizes SMTP errors: | ||||
| 
 | ||||
| ```typescript | ||||
| try { | ||||
|     await transporter.sendMail({ | ||||
|         from: 'unauthorized@domain.com',  // Invalid sender | ||||
|         to: 'recipient@example.com', | ||||
|         subject: 'Test' | ||||
|     }); | ||||
| } catch (error) { | ||||
|     console.error('SMTP Error:', error.message); | ||||
|     // Output: "Sender not authorized: Sender domain not authorized for your IP or subnet" | ||||
|      | ||||
|     console.log('Error details:', { | ||||
|         smtpCode: error.context.smtpCode,        // "550" | ||||
|         rejectedRecipients: error.context.rejectedRecipients | ||||
|     }); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Error Classification | ||||
| 
 | ||||
| ```typescript | ||||
| import {  | ||||
|     ConnectionError,  | ||||
|     AuthenticationError,  | ||||
|     ChannelError,  | ||||
|     TimeoutError, | ||||
|     QueueError, | ||||
|     MessageError  | ||||
| } from '@siwatsystem/smtp-ws-relay-client'; | ||||
|     MessageError, | ||||
|     TimeoutError  | ||||
| } from '@siwatsystem/mxrelay-consumer'; | ||||
| 
 | ||||
| try { | ||||
|     await client.sendSMTPCommand('EHLO example.com\\r\\n'); | ||||
|     await client.sendSMTPCommand('MAIL FROM: <test@example.com>'); | ||||
| } catch (error) { | ||||
|     if (error instanceof ConnectionError) { | ||||
|         console.error('Connection failed:', error.message); | ||||
|     } else if (error instanceof AuthenticationError) { | ||||
|         console.error('Authentication failed:', error.message); | ||||
|     } else if (error instanceof MessageError) { | ||||
|         console.error('SMTP error:', error.message, 'Code:', error.context.smtpCode); | ||||
|     } else if (error instanceof TimeoutError) { | ||||
|         console.error('Operation timed out:', error.message); | ||||
|     } else if (error instanceof QueueError) { | ||||
|         console.error('Queue error:', error.message); | ||||
|     } else if (error instanceof MessageError) { | ||||
|         console.error('Message processing failed:', error.message); | ||||
|     } | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Connection States | ||||
| 
 | ||||
| The client manages several connection states: | ||||
| The client manages connection states automatically: | ||||
| 
 | ||||
| - `DISCONNECTED` - No connection to server | ||||
| - `DISCONNECTED` - No connection | ||||
| - `CONNECTING` - Establishing WebSocket connection   | ||||
| - `CONNECTED` - WebSocket connected, authentication pending | ||||
| - `AUTHENTICATING` - Sending authentication credentials | ||||
| - `AUTHENTICATED` - Ready to open SMTP channels | ||||
| - `AUTHENTICATING` - Sending credentials | ||||
| - `AUTHENTICATED` - Ready for SMTP operations | ||||
| - `CHANNEL_OPENING` - Opening SMTP channel | ||||
| - `CHANNEL_READY` - SMTP channel active, ready for data | ||||
| - `CHANNEL_READY` - SMTP channel active | ||||
| - `CHANNEL_CLOSED` - SMTP channel closed | ||||
| - `CHANNEL_ERROR` - SMTP channel error occurred | ||||
| - `RECONNECTING` - Attempting to reconnect | ||||
| - `RECONNECTING` - Attempting reconnection | ||||
| - `FAILED` - Connection failed, max retries reached | ||||
| 
 | ||||
| ## Protocol Support | ||||
| ## Development | ||||
| 
 | ||||
| This client implements the SMTP over WebSocket protocol specification: | ||||
| This project uses Bun as the primary runtime. TypeScript files can be run directly without building. | ||||
| 
 | ||||
| ### Message Types | ||||
| ### Setup | ||||
| 
 | ||||
| ```bash | ||||
| # Clone repository | ||||
| git clone https://git.siwatsystem.com/siwat/mxrelay-consumer.git | ||||
| cd mxrelay-consumer | ||||
| 
 | ||||
| # Install dependencies | ||||
| bun install | ||||
| 
 | ||||
| # Run examples directly | ||||
| bun run examples/nodemailer-transport.ts | ||||
| 
 | ||||
| # Run with environment variable | ||||
| MXRELAY_API_KEY=your-key bun run examples/nodemailer-transport.ts | ||||
| ``` | ||||
| 
 | ||||
| ### Build & Test | ||||
| 
 | ||||
| ```bash | ||||
| # Build (optional - bun runs TypeScript directly) | ||||
| bun run build | ||||
| 
 | ||||
| # Run tests | ||||
| bun test | ||||
| 
 | ||||
| # Run linting | ||||
| bun run lint | ||||
| 
 | ||||
| # Format code | ||||
| bun run format | ||||
| ``` | ||||
| 
 | ||||
| ## Protocol Implementation | ||||
| 
 | ||||
| ### WebSocket Message Types | ||||
| 
 | ||||
| - `AUTHENTICATE` / `AUTHENTICATE_RESPONSE` - Authentication flow | ||||
| - `SMTP_CHANNEL_OPEN` / `SMTP_CHANNEL_READY` - Channel management   | ||||
| - `SMTP_CHANNEL_CLOSED` / `SMTP_CHANNEL_ERROR` - Channel lifecycle | ||||
| - `SMTP_TO_SERVER` / `SMTP_FROM_SERVER` - Data transfer | ||||
| - `SMTP_TO_SERVER` / `SMTP_FROM_SERVER` - SMTP data exchange | ||||
| 
 | ||||
| ### Connection Flow | ||||
| 
 | ||||
| 1. **WebSocket Connection** - Establish WebSocket to relay server | ||||
| 1. **WebSocket Connection** - Connect to relay server | ||||
| 2. **Authentication** - Authenticate using API key | ||||
| 3. **Channel Management** - Open/close SMTP channels per message | ||||
| 3. **Channel Management** - Open SMTP channel per message | ||||
| 4. **Data Transfer** - Exchange SMTP commands and responses | ||||
| 5. **Cleanup** - Close channels and connection when queue empty | ||||
| 
 | ||||
| ## Performance Considerations | ||||
| 
 | ||||
| ### Queue Management | ||||
| - Messages are processed in priority order | ||||
| - Batch processing reduces connection overhead | ||||
| - Automatic queue size management prevents memory issues | ||||
| 
 | ||||
| ### Connection Efficiency | ||||
| - Single WebSocket connection handles multiple SMTP sessions | ||||
| - Intelligent connect/disconnect based on queue state | ||||
| - Connection pooling and reuse optimization | ||||
| 
 | ||||
| ### Memory Usage | ||||
| - Efficient message queuing with cleanup | ||||
| - Automatic resource management | ||||
| - Configurable limits and timeouts | ||||
| 5. **Cleanup** - Close channel and disconnect when queue empty | ||||
| 
 | ||||
| ## Best Practices | ||||
| 
 | ||||
|  | @ -318,41 +325,15 @@ This client implements the SMTP over WebSocket protocol specification: | |||
| 
 | ||||
| ```typescript | ||||
| const client = new SMTPOverWSClient({ | ||||
|     url: 'ws://your-production-server/smtp', | ||||
|     apiKey: process.env.SMTP_RELAY_API_KEY!, | ||||
|     url: 'wss://api.siwatsystem.com/smtp', | ||||
|     apiKey: process.env.MXRELAY_API_KEY, | ||||
|      | ||||
|     // Connection settings | ||||
|     // Production settings | ||||
|     debug: false, | ||||
|     maxQueueSize: 5000, | ||||
|     reconnectInterval: 10000, | ||||
|     maxReconnectAttempts: 5, | ||||
|     heartbeatInterval: 30000, | ||||
|      | ||||
|     // Timeouts | ||||
|     authTimeout: 30000, | ||||
|     channelTimeout: 15000, | ||||
|     messageTimeout: 120000, | ||||
|      | ||||
|     // Queue settings | ||||
|     maxQueueSize: 5000, | ||||
|     batchSize: 50, | ||||
|      | ||||
|     // Monitoring | ||||
|     debug: false, | ||||
|     logger: new ProductionLogger() | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### Error Recovery | ||||
| 
 | ||||
| ```typescript | ||||
| client.on('error', async (error) => { | ||||
|     // Log error to monitoring system | ||||
|     logger.error('SMTP client error', { error: error.toJSON() }); | ||||
|      | ||||
|     // Implement circuit breaker pattern | ||||
|     if (error instanceof ConnectionError && consecutiveErrors > 10) { | ||||
|         await client.shutdown(); | ||||
|         // Implement fallback mechanism | ||||
|     } | ||||
|     messageTimeout: 120000 | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
|  | @ -360,99 +341,51 @@ client.on('error', async (error) => { | |||
| 
 | ||||
| ```typescript | ||||
| process.on('SIGTERM', async () => { | ||||
|     console.log('Shutting down SMTP client...'); | ||||
|     console.log('Shutting down...'); | ||||
|     try { | ||||
|         await client.shutdown(30000); // 30 second timeout | ||||
|         console.log('SMTP client shutdown complete'); | ||||
|         console.log('Shutdown complete'); | ||||
|     } catch (error) { | ||||
|         console.error('Forced shutdown due to timeout'); | ||||
|         console.error('Forced shutdown'); | ||||
|     } | ||||
|     process.exit(0); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ## Development | ||||
| 
 | ||||
| ### Building from Source | ||||
| 
 | ||||
| ```bash | ||||
| # Clone repository | ||||
| git clone https://github.com/siwatsystem/smtp-ws-relay-client.git | ||||
| cd smtp-ws-relay-client | ||||
| 
 | ||||
| # Install dependencies | ||||
| npm install | ||||
| 
 | ||||
| # Build library | ||||
| npm run build | ||||
| 
 | ||||
| # Run tests | ||||
| npm test | ||||
| 
 | ||||
| # Run examples | ||||
| npm run example:basic | ||||
| ``` | ||||
| 
 | ||||
| ### Testing | ||||
| 
 | ||||
| ```bash | ||||
| # Run all tests | ||||
| npm test | ||||
| 
 | ||||
| # Run with coverage | ||||
| npm run test:coverage | ||||
| 
 | ||||
| # Watch mode | ||||
| npm run test:watch | ||||
| ``` | ||||
| 
 | ||||
| ## API Reference | ||||
| 
 | ||||
| ### createTransport(apiKey, options?) | ||||
| 
 | ||||
| Creates a Nodemailer-compatible transport. | ||||
| 
 | ||||
| - `apiKey` - Your API key for authentication | ||||
| - `options` - Optional transport configuration | ||||
| 
 | ||||
| ### SMTPOverWSClient | ||||
| 
 | ||||
| #### Constructor | ||||
| - `new SMTPOverWSClient(config: SMTPClientConfig)` | ||||
| Main client class for direct SMTP operations. | ||||
| 
 | ||||
| #### Methods | ||||
| - `sendSMTPCommand(data: string, options?: SendOptions): Promise<string>` | ||||
| - `getStats(): ClientStats`   | ||||
| - `getConnectionState(): ConnectionState` | ||||
| - `getQueueSize(): number` | ||||
| - `clearQueue(): void` | ||||
| - `shutdown(timeout?: number): Promise<void>` | ||||
| - `sendSMTPCommand(data, options?)` - Send SMTP command | ||||
| - `getStats()` - Get client statistics   | ||||
| - `getConnectionState()` - Get current state | ||||
| - `getQueueSize()` - Get queue size | ||||
| - `shutdown(timeout?)` - Graceful shutdown | ||||
| 
 | ||||
| #### Events | ||||
| - `connecting`, `connected`, `authenticated`, `disconnected` | ||||
| - `reconnecting`, `reconnected`, `error`   | ||||
| - `messageQueued`, `messageProcessed`, `messageFailed` | ||||
| - `queueProcessingStarted`, `queueProcessingCompleted` | ||||
| - `channelOpened`, `channelClosed`, `channelError` | ||||
| - `stateChanged` | ||||
| 
 | ||||
| ## Contributing | ||||
| 
 | ||||
| We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details. | ||||
| 
 | ||||
| 1. Fork the repository | ||||
| 2. Create a feature branch (`git checkout -b feature/amazing-feature`) | ||||
| 3. Commit your changes (`git commit -m 'Add amazing feature'`) | ||||
| 4. Push to the branch (`git push origin feature/amazing-feature`) | ||||
| 5. Open a Pull Request | ||||
| - Connection: `connecting`, `connected`, `authenticated`, `disconnected` | ||||
| - Queue: `messageQueued`, `messageProcessed`, `messageFailed` | ||||
| - State: `stateChanged`, `error` | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. | ||||
| MIT License - see [LICENSE](LICENSE) file for details. | ||||
| 
 | ||||
| ## Support | ||||
| 
 | ||||
| - 📧 Email: [support@siwatsystem.com](mailto:support@siwatsystem.com) | ||||
| - 🐛 Issues: [GitHub Issues](https://github.com/siwatsystem/smtp-ws-relay-client/issues) | ||||
| - 📖 Documentation: [API Docs](https://siwatsystem.github.io/smtp-ws-relay-client/) | ||||
| 
 | ||||
| ## Changelog | ||||
| 
 | ||||
| See [CHANGELOG.md](CHANGELOG.md) for version history and updates. | ||||
| - Issues: [Git Repository Issues](https://git.siwatsystem.com/siwat/mxrelay-consumer/issues) | ||||
| - Documentation: [Project Repository](https://git.siwatsystem.com/siwat/mxrelay-consumer) | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Built with ❤️ by [SiwatSystem](https://siwatsystem.com) | ||||
| Built by SiwatSystem | ||||
|  | @ -11,13 +11,8 @@ async function nodemailerTransportExample() { | |||
|     console.log('Nodemailer SMTP WebSocket Transport Example\n'); | ||||
| 
 | ||||
|     // Create the WebSocket transport
 | ||||
|     const transport = createTransport({ | ||||
|         host: '192.168.0.62', | ||||
|         apiKey: 'cebc9a7f-4e0c-4fda-9dd0-85f48c02800c', | ||||
|         port: 80, | ||||
|         secure: false, // Set to true for wss://
 | ||||
|         debug: true | ||||
|     }); | ||||
|     const apiKey = process.env.MXRELAY_API_KEY || 'your-api-key-here'; | ||||
|     const transport = createTransport(apiKey, {debug: false }); | ||||
| 
 | ||||
|     // Create Nodemailer transporter
 | ||||
|     const transporter = nodemailer.createTransport(transport); | ||||
|  | @ -37,8 +32,8 @@ async function nodemailerTransportExample() { | |||
|         console.log('Sending test email...'); | ||||
|          | ||||
|         const info = await transporter.sendMail({ | ||||
|             from: 'cudconnex@satitm.chula.ac.th', | ||||
|             to: 'siwat.s@chula.ac.th', | ||||
|             from: 'sender@example.com', | ||||
|             to: 'recipient@example.com', | ||||
|             subject: 'Test Email via SMTP WebSocket', | ||||
|             text: 'This email was sent using the SMTP WebSocket transport!', | ||||
|             html: ` | ||||
|  |  | |||
							
								
								
									
										170
									
								
								src/transport.ts
									
										
									
									
									
								
							
							
						
						
									
										170
									
								
								src/transport.ts
									
										
									
									
									
								
							|  | @ -13,7 +13,7 @@ import { Readable } from 'stream'; | |||
|  */ | ||||
| export interface TransportOptions extends Omit<SMTPClientConfig, 'url' | 'apiKey'> { | ||||
|     /** WebSocket server URL */ | ||||
|     host: string; | ||||
|     host?: string; | ||||
|      | ||||
|     /** WebSocket server port */ | ||||
|     port?: number; | ||||
|  | @ -22,7 +22,7 @@ export interface TransportOptions extends Omit<SMTPClientConfig, 'url' | 'apiKey | |||
|     secure?: boolean; | ||||
|      | ||||
|     /** API key for authentication */ | ||||
|     apiKey: string; | ||||
|     apiKey?: string; | ||||
|      | ||||
|     /** Transport name */ | ||||
|     name?: string; | ||||
|  | @ -87,26 +87,33 @@ export class SMTPWSTransport extends EventEmitter { | |||
|     constructor(options: TransportOptions) { | ||||
|         super(); | ||||
|          | ||||
|         this.options = options; | ||||
|         // Set defaults
 | ||||
|         this.options = { | ||||
|             host: 'api.siwatsystem.com', | ||||
|             port: 443, | ||||
|             secure: true, | ||||
|             debug: false, | ||||
|             ...options | ||||
|         }; | ||||
|          | ||||
|         // Convert transport options to client config
 | ||||
|         const protocol = options.secure ? 'wss' : 'ws'; | ||||
|         const port = options.port || (options.secure ? 443 : 3000); | ||||
|         const url = `${protocol}://${options.host}:${port}/smtp`; | ||||
|         const protocol = this.options.secure ? 'wss' : 'ws'; | ||||
|         const port = this.options.port || (this.options.secure ? 443 : 3000); | ||||
|         const url = `${protocol}://${this.options.host}:${port}/smtp`; | ||||
|          | ||||
|         const clientConfig: SMTPClientConfig = { | ||||
|             url, | ||||
|             apiKey: options.apiKey, | ||||
|             ...(options.debug !== undefined && { debug: options.debug }), | ||||
|             ...(options.maxQueueSize !== undefined && { maxQueueSize: options.maxQueueSize }), | ||||
|             ...(options.reconnectInterval !== undefined && { reconnectInterval: options.reconnectInterval }), | ||||
|             ...(options.maxReconnectAttempts !== undefined && { maxReconnectAttempts: options.maxReconnectAttempts }), | ||||
|             ...(options.authTimeout !== undefined && { authTimeout: options.authTimeout }), | ||||
|             ...(options.channelTimeout !== undefined && { channelTimeout: options.channelTimeout }), | ||||
|             ...(options.messageTimeout !== undefined && { messageTimeout: options.messageTimeout }), | ||||
|             ...(options.maxConcurrentMessages !== undefined && { maxConcurrentMessages: options.maxConcurrentMessages }), | ||||
|             ...(options.logger !== undefined && { logger: options.logger }), | ||||
|             ...(options.heartbeatInterval !== undefined && { heartbeatInterval: options.heartbeatInterval }) | ||||
|             apiKey: this.options.apiKey!, | ||||
|             ...(this.options.debug !== undefined && { debug: this.options.debug }), | ||||
|             ...(this.options.maxQueueSize !== undefined && { maxQueueSize: this.options.maxQueueSize }), | ||||
|             ...(this.options.reconnectInterval !== undefined && { reconnectInterval: this.options.reconnectInterval }), | ||||
|             ...(this.options.maxReconnectAttempts !== undefined && { maxReconnectAttempts: this.options.maxReconnectAttempts }), | ||||
|             ...(this.options.authTimeout !== undefined && { authTimeout: this.options.authTimeout }), | ||||
|             ...(this.options.channelTimeout !== undefined && { channelTimeout: this.options.channelTimeout }), | ||||
|             ...(this.options.messageTimeout !== undefined && { messageTimeout: this.options.messageTimeout }), | ||||
|             ...(this.options.maxConcurrentMessages !== undefined && { maxConcurrentMessages: this.options.maxConcurrentMessages }), | ||||
|             ...(this.options.logger !== undefined && { logger: this.options.logger }), | ||||
|             ...(this.options.heartbeatInterval !== undefined && { heartbeatInterval: this.options.heartbeatInterval }) | ||||
|         }; | ||||
|          | ||||
|         this.client = new SMTPOverWSClient(clientConfig); | ||||
|  | @ -233,14 +240,25 @@ export class SMTPWSTransport extends EventEmitter { | |||
|         // Send complete SMTP transaction in one session
 | ||||
|         const response = await this.client.sendSMTPCommand(smtpTransaction); | ||||
|          | ||||
|         return { | ||||
|             envelope, | ||||
|             messageId, | ||||
|             accepted: [...envelope.to], | ||||
|             rejected: [], | ||||
|             pending: [], | ||||
|             response | ||||
|         }; | ||||
|         // Parse SMTP response for success/failure
 | ||||
|         const result = this.parseSmtpResponse(response, envelope, messageId); | ||||
|          | ||||
|         // If there were SMTP errors, throw an appropriate error
 | ||||
|         if (result.rejected.length > 0 || !this.isSuccessfulResponse(response)) { | ||||
|             const errorDetails = this.extractSmtpError(response); | ||||
|             throw new MessageError( | ||||
|                 errorDetails.message,  | ||||
|                 messageId,  | ||||
|                 0,  | ||||
|                 {  | ||||
|                     smtpCode: errorDetails.code, | ||||
|                     smtpResponse: response, | ||||
|                     rejectedRecipients: result.rejected | ||||
|                 } | ||||
|             ); | ||||
|         } | ||||
|          | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -371,11 +389,109 @@ export class SMTPWSTransport extends EventEmitter { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Parse SMTP response and categorize recipients | ||||
|      */ | ||||
|     private parseSmtpResponse(response: string, envelope: Envelope, messageId: string): SendResult { | ||||
|         const lines = response.split('\n').map(line => line.trim()).filter(line => line); | ||||
|         const accepted: string[] = []; | ||||
|         const rejected: string[] = []; | ||||
|         const pending: string[] = []; | ||||
|          | ||||
|         // Check each line for SMTP status codes
 | ||||
|         let hasErrors = false; | ||||
|         for (const line of lines) { | ||||
|             const match = line.match(/^(\d{3})\s/); | ||||
|             if (match && match[1]) { | ||||
|                 const code = parseInt(match[1]); | ||||
|                 if (code >= 500) { | ||||
|                     // 5xx = permanent failure
 | ||||
|                     hasErrors = true; | ||||
|                     rejected.push(...envelope.to); | ||||
|                 } else if (code >= 400) { | ||||
|                     // 4xx = temporary failure
 | ||||
|                     hasErrors = true; | ||||
|                     pending.push(...envelope.to); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // If no errors found, all recipients are accepted
 | ||||
|         if (!hasErrors) { | ||||
|             accepted.push(...envelope.to); | ||||
|         } | ||||
|          | ||||
|         return { | ||||
|             envelope, | ||||
|             messageId, | ||||
|             accepted: [...new Set(accepted)], // Remove duplicates
 | ||||
|             rejected: [...new Set(rejected)], | ||||
|             pending: [...new Set(pending)], | ||||
|             response | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if SMTP response indicates success | ||||
|      */ | ||||
|     private isSuccessfulResponse(response: string): boolean { | ||||
|         const lines = response.split('\n').map(line => line.trim()).filter(line => line); | ||||
|          | ||||
|         for (const line of lines) { | ||||
|             const match = line.match(/^(\d{3})\s/); | ||||
|             if (match && match[1]) { | ||||
|                 const code = parseInt(match[1]); | ||||
|                 if (code >= 400) { | ||||
|                     return false; // Any 4xx or 5xx code means failure
 | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract error details from SMTP response | ||||
|      */ | ||||
|     private extractSmtpError(response: string): { code: string; message: string } { | ||||
|         const lines = response.split('\n').map(line => line.trim()).filter(line => line); | ||||
|          | ||||
|         // Find the first error line (4xx or 5xx)
 | ||||
|         for (const line of lines) { | ||||
|             const match = line.match(/^(\d{3})\s+(.+)/); | ||||
|             if (match && match[1] && match[2]) { | ||||
|                 const code = parseInt(match[1]); | ||||
|                 if (code >= 400) { | ||||
|                     const errorCode = match[1]; | ||||
|                     const errorMessage = match[2]; | ||||
|                      | ||||
|                     // Provide user-friendly error messages for common codes
 | ||||
|                     let friendlyMessage = errorMessage; | ||||
|                     if (code === 550) { | ||||
|                         friendlyMessage = `Sender not authorized: ${errorMessage}`; | ||||
|                     } else if (code === 553) { | ||||
|                         friendlyMessage = `Invalid recipient address: ${errorMessage}`; | ||||
|                     } else if (code === 554) { | ||||
|                         friendlyMessage = `Message rejected: ${errorMessage}`; | ||||
|                     } else if (code >= 500) { | ||||
|                         friendlyMessage = `Permanent error: ${errorMessage}`; | ||||
|                     } else if (code >= 400) { | ||||
|                         friendlyMessage = `Temporary error: ${errorMessage}`; | ||||
|                     } | ||||
|                      | ||||
|                     return { code: errorCode, message: friendlyMessage }; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         return { code: '500', message: 'Unknown SMTP error occurred' }; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Create transport instance | ||||
|  */ | ||||
| export function createTransport(options: TransportOptions): SMTPWSTransport { | ||||
|     return new SMTPWSTransport(options); | ||||
| export function createTransport(apiKey: string, options?: Omit<TransportOptions, 'apiKey'>): SMTPWSTransport { | ||||
|     return new SMTPWSTransport({ apiKey, ...options }); | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue