initial commit
This commit is contained in:
		
						commit
						619cb97fa3
					
				
					 23 changed files with 9242 additions and 0 deletions
				
			
		
							
								
								
									
										15
									
								
								.claude/settings.local.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								.claude/settings.local.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| { | ||||
|   "permissions": { | ||||
|     "allow": [ | ||||
|       "Bash(mkdir:*)", | ||||
|       "Bash(mv:*)", | ||||
|       "Bash(npm install)", | ||||
|       "Bash(npm test:*)", | ||||
|       "Bash(npm install:*)", | ||||
|       "Bash(npm run build:*)", | ||||
|       "Bash(bun:*)" | ||||
|     ], | ||||
|     "deny": [], | ||||
|     "ask": [] | ||||
|   } | ||||
| } | ||||
							
								
								
									
										23
									
								
								.eslintrc.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								.eslintrc.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| module.exports = { | ||||
|   parser: '@typescript-eslint/parser', | ||||
|   extends: [ | ||||
|     'eslint:recommended', | ||||
|     '@typescript-eslint/recommended', | ||||
|     'prettier', | ||||
|   ], | ||||
|   plugins: ['@typescript-eslint', 'prettier'], | ||||
|   parserOptions: { | ||||
|     ecmaVersion: 2020, | ||||
|     sourceType: 'module', | ||||
|     project: './tsconfig.json', | ||||
|   }, | ||||
|   rules: { | ||||
|     'prettier/prettier': 'error', | ||||
|     '@typescript-eslint/no-unused-vars': 'error', | ||||
|     '@typescript-eslint/explicit-function-return-type': 'off', | ||||
|     '@typescript-eslint/explicit-module-boundary-types': 'off', | ||||
|     '@typescript-eslint/no-explicit-any': 'warn', | ||||
|     '@typescript-eslint/no-non-null-assertion': 'warn', | ||||
|   }, | ||||
|   ignorePatterns: ['lib/', 'node_modules/', '*.js'], | ||||
| }; | ||||
							
								
								
									
										55
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								.gitignore
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,55 @@ | |||
| # Dependencies | ||||
| node_modules/ | ||||
| npm-debug.log* | ||||
| yarn-debug.log* | ||||
| yarn-error.log* | ||||
| 
 | ||||
| # Build outputs | ||||
| lib/ | ||||
| dist/ | ||||
| *.tsbuildinfo | ||||
| src/ | ||||
| 
 | ||||
| # Coverage directory used by tools like istanbul | ||||
| coverage/ | ||||
| *.lcov | ||||
| 
 | ||||
| # Runtime data | ||||
| pids | ||||
| *.pid | ||||
| *.seed | ||||
| *.pid.lock | ||||
| 
 | ||||
| # Logs | ||||
| logs | ||||
| *.log | ||||
| 
 | ||||
| # Dependency directories | ||||
| .npm | ||||
| .yarn/ | ||||
| .pnp.* | ||||
| 
 | ||||
| # Optional eslint cache | ||||
| .eslintcache | ||||
| 
 | ||||
| # IDE | ||||
| .vscode/ | ||||
| .idea/ | ||||
| *.swp | ||||
| *.swo | ||||
| 
 | ||||
| # OS generated files | ||||
| .DS_Store | ||||
| .DS_Store? | ||||
| ._* | ||||
| .Spotlight-V100 | ||||
| .Trashes | ||||
| ehthumbs.db | ||||
| Thumbs.db | ||||
| 
 | ||||
| # Environment variables | ||||
| .env | ||||
| .env.local | ||||
| .env.development.local | ||||
| .env.test.local | ||||
| .env.production.local | ||||
							
								
								
									
										8
									
								
								.prettierrc
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.prettierrc
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| { | ||||
|   "semi": true, | ||||
|   "trailingComma": "es5", | ||||
|   "singleQuote": true, | ||||
|   "printWidth": 100, | ||||
|   "tabWidth": 2, | ||||
|   "useTabs": false | ||||
| } | ||||
							
								
								
									
										112
									
								
								CLAUDE.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								CLAUDE.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,112 @@ | |||
| # CLAUDE.md | ||||
| 
 | ||||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||||
| 
 | ||||
| ## Project Overview | ||||
| 
 | ||||
| This is **@siwatsystem/mxrelay-consumer**, a TypeScript client library for SMTP over WebSocket protocol. It provides intelligent queue management, automatic reconnection, and comprehensive error handling for sending emails through WebSocket-based SMTP relay servers. | ||||
| 
 | ||||
| ## Development Commands | ||||
| 
 | ||||
| ### 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 | ||||
| 
 | ||||
| ### 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 | ||||
| 
 | ||||
| ### 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 | ||||
| 
 | ||||
| ### Example Commands | ||||
| - `npm run example:basic` - Run basic usage example | ||||
| - `npm run example:queue` - Run queue management example | ||||
| 
 | ||||
| ## Architecture | ||||
| 
 | ||||
| ### Core Components | ||||
| 
 | ||||
| **SMTPOverWSClient (`src/client.ts`)** - Main client class that: | ||||
| - Manages WebSocket connections to SMTP relay servers | ||||
| - Implements intelligent message queuing with priority support | ||||
| - Handles automatic reconnection with exponential backoff   | ||||
| - Provides comprehensive error handling and recovery | ||||
| - Supports connection state management and event emission | ||||
| - Implements SMTP channel lifecycle management | ||||
| 
 | ||||
| **Message Queue System** - Priority-based queue that: | ||||
| - Automatically connects when messages are queued | ||||
| - Processes messages sequentially with retry logic | ||||
| - Disconnects when queue is empty to optimize resources | ||||
| - Supports HIGH/NORMAL/LOW/CRITICAL priority levels | ||||
| 
 | ||||
| **Connection State Machine** - Manages states: | ||||
| - DISCONNECTED → CONNECTING → CONNECTED → AUTHENTICATING → AUTHENTICATED | ||||
| - CHANNEL_OPENING → CHANNEL_READY → CHANNEL_CLOSED | ||||
| - RECONNECTING → FAILED (on max retry exhaustion) | ||||
| 
 | ||||
| **SMTPWSTransport (`src/transport.ts`)** - Nodemailer-compatible transport that: | ||||
| - Converts Nodemailer mail objects to SMTP commands | ||||
| - Implements standard SMTP protocol flow (EHLO, MAIL FROM, RCPT TO, DATA, QUIT) | ||||
| - Handles recipient validation and error reporting | ||||
| - Provides transport verification and lifecycle management | ||||
| 
 | ||||
| **Error Hierarchy (`src/errors.ts`)** - Structured errors: | ||||
| - `SMTPWSError` (base) → `ConnectionError`, `AuthenticationError`, `ChannelError` | ||||
| - `TimeoutError`, `QueueError`, `MessageError`, `ShutdownError` | ||||
| - `NetworkError`, `ProtocolError`, `ConfigurationError` | ||||
| - `ErrorFactory` for creating appropriate error instances | ||||
| 
 | ||||
| ### Protocol Implementation | ||||
| 
 | ||||
| **WebSocket Message Types**: | ||||
| - Authentication: `AUTHENTICATE` ↔ `AUTHENTICATE_RESPONSE` | ||||
| - Channel lifecycle: `SMTP_CHANNEL_OPEN` → `SMTP_CHANNEL_READY` → `SMTP_CHANNEL_CLOSED` | ||||
| - Data transfer: `SMTP_TO_SERVER` ↔ `SMTP_FROM_SERVER`   | ||||
| - Error handling: `SMTP_CHANNEL_ERROR` | ||||
| 
 | ||||
| **Queue Processing**: | ||||
| 1. Messages queued by priority (CRITICAL > HIGH > NORMAL > LOW) | ||||
| 2. Auto-connect when queue has messages | ||||
| 3. Sequential processing with retry logic (configurable retry count) | ||||
| 4. Auto-disconnect when queue empty | ||||
| 5. Comprehensive timeout handling at all levels | ||||
| 
 | ||||
| ### Key Configuration Options | ||||
| 
 | ||||
| - `url`: WebSocket server endpoint | ||||
| - `apiKey`: Authentication credential | ||||
| - `debug`: Enable console logging (default: false, library is silent by default) | ||||
| - `maxQueueSize`: Queue capacity limit (default: 1000) | ||||
| - `reconnectInterval`: Delay between reconnect attempts (default: 5000ms) | ||||
| - `maxReconnectAttempts`: Max retry count (default: 10) | ||||
| - `messageTimeout`: Per-message timeout (default: 60000ms) | ||||
| - `heartbeatInterval`: Keep-alive interval (default: 30000ms) | ||||
| 
 | ||||
| ### Build System | ||||
| 
 | ||||
| The project uses multiple TypeScript configurations: | ||||
| - `tsconfig.json` - Main config for CommonJS build | ||||
| - `tsconfig.esm.json` - ES modules build   | ||||
| - `tsconfig.types.json` - Type declarations build | ||||
| - `tsconfig.cjs.json` - CommonJS specific build | ||||
| 
 | ||||
| Output formats: | ||||
| - CommonJS: `lib/index.js` | ||||
| - ES Modules: `lib/index.esm.js`   | ||||
| - Type Declarations: `lib/index.d.ts` | ||||
| 
 | ||||
| ### Testing | ||||
| 
 | ||||
| - Jest with TypeScript support | ||||
| - WebSocket mocking in `tests/setup.ts` | ||||
| - 30-second test timeout for integration tests | ||||
| - Coverage collection from all source files except index.ts | ||||
							
								
								
									
										21
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| MIT License | ||||
| 
 | ||||
| Copyright (c) 2024 SiwatSystem | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										458
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								README.md
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,458 @@ | |||
| # @siwatsystem/smtp-ws-relay-client | ||||
| 
 | ||||
| [](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. | ||||
| 
 | ||||
| ## Features | ||||
| 
 | ||||
| 🚀 **Intelligent Queue Management** | ||||
| - Automatic WebSocket connection when messages are queued | ||||
| - Priority-based message processing | ||||
| - Configurable batch processing | ||||
| - Queue size limits and overflow protection | ||||
| 
 | ||||
| 🔄 **Robust Connection Handling**  | ||||
| - Automatic reconnection with exponential backoff | ||||
| - Connection state management | ||||
| - Heartbeat monitoring | ||||
| - Graceful connection lifecycle | ||||
| 
 | ||||
| ⚡ **High Performance** | ||||
| - Efficient SMTP channel cycling | ||||
| - Concurrent message processing support   | ||||
| - Optimized resource usage | ||||
| - Minimal memory footprint | ||||
| 
 | ||||
| 🛡️ **Enterprise-Grade Reliability** | ||||
| - Comprehensive error handling and classification | ||||
| - Timeout management for all operations | ||||
| - Retry logic with configurable attempts | ||||
| - Graceful degradation and recovery | ||||
| 
 | ||||
| 📊 **Monitoring & Observability** | ||||
| - Real-time statistics and metrics | ||||
| - Structured logging with configurable levels | ||||
| - Event-driven architecture for monitoring | ||||
| - Performance tracking and analytics | ||||
| 
 | ||||
| ## Installation | ||||
| 
 | ||||
| ```bash | ||||
| npm install @siwatsystem/smtp-ws-relay-client | ||||
| ``` | ||||
| 
 | ||||
| ## Quick Start | ||||
| 
 | ||||
| ```typescript | ||||
| import { SMTPOverWSClient, MessagePriority } from '@siwatsystem/smtp-ws-relay-client'; | ||||
| 
 | ||||
| // Create client instance | ||||
| const client = new SMTPOverWSClient({ | ||||
|     url: 'ws://your-smtp-relay-server:3000/smtp', | ||||
|     apiKey: 'your-api-key', | ||||
|     debug: true | ||||
| }); | ||||
| 
 | ||||
| // Send SMTP commands | ||||
| 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]); | ||||
| } catch (error) { | ||||
|     console.error('SMTP error:', error); | ||||
| } | ||||
| 
 | ||||
| // Graceful shutdown | ||||
| await client.shutdown(); | ||||
| ``` | ||||
| 
 | ||||
| ## Configuration Options | ||||
| 
 | ||||
| ```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; | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ## Advanced Usage | ||||
| 
 | ||||
| ### Priority-Based Message Queuing | ||||
| 
 | ||||
| ```typescript | ||||
| import { MessagePriority } from '@siwatsystem/smtp-ws-relay-client'; | ||||
| 
 | ||||
| // High priority message (processed first) | ||||
| await client.sendSMTPCommand('URGENT EMAIL DATA\\r\\n', { | ||||
|     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 | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### Event Monitoring | ||||
| 
 | ||||
| ```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); | ||||
| }); | ||||
| 
 | ||||
| // Queue events | ||||
| client.on('messageQueued', (messageId, queueSize) => { | ||||
|     console.log(`Message ${messageId} queued. Queue size: ${queueSize}`); | ||||
| }); | ||||
| 
 | ||||
| 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); | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### 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: | ||||
| 
 | ||||
| ```typescript | ||||
| import {  | ||||
|     ConnectionError,  | ||||
|     AuthenticationError,  | ||||
|     ChannelError,  | ||||
|     TimeoutError, | ||||
|     QueueError, | ||||
|     MessageError  | ||||
| } from '@siwatsystem/smtp-ws-relay-client'; | ||||
| 
 | ||||
| try { | ||||
|     await client.sendSMTPCommand('EHLO example.com\\r\\n'); | ||||
| } 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 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: | ||||
| 
 | ||||
| - `DISCONNECTED` - No connection to server | ||||
| - `CONNECTING` - Establishing WebSocket connection   | ||||
| - `CONNECTED` - WebSocket connected, authentication pending | ||||
| - `AUTHENTICATING` - Sending authentication credentials | ||||
| - `AUTHENTICATED` - Ready to open SMTP channels | ||||
| - `CHANNEL_OPENING` - Opening SMTP channel | ||||
| - `CHANNEL_READY` - SMTP channel active, ready for data | ||||
| - `CHANNEL_CLOSED` - SMTP channel closed | ||||
| - `CHANNEL_ERROR` - SMTP channel error occurred | ||||
| - `RECONNECTING` - Attempting to reconnect | ||||
| - `FAILED` - Connection failed, max retries reached | ||||
| 
 | ||||
| ## Protocol Support | ||||
| 
 | ||||
| This client implements the SMTP over WebSocket protocol specification: | ||||
| 
 | ||||
| ### 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 | ||||
| 
 | ||||
| ### Connection Flow | ||||
| 
 | ||||
| 1. **WebSocket Connection** - Establish WebSocket to relay server | ||||
| 2. **Authentication** - Authenticate using API key | ||||
| 3. **Channel Management** - Open/close SMTP channels 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 | ||||
| 
 | ||||
| ## Best Practices | ||||
| 
 | ||||
| ### Production Configuration | ||||
| 
 | ||||
| ```typescript | ||||
| const client = new SMTPOverWSClient({ | ||||
|     url: 'ws://your-production-server/smtp', | ||||
|     apiKey: process.env.SMTP_RELAY_API_KEY!, | ||||
|      | ||||
|     // Connection settings | ||||
|     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 | ||||
|     } | ||||
| }); | ||||
| ``` | ||||
| 
 | ||||
| ### Graceful Shutdown | ||||
| 
 | ||||
| ```typescript | ||||
| process.on('SIGTERM', async () => { | ||||
|     console.log('Shutting down SMTP client...'); | ||||
|     try { | ||||
|         await client.shutdown(30000); // 30 second timeout | ||||
|         console.log('SMTP client shutdown complete'); | ||||
|     } catch (error) { | ||||
|         console.error('Forced shutdown due to timeout'); | ||||
|     } | ||||
|     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 | ||||
| 
 | ||||
| ### SMTPOverWSClient | ||||
| 
 | ||||
| #### Constructor | ||||
| - `new SMTPOverWSClient(config: SMTPClientConfig)` | ||||
| 
 | ||||
| #### Methods | ||||
| - `sendSMTPCommand(data: string, options?: SendOptions): Promise<string>` | ||||
| - `getStats(): ClientStats`   | ||||
| - `getConnectionState(): ConnectionState` | ||||
| - `getQueueSize(): number` | ||||
| - `clearQueue(): void` | ||||
| - `shutdown(timeout?: number): Promise<void>` | ||||
| 
 | ||||
| #### 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 | ||||
| 
 | ||||
| ## License | ||||
| 
 | ||||
| This project is licensed under the MIT License - see the [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. | ||||
| 
 | ||||
| --- | ||||
| 
 | ||||
| Built with ❤️ by [SiwatSystem](https://siwatsystem.com) | ||||
							
								
								
									
										103
									
								
								examples/basic-usage.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								examples/basic-usage.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,103 @@ | |||
| #!/usr/bin/env ts-node | ||||
| 
 | ||||
| /** | ||||
|  * Basic usage example for SMTP over WebSocket client | ||||
|  */ | ||||
| 
 | ||||
| import { SMTPOverWSClient, MessagePriority } from '../src/index'; | ||||
| 
 | ||||
| async function basicUsageExample() { | ||||
|     console.log('Starting SMTP WebSocket Client Basic Example\n'); | ||||
| 
 | ||||
|     // Create client with basic configuration
 | ||||
|     const client = new SMTPOverWSClient({ | ||||
|         url: 'ws://localhost:3000/smtp', // Replace with your server URL
 | ||||
|         apiKey: 'your-api-key-here',    // Replace with your API key
 | ||||
|         debug: true, | ||||
|         reconnectInterval: 5000, | ||||
|         maxReconnectAttempts: 3 | ||||
|     }); | ||||
| 
 | ||||
|     // Set up event listeners
 | ||||
|     client.on('connected', () => { | ||||
|         console.log('Connected to WebSocket server'); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('authenticated', () => { | ||||
|         console.log('Authentication successful'); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('disconnected', (reason) => { | ||||
|         console.log(`Disconnected: ${reason || 'Unknown reason'}`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('error', (error) => { | ||||
|         console.error('Client error:', error.message); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('messageQueued', (messageId, queueSize) => { | ||||
|         console.log(`Message ${messageId} queued (Queue size: ${queueSize})`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('messageProcessed', (messageId, responseTime) => { | ||||
|         console.log(`Message ${messageId} processed in ${responseTime}ms`); | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|         console.log('Sending SMTP commands...\n'); | ||||
| 
 | ||||
|         // Send basic SMTP sequence
 | ||||
|         const ehloResponse = await client.sendSMTPCommand('EHLO example.com\r\n'); | ||||
|         console.log('EHLO Response:', ehloResponse.trim()); | ||||
| 
 | ||||
|         const mailFromResponse = await client.sendSMTPCommand('MAIL FROM: <sender@example.com>\r\n'); | ||||
|         console.log('MAIL FROM Response:', mailFromResponse.trim()); | ||||
| 
 | ||||
|         const rcptToResponse = await client.sendSMTPCommand('RCPT TO: <recipient@example.com>\r\n'); | ||||
|         console.log('RCPT TO Response:', rcptToResponse.trim()); | ||||
| 
 | ||||
|         const dataResponse = await client.sendSMTPCommand('DATA\r\n'); | ||||
|         console.log('DATA Response:', dataResponse.trim()); | ||||
| 
 | ||||
|         // Send email content
 | ||||
|         const emailContent = `From: sender@example.com\r\nTo: recipient@example.com\r\nSubject: Test Email\r\n\r\nThis is a test email.\r\n.\r\n`; | ||||
|         const contentResponse = await client.sendSMTPCommand(emailContent); | ||||
|         console.log('Content Response:', contentResponse.trim()); | ||||
| 
 | ||||
|         const quitResponse = await client.sendSMTPCommand('QUIT\r\n'); | ||||
|         console.log('QUIT Response:', quitResponse.trim()); | ||||
| 
 | ||||
|         console.log('\nAll SMTP commands sent successfully!'); | ||||
| 
 | ||||
|         // Display statistics
 | ||||
|         const stats = client.getStats(); | ||||
|         console.log('\nClient Statistics:'); | ||||
|         console.log(`   Messages Queued: ${stats.messagesQueued}`); | ||||
|         console.log(`   Messages Processed: ${stats.messagesProcessed}`); | ||||
|         console.log(`   Messages Failed: ${stats.messagesFailed}`); | ||||
|         console.log(`   Average Response Time: ${stats.averageResponseTime.toFixed(2)}ms`); | ||||
|         console.log(`   Connection Uptime: ${stats.connectionUptime}ms`); | ||||
| 
 | ||||
|     } catch (error) { | ||||
|         console.error('Error during SMTP communication:', error); | ||||
|     } finally { | ||||
|         console.log('\nShutting down client...'); | ||||
|         await client.shutdown(); | ||||
|         console.log('Client shutdown complete'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Run the example
 | ||||
| if (require.main === module) { | ||||
|     basicUsageExample() | ||||
|         .then(() => { | ||||
|             console.log('\nBasic usage example completed successfully'); | ||||
|             process.exit(0); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|             console.error('\nExample failed:', error); | ||||
|             process.exit(1); | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| export { basicUsageExample }; | ||||
							
								
								
									
										210
									
								
								examples/bulk-email.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								examples/bulk-email.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,210 @@ | |||
| #!/usr/bin/env ts-node | ||||
| 
 | ||||
| /** | ||||
|  * Bulk email sending example using Nodemailer transport | ||||
|  */ | ||||
| 
 | ||||
| import nodemailer from 'nodemailer'; | ||||
| import { createTransport } from '../src/index'; | ||||
| 
 | ||||
| async function bulkEmailExample() { | ||||
|     console.log('Bulk Email Example using SMTP WebSocket Transport\n'); | ||||
| 
 | ||||
|     // Create the WebSocket transport
 | ||||
|     const transport = createTransport({ | ||||
|         host: 'localhost', | ||||
|         port: 3000, | ||||
|         auth: { | ||||
|             user: 'your-api-key-here' | ||||
|         }, | ||||
|         maxQueueSize: 1000, // Handle large queues
 | ||||
|         debug: false // Disable debug for bulk operations
 | ||||
|     }); | ||||
| 
 | ||||
|     const transporter = nodemailer.createTransporter(transport); | ||||
| 
 | ||||
|     // Sample recipient list
 | ||||
|     const recipients = [ | ||||
|         { email: 'user1@example.com', name: 'User One' }, | ||||
|         { email: 'user2@example.com', name: 'User Two' }, | ||||
|         { email: 'user3@example.com', name: 'User Three' }, | ||||
|         { email: 'user4@example.com', name: 'User Four' }, | ||||
|         { email: 'user5@example.com', name: 'User Five' } | ||||
|     ]; | ||||
| 
 | ||||
|     console.log(`Sending emails to ${recipients.length} recipients...\n`); | ||||
| 
 | ||||
|     const results = []; | ||||
|     const startTime = Date.now(); | ||||
| 
 | ||||
|     // Send emails concurrently (transport handles queuing automatically)
 | ||||
|     const emailPromises = recipients.map(async (recipient, index) => { | ||||
|         try { | ||||
|             const info = await transporter.sendMail({ | ||||
|                 from: 'newsletter@example.com', | ||||
|                 to: recipient.email, | ||||
|                 subject: `Newsletter #${index + 1} - ${new Date().toLocaleDateString()}`, | ||||
|                 text: `Hello ${recipient.name}!\n\nThis is your personalized newsletter.\n\nBest regards,\nThe Newsletter Team`, | ||||
|                 html: ` | ||||
|                     <h2>Hello ${recipient.name}!</h2> | ||||
|                     <p>This is your personalized newsletter for ${new Date().toLocaleDateString()}.</p> | ||||
|                     <p>This email was delivered via our SMTP WebSocket transport system.</p> | ||||
|                     <hr> | ||||
|                     <p><small>Newsletter #${index + 1} | Sent at ${new Date().toLocaleTimeString()}</small></p> | ||||
|                 ` | ||||
|             }); | ||||
| 
 | ||||
|             console.log(`Email ${index + 1}/${recipients.length} sent to ${recipient.email}`); | ||||
|             return { | ||||
|                 success: true, | ||||
|                 recipient: recipient.email, | ||||
|                 messageId: info.messageId, | ||||
|                 response: info.response | ||||
|             }; | ||||
| 
 | ||||
|         } catch (error) { | ||||
|             console.error(`Failed to send email ${index + 1} to ${recipient.email}:`, (error as Error).message); | ||||
|             return { | ||||
|                 success: false, | ||||
|                 recipient: recipient.email, | ||||
|                 error: (error as Error).message | ||||
|             }; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Wait for all emails to complete
 | ||||
|     const emailResults = await Promise.allSettled(emailPromises); | ||||
|     const duration = Date.now() - startTime; | ||||
| 
 | ||||
|     // Process results
 | ||||
|     let successful = 0; | ||||
|     let failed = 0; | ||||
| 
 | ||||
|     emailResults.forEach((result) => { | ||||
|         if (result.status === 'fulfilled') { | ||||
|             results.push(result.value); | ||||
|             if (result.value.success) { | ||||
|                 successful++; | ||||
|             } else { | ||||
|                 failed++; | ||||
|             } | ||||
|         } else { | ||||
|             failed++; | ||||
|             results.push({ | ||||
|                 success: false, | ||||
|                 error: result.reason.message | ||||
|             }); | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Display summary
 | ||||
|     console.log('\n--- Bulk Email Results ---'); | ||||
|     console.log(`Total emails: ${recipients.length}`); | ||||
|     console.log(`Successful: ${successful}`); | ||||
|     console.log(`Failed: ${failed}`); | ||||
|     console.log(`Duration: ${(duration / 1000).toFixed(2)} seconds`); | ||||
|     console.log(`Average time per email: ${(duration / recipients.length).toFixed(0)}ms`); | ||||
| 
 | ||||
|     // Display failed emails if any
 | ||||
|     if (failed > 0) { | ||||
|         console.log('\nFailed emails:'); | ||||
|         results.forEach((result, index) => { | ||||
|             if (!result.success) { | ||||
|                 console.log(`  ${index + 1}. ${result.recipient || 'Unknown'}: ${result.error}`); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     // Close transport
 | ||||
|     await transport.close(); | ||||
|     console.log('\nTransport closed'); | ||||
| } | ||||
| 
 | ||||
| // Advanced bulk email with throttling
 | ||||
| async function throttledBulkEmail() { | ||||
|     console.log('\nThrottled Bulk Email Example\n'); | ||||
| 
 | ||||
|     const transport = createTransport({ | ||||
|         host: 'localhost', | ||||
|         port: 3000, | ||||
|         auth: { | ||||
|             user: 'your-api-key-here' | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     const transporter = nodemailer.createTransporter(transport); | ||||
| 
 | ||||
|     // Generate larger recipient list
 | ||||
|     const recipients = Array.from({ length: 20 }, (_, i) => ({ | ||||
|         email: `user${i + 1}@example.com`, | ||||
|         name: `User ${i + 1}` | ||||
|     })); | ||||
| 
 | ||||
|     console.log(`Sending throttled emails to ${recipients.length} recipients...`); | ||||
|     console.log('Processing 5 emails at a time with 1 second delay between batches\n'); | ||||
| 
 | ||||
|     const batchSize = 5; | ||||
|     const batches = []; | ||||
|      | ||||
|     for (let i = 0; i < recipients.length; i += batchSize) { | ||||
|         batches.push(recipients.slice(i, i + batchSize)); | ||||
|     } | ||||
| 
 | ||||
|     let totalSuccessful = 0; | ||||
|     let totalFailed = 0; | ||||
| 
 | ||||
|     for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { | ||||
|         const batch = batches[batchIndex]; | ||||
|         console.log(`Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} emails)...`); | ||||
| 
 | ||||
|         const batchPromises = batch.map(async (recipient) => { | ||||
|             try { | ||||
|                 await transporter.sendMail({ | ||||
|                     from: 'batch@example.com', | ||||
|                     to: recipient.email, | ||||
|                     subject: `Batch Email - ${recipient.name}`, | ||||
|                     text: `Hello ${recipient.name}, this is a batch email.` | ||||
|                 }); | ||||
|                 return { success: true, email: recipient.email }; | ||||
|             } catch (error) { | ||||
|                 return { success: false, email: recipient.email, error: (error as Error).message }; | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         const batchResults = await Promise.all(batchPromises); | ||||
|          | ||||
|         const batchSuccessful = batchResults.filter(r => r.success).length; | ||||
|         const batchFailed = batchResults.filter(r => !r.success).length; | ||||
|          | ||||
|         totalSuccessful += batchSuccessful; | ||||
|         totalFailed += batchFailed; | ||||
|          | ||||
|         console.log(`Batch ${batchIndex + 1} complete: ${batchSuccessful} successful, ${batchFailed} failed`); | ||||
| 
 | ||||
|         // Wait between batches (except for the last one)
 | ||||
|         if (batchIndex < batches.length - 1) { | ||||
|             await new Promise(resolve => setTimeout(resolve, 1000)); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     console.log(`\nThrottled bulk email complete: ${totalSuccessful} successful, ${totalFailed} failed`); | ||||
|      | ||||
|     await transport.close(); | ||||
| } | ||||
| 
 | ||||
| // Run the examples
 | ||||
| if (require.main === module) { | ||||
|     (async () => { | ||||
|         try { | ||||
|             await bulkEmailExample(); | ||||
|             await throttledBulkEmail(); | ||||
|             console.log('\nBulk email examples completed successfully'); | ||||
|         } catch (error) { | ||||
|             console.error('\nExamples failed:', error); | ||||
|         } finally { | ||||
|             process.exit(0); | ||||
|         } | ||||
|     })(); | ||||
| } | ||||
| 
 | ||||
| export { bulkEmailExample, throttledBulkEmail }; | ||||
							
								
								
									
										88
									
								
								examples/nodemailer-transport.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								examples/nodemailer-transport.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| #!/usr/bin/env ts-node | ||||
| 
 | ||||
| /** | ||||
|  * Nodemailer transport example for SMTP over WebSocket | ||||
|  */ | ||||
| 
 | ||||
| import nodemailer from 'nodemailer'; | ||||
| import { createTransport } from '../src/index'; | ||||
| 
 | ||||
| async function nodemailerTransportExample() { | ||||
|     console.log('Nodemailer SMTP WebSocket Transport Example\n'); | ||||
| 
 | ||||
|     // Create the WebSocket transport
 | ||||
|     const transport = createTransport({ | ||||
|         host: '192.168.0.62', | ||||
|         port: 80, | ||||
|         secure: false, // Set to true for wss://
 | ||||
|         auth: { | ||||
|             user: 'cebc9a7f-4e0c-4fda-9dd0-85f48c02800c' // Your SMTP relay API key
 | ||||
|         }, | ||||
|         debug: true | ||||
|     }); | ||||
| 
 | ||||
|     // Create Nodemailer transporter
 | ||||
|     const transporter = nodemailer.createTransport(transport); | ||||
| 
 | ||||
|     // Verify the transport configuration
 | ||||
|     try { | ||||
|         console.log('Verifying transport configuration...'); | ||||
|         await transporter.verify(); | ||||
|         console.log('Transport verification successful\n'); | ||||
|     } catch (error) { | ||||
|         console.error('Transport verification failed:', error); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     // Send a test email
 | ||||
|     try { | ||||
|         console.log('Sending test email...'); | ||||
|          | ||||
|         const info = await transporter.sendMail({ | ||||
|             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: ` | ||||
|                 <h1>Test Email</h1> | ||||
|                 <p>This email was sent using the <strong>SMTP WebSocket transport</strong>!</p> | ||||
|                 <p>Features:</p> | ||||
|                 <ul> | ||||
|                     <li>Automatic connection management</li> | ||||
|                     <li>Queue-based message handling</li> | ||||
|                     <li>Nodemailer compatibility</li> | ||||
|                     <li>WebSocket-based SMTP relay</li> | ||||
|                 </ul> | ||||
|             ` | ||||
|         }); | ||||
| 
 | ||||
|         console.log('Email sent successfully!'); | ||||
|         console.log('Message ID:', info.messageId); | ||||
|         console.log('Accepted recipients:', info.accepted); | ||||
|         console.log('Rejected recipients:', info.rejected); | ||||
|         console.log('Response:', info.response); | ||||
| 
 | ||||
|     } catch (error) { | ||||
|         console.error('Failed to send email:', error); | ||||
|     } finally { | ||||
|         // Close the transport
 | ||||
|         console.log('\nClosing transport...'); | ||||
|         await transport.close(); | ||||
|         console.log('Transport closed'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Run the example
 | ||||
| if (require.main === module) { | ||||
|     nodemailerTransportExample() | ||||
|         .then(() => { | ||||
|             console.log('\nNodemailer transport example completed successfully'); | ||||
|             process.exit(0); | ||||
|         }) | ||||
|         .catch((error) => { | ||||
|             console.error('\nExample failed:', error); | ||||
|             process.exit(1); | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| export { nodemailerTransportExample }; | ||||
							
								
								
									
										226
									
								
								examples/queue-management.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								examples/queue-management.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,226 @@ | |||
| #!/usr/bin/env ts-node | ||||
| 
 | ||||
| /** | ||||
|  * Queue management and priority example for SMTP over WebSocket client | ||||
|  */ | ||||
| 
 | ||||
| import { SMTPOverWSClient, MessagePriority } from '../src/index'; | ||||
| 
 | ||||
| async function queueManagementExample() { | ||||
|     console.log('Starting SMTP WebSocket Client Queue Management Example\n'); | ||||
| 
 | ||||
|     // Create client - queue management is automatic
 | ||||
|     const client = new SMTPOverWSClient({ | ||||
|         url: 'ws://localhost:3000/smtp', | ||||
|         apiKey: 'your-api-key-here', | ||||
|         debug: true, | ||||
|         maxQueueSize: 100, | ||||
|         reconnectInterval: 2000, | ||||
|         maxReconnectAttempts: 5 | ||||
|     }); | ||||
| 
 | ||||
|     // Set up comprehensive event monitoring
 | ||||
|     client.on('connecting', () => { | ||||
|         console.log('Connecting to server...'); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('connected', () => { | ||||
|         console.log('Connected to WebSocket server'); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('authenticated', () => { | ||||
|         console.log('Authentication successful'); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('queueProcessingStarted', (queueSize) => { | ||||
|         console.log(`Queue processing started with ${queueSize} messages`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('queueProcessingCompleted', (processed, failed) => { | ||||
|         console.log(`Queue processing completed: ${processed} processed, ${failed} failed`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('messageQueued', (messageId, queueSize) => { | ||||
|         console.log(`Message ${messageId} queued (Queue: ${queueSize})`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('messageProcessed', (messageId, responseTime) => { | ||||
|         console.log(`Message ${messageId} processed in ${responseTime}ms`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('messageFailed', (messageId, error) => { | ||||
|         console.log(`Message ${messageId} failed: ${error.message}`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('stateChanged', (oldState, newState) => { | ||||
|         console.log(`State changed: ${oldState} → ${newState}`); | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|         console.log('Demonstrating priority-based queue management...\n'); | ||||
| 
 | ||||
|         // Queue multiple messages with different priorities
 | ||||
|         const messagePromises: Promise<string>[] = []; | ||||
| 
 | ||||
|         // Low priority messages (will be processed last)
 | ||||
|         console.log('Queuing low priority messages...'); | ||||
|         for (let i = 1; i <= 3; i++) { | ||||
|             const promise = client.sendSMTPCommand(`LOW PRIORITY ${i}\r\n`, { | ||||
|                 priority: MessagePriority.LOW, | ||||
|                 timeout: 30000 | ||||
|             }); | ||||
|             messagePromises.push(promise); | ||||
|         } | ||||
| 
 | ||||
|         // Normal priority messages
 | ||||
|         console.log('Queuing normal priority messages...'); | ||||
|         for (let i = 1; i <= 5; i++) { | ||||
|             const promise = client.sendSMTPCommand(`NORMAL PRIORITY ${i}\r\n`, { | ||||
|                 priority: MessagePriority.NORMAL, | ||||
|                 timeout: 30000 | ||||
|             }); | ||||
|             messagePromises.push(promise); | ||||
|         } | ||||
| 
 | ||||
|         // High priority messages (will be processed first)
 | ||||
|         console.log('Queuing high priority messages...'); | ||||
|         for (let i = 1; i <= 2; i++) { | ||||
|             const promise = client.sendSMTPCommand(`HIGH PRIORITY ${i}\r\n`, { | ||||
|                 priority: MessagePriority.HIGH, | ||||
|                 timeout: 30000 | ||||
|             }); | ||||
|             messagePromises.push(promise); | ||||
|         } | ||||
| 
 | ||||
|         // Critical priority message (highest priority)
 | ||||
|         console.log('Queuing critical priority message...'); | ||||
|         const criticalPromise = client.sendSMTPCommand('CRITICAL PRIORITY MESSAGE\r\n', { | ||||
|             priority: MessagePriority.CRITICAL, | ||||
|             timeout: 30000 | ||||
|         }); | ||||
|         messagePromises.push(criticalPromise); | ||||
| 
 | ||||
|         console.log(`\nTotal messages queued: ${messagePromises.length}`); | ||||
|         console.log(`Current queue size: ${client.getQueueSize()}`); | ||||
| 
 | ||||
|         // Add some messages after a delay to show dynamic queuing
 | ||||
|         setTimeout(() => { | ||||
|             console.log('\nAdding more messages to active queue...'); | ||||
|              | ||||
|             const additionalPromises = [ | ||||
|                 client.sendSMTPCommand('LATE HIGH PRIORITY\r\n', { | ||||
|                     priority: MessagePriority.HIGH | ||||
|                 }), | ||||
|                 client.sendSMTPCommand('LATE NORMAL PRIORITY\r\n', { | ||||
|                     priority: MessagePriority.NORMAL | ||||
|                 }) | ||||
|             ]; | ||||
| 
 | ||||
|             messagePromises.push(...additionalPromises); | ||||
|         }, 2000); | ||||
| 
 | ||||
|         // Wait for all messages to be processed
 | ||||
|         console.log('\nWaiting for all messages to be processed...\n'); | ||||
|         const responses = await Promise.allSettled(messagePromises); | ||||
| 
 | ||||
|         // Analyze results
 | ||||
|         console.log('\nProcessing Results:'); | ||||
|         let successful = 0; | ||||
|         let failed = 0; | ||||
| 
 | ||||
|         responses.forEach((result, index) => { | ||||
|             if (result.status === 'fulfilled') { | ||||
|                 successful++; | ||||
|                 console.log(`Message ${index + 1}: ${result.value.trim()}`); | ||||
|             } else { | ||||
|                 failed++; | ||||
|                 console.log(`Message ${index + 1}: ${result.reason.message}`); | ||||
|             } | ||||
|         }); | ||||
| 
 | ||||
|         console.log(`\nSummary: ${successful} successful, ${failed} failed`); | ||||
| 
 | ||||
|         // Display final statistics
 | ||||
|         const stats = client.getStats(); | ||||
|         console.log('\nFinal Client Statistics:'); | ||||
|         console.log(`   Messages Queued: ${stats.messagesQueued}`); | ||||
|         console.log(`   Messages Processed: ${stats.messagesProcessed}`); | ||||
|         console.log(`   Messages Failed: ${stats.messagesFailed}`); | ||||
|         console.log(`   Average Response Time: ${stats.averageResponseTime.toFixed(2)}ms`); | ||||
|         console.log(`   Total Connections: ${stats.totalConnections}`); | ||||
|         console.log(`   Reconnection Attempts: ${stats.reconnectionAttempts}`); | ||||
|         console.log(`   Connection Uptime: ${(stats.connectionUptime / 1000).toFixed(2)}s`); | ||||
| 
 | ||||
|     } catch (error) { | ||||
|         console.error('Error during queue management demo:', error); | ||||
|     } finally { | ||||
|         console.log('\nShutting down client...'); | ||||
|         await client.shutdown(10000); // 10 second timeout
 | ||||
|         console.log('Client shutdown complete'); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Simulate network issues for demonstration
 | ||||
| async function resilenceExample() { | ||||
|     console.log('\nStarting resilience demonstration...\n'); | ||||
| 
 | ||||
|     const client = new SMTPOverWSClient({ | ||||
|         url: 'ws://localhost:3000/smtp', | ||||
|         apiKey: 'your-api-key-here', | ||||
|         debug: true, | ||||
|         reconnectInterval: 1000, | ||||
|         maxReconnectAttempts: 3, | ||||
|         messageTimeout: 5000 | ||||
|     }); | ||||
| 
 | ||||
|     client.on('reconnecting', (attempt, maxAttempts) => { | ||||
|         console.log(`Reconnection attempt ${attempt}/${maxAttempts}`); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('reconnected', () => { | ||||
|         console.log('Successfully reconnected'); | ||||
|     }); | ||||
| 
 | ||||
|     client.on('error', (error) => { | ||||
|         console.log(`Error handled: ${error.message}`); | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|         // Queue messages that will trigger reconnection scenarios
 | ||||
|         const promises = []; | ||||
|          | ||||
|         for (let i = 1; i <= 5; i++) { | ||||
|             promises.push( | ||||
|                 client.sendSMTPCommand(`RESILIENCE TEST ${i}\r\n`, { | ||||
|                     retries: 2, | ||||
|                     timeout: 10000 | ||||
|                 }).catch(error => { | ||||
|                     console.log(`Message ${i} failed: ${error.message}`); | ||||
|                     return `FAILED: ${error.message}`; | ||||
|                 }) | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         await Promise.all(promises); | ||||
|          | ||||
|     } finally { | ||||
|         await client.shutdown(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Run the examples
 | ||||
| if (require.main === module) { | ||||
|     (async () => { | ||||
|         try { | ||||
|             await queueManagementExample(); | ||||
|             await resilenceExample(); | ||||
|             console.log('\nQueue management examples completed successfully'); | ||||
|         } catch (error) { | ||||
|             console.error('\nExamples failed:', error); | ||||
|         } finally { | ||||
|             process.exit(0); | ||||
|         } | ||||
|     })(); | ||||
| } | ||||
| 
 | ||||
| export { queueManagementExample, resilenceExample }; | ||||
							
								
								
									
										61
									
								
								examples/simple-usage.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								examples/simple-usage.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,61 @@ | |||
| #!/usr/bin/env ts-node | ||||
| 
 | ||||
| /** | ||||
|  * Simple usage example for SMTP over WebSocket client | ||||
|  */ | ||||
| 
 | ||||
| import { SMTPOverWSClient } from '../src/index'; | ||||
| 
 | ||||
| async function simpleUsageExample() { | ||||
|     console.log('Simple SMTP WebSocket Client Example\n'); | ||||
| 
 | ||||
|     // Create client - that's it, everything else is automatic
 | ||||
|     const client = new SMTPOverWSClient({ | ||||
|         url: 'ws://localhost:3000/smtp', | ||||
|         apiKey: 'your-api-key-here', | ||||
|         debug: true | ||||
|     }); | ||||
| 
 | ||||
|     try { | ||||
|         // Just send commands - client handles connection, queuing, everything automatically
 | ||||
|         console.log('Sending SMTP commands...'); | ||||
|          | ||||
|         const response1 = await client.sendSMTPCommand('EHLO example.com\r\n'); | ||||
|         console.log('EHLO:', response1.trim()); | ||||
| 
 | ||||
|         const response2 = await client.sendSMTPCommand('MAIL FROM: <test@example.com>\r\n'); | ||||
|         console.log('MAIL FROM:', response2.trim()); | ||||
| 
 | ||||
|         const response3 = await client.sendSMTPCommand('RCPT TO: <recipient@example.com>\r\n'); | ||||
|         console.log('RCPT TO:', response3.trim()); | ||||
| 
 | ||||
|         const response4 = await client.sendSMTPCommand('DATA\r\n'); | ||||
|         console.log('DATA:', response4.trim()); | ||||
| 
 | ||||
|         const response5 = await client.sendSMTPCommand('Subject: Test\r\n\r\nHello World!\r\n.\r\n'); | ||||
|         console.log('Message:', response5.trim()); | ||||
| 
 | ||||
|         const response6 = await client.sendSMTPCommand('QUIT\r\n'); | ||||
|         console.log('QUIT:', response6.trim()); | ||||
| 
 | ||||
|         console.log('\nAll done! Client automatically handled connection and cleanup.'); | ||||
| 
 | ||||
|     } catch (error) { | ||||
|         console.error('Error:', error); | ||||
|     } finally { | ||||
|         // Optional - client will clean up automatically when process exits
 | ||||
|         await client.shutdown(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| // Run the example
 | ||||
| if (require.main === module) { | ||||
|     simpleUsageExample() | ||||
|         .then(() => process.exit(0)) | ||||
|         .catch((error) => { | ||||
|             console.error('Example failed:', error); | ||||
|             process.exit(1); | ||||
|         }); | ||||
| } | ||||
| 
 | ||||
| export { simpleUsageExample }; | ||||
							
								
								
									
										23
									
								
								jest.config.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								jest.config.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| module.exports = { | ||||
|   preset: 'ts-jest', | ||||
|   testEnvironment: 'node', | ||||
|   roots: ['<rootDir>/src', '<rootDir>/tests'], | ||||
|   testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'], | ||||
|   transform: { | ||||
|     '^.+\\.ts$': 'ts-jest', | ||||
|   }, | ||||
|   collectCoverageFrom: [ | ||||
|     'src/**/*.ts', | ||||
|     '!src/**/*.d.ts', | ||||
|     '!src/index.ts', | ||||
|     '!**/node_modules/**', | ||||
|     '!**/examples/**', | ||||
|   ], | ||||
|   coverageDirectory: 'coverage', | ||||
|   coverageReporters: ['text', 'lcov', 'html'], | ||||
|   setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'], | ||||
|   testTimeout: 30000, | ||||
|   verbose: true, | ||||
|   clearMocks: true, | ||||
|   restoreMocks: true, | ||||
| }; | ||||
							
								
								
									
										7229
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										7229
									
								
								package-lock.json
									
										
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										104
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								package.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,104 @@ | |||
| { | ||||
|   "name": "@siwats/mxrelay-consumer", | ||||
|   "version": "1.0.0", | ||||
|   "description": "An internal TypeScript client library for transporting SMTP messages", | ||||
|   "main": "lib/index.js", | ||||
|   "module": "lib/index.esm.js", | ||||
|   "types": "lib/index.d.ts", | ||||
|   "files": [ | ||||
|     "lib/**/*", | ||||
|     "src/**/*", | ||||
|     "README.md", | ||||
|     "LICENSE" | ||||
|   ], | ||||
|   "exports": { | ||||
|     ".": { | ||||
|       "types": "./lib/index.d.ts", | ||||
|       "import": "./lib/index.esm.js", | ||||
|       "require": "./lib/index.js" | ||||
|     }, | ||||
|     "./types": { | ||||
|       "types": "./lib/types.d.ts", | ||||
|       "import": "./lib/types.esm.js", | ||||
|       "require": "./lib/types.js" | ||||
|     } | ||||
|   }, | ||||
|   "scripts": { | ||||
|     "build": "npm run build:cjs && npm run build:esm && npm run build:types", | ||||
|     "build:cjs": "tsc -p tsconfig.cjs.json", | ||||
|     "build:esm": "echo 'ESM build disabled - use CommonJS for now'", | ||||
|     "build:types": "tsc -p tsconfig.types.json", | ||||
|     "dev": "tsc --watch", | ||||
|     "typecheck": "tsc --noEmit", | ||||
|     "clean": "rimraf lib", | ||||
|     "prepublishOnly": "npm run clean && npm run build", | ||||
|     "test": "jest", | ||||
|     "test:watch": "jest --watch", | ||||
|     "test:coverage": "jest --coverage", | ||||
|     "lint": "eslint src/**/*.ts", | ||||
|     "lint:fix": "eslint src/**/*.ts --fix", | ||||
|     "format": "prettier --write 'src/**/*.ts'", | ||||
|     "docs": "typedoc src/index.ts", | ||||
|     "example:basic": "ts-node examples/basic-usage.ts", | ||||
|     "example:queue": "ts-node examples/queue-management.ts" | ||||
|   }, | ||||
|   "keywords": [ | ||||
|     "smtp", | ||||
|     "websocket", | ||||
|     "email", | ||||
|     "relay", | ||||
|     "queue", | ||||
|     "client", | ||||
|     "typescript", | ||||
|     "async", | ||||
|     "promise", | ||||
|     "reconnection", | ||||
|     "siwats" | ||||
|   ], | ||||
|   "author": { | ||||
|     "name": "Siwat Sirichai", | ||||
|     "email": "siwat@siwatinc.com", | ||||
|     "url": "https://siwatsystem.com" | ||||
|   }, | ||||
|   "license": "MIT", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/siwats/smtp-ws-relay-client.git" | ||||
|   }, | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/siwats/smtp-ws-relay-client/issues" | ||||
|   }, | ||||
|   "homepage": "https://github.com/siwats/smtp-ws-relay-client#readme", | ||||
|   "engines": { | ||||
|     "node": ">=16.0.0" | ||||
|   }, | ||||
|   "dependencies": { | ||||
|     "nodemailer": "^7.0.5", | ||||
|     "ws": "^8.14.2" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
|     "@types/node": ">=16" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/jest": "^29.5.5", | ||||
|     "@types/node": "^20.6.3", | ||||
|     "@types/nodemailer": "^6.4.8", | ||||
|     "@types/ws": "^8.5.6", | ||||
|     "@typescript-eslint/eslint-plugin": "^6.7.2", | ||||
|     "@typescript-eslint/parser": "^6.7.2", | ||||
|     "eslint": "^8.49.0", | ||||
|     "eslint-config-prettier": "^9.0.0", | ||||
|     "eslint-plugin-prettier": "^5.0.0", | ||||
|     "jest": "^29.7.0", | ||||
|     "prettier": "^3.0.3", | ||||
|     "rimraf": "^5.0.1", | ||||
|     "ts-jest": "^29.1.1", | ||||
|     "ts-node": "^10.9.1", | ||||
|     "typedoc": "^0.25.1", | ||||
|     "typescript": "^5.2.2" | ||||
|   }, | ||||
|   "publishConfig": { | ||||
|     "access": "public", | ||||
|     "registry": "https://registry.npmjs.org/" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										131
									
								
								tests/client.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								tests/client.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,131 @@ | |||
| import { SMTPOverWSClient, ConnectionState, MessagePriority } from '../src/index'; | ||||
| 
 | ||||
| describe('SMTPOverWSClient', () => { | ||||
|   let client: SMTPOverWSClient; | ||||
|    | ||||
|   beforeEach(() => { | ||||
|     client = new SMTPOverWSClient({ | ||||
|       url: 'ws://localhost:3000/smtp', | ||||
|       apiKey: 'test-api-key' | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(async () => { | ||||
|     if (client) { | ||||
|       await client.shutdown(); | ||||
|     } | ||||
|   }); | ||||
| 
 | ||||
|   describe('constructor', () => { | ||||
|     it('should create client with default configuration', () => { | ||||
|       expect(client.getConnectionState()).toBe(ConnectionState.DISCONNECTED); | ||||
|       expect(client.getQueueSize()).toBe(0); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw error for missing URL', () => { | ||||
|       expect(() => { | ||||
|         new SMTPOverWSClient({ | ||||
|           url: '', | ||||
|           apiKey: 'test-key' | ||||
|         }); | ||||
|       }).toThrow('URL is required'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw error for missing API key', () => { | ||||
|       expect(() => { | ||||
|         new SMTPOverWSClient({ | ||||
|           url: 'ws://localhost:3000', | ||||
|           apiKey: '' | ||||
|         }); | ||||
|       }).toThrow('API key is required'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('sendSMTPCommand', () => { | ||||
|     it('should queue message and return promise', async () => { | ||||
|       const promise = client.sendSMTPCommand('EHLO example.com\\r\\n'); | ||||
|        | ||||
|       expect(client.getQueueSize()).toBe(1); | ||||
|       expect(promise).toBeInstanceOf(Promise); | ||||
| 
 | ||||
|       // Clean up
 | ||||
|       client.clearQueue(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should respect priority ordering', async () => { | ||||
|       // Queue messages with different priorities
 | ||||
|       const lowPromise = client.sendSMTPCommand('LOW', { priority: MessagePriority.LOW }); | ||||
|       const highPromise = client.sendSMTPCommand('HIGH', { priority: MessagePriority.HIGH }); | ||||
|       const normalPromise = client.sendSMTPCommand('NORMAL', { priority: MessagePriority.NORMAL }); | ||||
| 
 | ||||
|       expect(client.getQueueSize()).toBe(3); | ||||
| 
 | ||||
|       // Clean up
 | ||||
|       client.clearQueue(); | ||||
|     }); | ||||
| 
 | ||||
|     it('should reject when client is shutting down', async () => { | ||||
|       const shutdownPromise = client.shutdown(); | ||||
|        | ||||
|       await expect(client.sendSMTPCommand('TEST')).rejects.toThrow('Client is shutting down'); | ||||
|       await shutdownPromise; | ||||
|     }); | ||||
| 
 | ||||
|     it('should reject when queue is full', async () => { | ||||
|       const smallQueueClient = new SMTPOverWSClient({ | ||||
|         url: 'ws://localhost:3000/smtp', | ||||
|         apiKey: 'test-key', | ||||
|         maxQueueSize: 2 | ||||
|       }); | ||||
| 
 | ||||
|       // Fill queue
 | ||||
|       smallQueueClient.sendSMTPCommand('MSG1'); | ||||
|       smallQueueClient.sendSMTPCommand('MSG2'); | ||||
|        | ||||
|       // This should fail
 | ||||
|       await expect(smallQueueClient.sendSMTPCommand('MSG3')).rejects.toThrow('Queue is full'); | ||||
|        | ||||
|       await smallQueueClient.shutdown(); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('statistics', () => { | ||||
|     it('should provide initial statistics', () => { | ||||
|       const stats = client.getStats(); | ||||
|        | ||||
|       expect(stats).toEqual({ | ||||
|         messagesQueued: 0, | ||||
|         messagesProcessed: 0, | ||||
|         messagesFailed: 0, | ||||
|         reconnectionAttempts: 0, | ||||
|         totalConnections: 0, | ||||
|         averageResponseTime: 0, | ||||
|         queueSize: 0, | ||||
|         connectionUptime: 0 | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('queue management', () => { | ||||
|     it('should clear queue', () => { | ||||
|       client.sendSMTPCommand('MSG1'); | ||||
|       client.sendSMTPCommand('MSG2'); | ||||
|        | ||||
|       expect(client.getQueueSize()).toBe(2); | ||||
|        | ||||
|       client.clearQueue(); | ||||
|       expect(client.getQueueSize()).toBe(0); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('shutdown', () => { | ||||
|     it('should shutdown gracefully', async () => { | ||||
|       await expect(client.shutdown()).resolves.toBeUndefined(); | ||||
|       expect(client.getConnectionState()).toBe(ConnectionState.DISCONNECTED); | ||||
|     }); | ||||
| 
 | ||||
|     it('should timeout if shutdown takes too long', async () => { | ||||
|       await expect(client.shutdown(100)).resolves.toBeUndefined(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										15
									
								
								tests/setup.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								tests/setup.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| /** | ||||
|  * Jest test setup | ||||
|  */ | ||||
| 
 | ||||
| // Extend Jest timeout for integration tests
 | ||||
| jest.setTimeout(30000); | ||||
| 
 | ||||
| // Mock WebSocket for tests
 | ||||
| (global as any).WebSocket = jest.fn().mockImplementation(() => ({ | ||||
|   send: jest.fn(), | ||||
|   close: jest.fn(), | ||||
|   terminate: jest.fn(), | ||||
|   on: jest.fn(), | ||||
|   readyState: 1, // OPEN
 | ||||
| })); | ||||
							
								
								
									
										223
									
								
								tests/transport.test.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								tests/transport.test.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,223 @@ | |||
| import nodemailer from 'nodemailer'; | ||||
| import { SMTPWSTransport, createTransport } from '../src/transport'; | ||||
| 
 | ||||
| describe('SMTPWSTransport', () => { | ||||
|   let transport: SMTPWSTransport; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     transport = createTransport({ | ||||
|       host: 'localhost', | ||||
|       port: 3000, | ||||
|       auth: { | ||||
|         user: 'test-api-key' | ||||
|       }, | ||||
|       debug: false | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(async () => { | ||||
|     await transport.close(); | ||||
|   }); | ||||
| 
 | ||||
|   describe('constructor', () => { | ||||
|     it('should create transport with correct configuration', () => { | ||||
|       expect(transport.name).toBe('SMTPWS'); | ||||
|       expect(transport.version).toBe('1.0.0'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should handle secure connection configuration', () => { | ||||
|       const secureTransport = createTransport({ | ||||
|         host: 'localhost', | ||||
|         port: 443, | ||||
|         secure: true, | ||||
|         auth: { | ||||
|           user: 'test-key' | ||||
|         }, | ||||
|         debug: false | ||||
|       }); | ||||
| 
 | ||||
|       const info = secureTransport.getTransportInfo(); | ||||
|       expect(info.secure).toBe(true); | ||||
|       expect(info.port).toBe(443); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('getTransportInfo', () => { | ||||
|     it('should return transport information', () => { | ||||
|       const info = transport.getTransportInfo(); | ||||
|        | ||||
|       expect(info).toMatchObject({ | ||||
|         name: 'SMTPWS', | ||||
|         version: '1.0.0', | ||||
|         host: 'localhost', | ||||
|         port: 3000, | ||||
|         secure: false | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('send', () => { | ||||
|     it('should send mail message', async () => { | ||||
|       const mockMail = { | ||||
|         data: { | ||||
|           envelope: { | ||||
|             from: 'test@example.com', | ||||
|             to: ['recipient@example.com'] | ||||
|           }, | ||||
|           raw: 'Subject: Test\r\n\r\nTest message' | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       // Mock the internal client
 | ||||
|       const mockSendCommand = jest.fn() | ||||
|         .mockResolvedValueOnce('250 Hello')  // EHLO
 | ||||
|         .mockResolvedValueOnce('250 OK')     // MAIL FROM
 | ||||
|         .mockResolvedValueOnce('250 OK')     // RCPT TO
 | ||||
|         .mockResolvedValueOnce('354 Start mail input') // DATA
 | ||||
|         .mockResolvedValueOnce('250 Message accepted')  // Message content
 | ||||
|         .mockResolvedValueOnce('221 Bye');   // QUIT
 | ||||
| 
 | ||||
|       (transport as any).client.sendSMTPCommand = mockSendCommand; | ||||
| 
 | ||||
|       const result = await transport.send(mockMail); | ||||
| 
 | ||||
|       expect(result).toMatchObject({ | ||||
|         envelope: mockMail.data.envelope, | ||||
|         accepted: ['recipient@example.com'], | ||||
|         rejected: [], | ||||
|         pending: [] | ||||
|       }); | ||||
| 
 | ||||
|       expect(mockSendCommand).toHaveBeenCalledTimes(6); | ||||
|     }); | ||||
| 
 | ||||
|     it('should handle rejected recipients', async () => { | ||||
|       const mockMail = { | ||||
|         data: { | ||||
|           envelope: { | ||||
|             from: 'test@example.com', | ||||
|             to: ['good@example.com', 'bad@example.com'] | ||||
|           }, | ||||
|           raw: 'Subject: Test\r\n\r\nTest message' | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       const mockSendCommand = jest.fn() | ||||
|         .mockResolvedValueOnce('250 Hello')  // EHLO
 | ||||
|         .mockResolvedValueOnce('250 OK')     // MAIL FROM
 | ||||
|         .mockResolvedValueOnce('250 OK')     // RCPT TO (good)
 | ||||
|         .mockResolvedValueOnce('550 No such user') // RCPT TO (bad)
 | ||||
|         .mockResolvedValueOnce('354 Start mail input') // DATA
 | ||||
|         .mockResolvedValueOnce('250 Message accepted')  // Message content
 | ||||
|         .mockResolvedValueOnce('221 Bye');   // QUIT
 | ||||
| 
 | ||||
|       (transport as any).client.sendSMTPCommand = mockSendCommand; | ||||
| 
 | ||||
|       const result = await transport.send(mockMail); | ||||
| 
 | ||||
|       expect(result.accepted).toEqual(['good@example.com']); | ||||
|       expect(result.rejected).toEqual(['bad@example.com']); | ||||
|     }); | ||||
| 
 | ||||
|     it('should call callback on success', (done) => { | ||||
|       const mockMail = { | ||||
|         data: { | ||||
|           envelope: { | ||||
|             from: 'test@example.com', | ||||
|             to: ['recipient@example.com'] | ||||
|           }, | ||||
|           raw: 'Test message' | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       const mockSendCommand = jest.fn() | ||||
|         .mockResolvedValueOnce('250 Hello') | ||||
|         .mockResolvedValueOnce('250 OK') | ||||
|         .mockResolvedValueOnce('250 OK') | ||||
|         .mockResolvedValueOnce('354 Start mail input') | ||||
|         .mockResolvedValueOnce('250 Message accepted') | ||||
|         .mockResolvedValueOnce('221 Bye'); | ||||
| 
 | ||||
|       (transport as any).client.sendSMTPCommand = mockSendCommand; | ||||
| 
 | ||||
|       transport.send(mockMail, (err, info) => { | ||||
|         expect(err).toBeNull(); | ||||
|         expect(info).toBeDefined(); | ||||
|         expect(info?.accepted).toEqual(['recipient@example.com']); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|     it('should call callback on error', (done) => { | ||||
|       const mockMail = { | ||||
|         envelope: { | ||||
|           from: 'test@example.com', | ||||
|           to: ['recipient@example.com'] | ||||
|         }, | ||||
|         raw: 'Test message' | ||||
|       }; | ||||
| 
 | ||||
|       const mockSendCommand = jest.fn() | ||||
|         .mockRejectedValueOnce(new Error('Connection failed')); | ||||
| 
 | ||||
|       (transport as any).client.sendSMTPCommand = mockSendCommand; | ||||
| 
 | ||||
|       transport.send(mockMail, (err, info) => { | ||||
|         expect(err).toBeDefined(); | ||||
|         expect(err?.message).toBe('Connection failed'); | ||||
|         expect(info).toBeUndefined(); | ||||
|         done(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('verify', () => { | ||||
|     it('should verify transport connectivity', async () => { | ||||
|       const mockSendCommand = jest.fn() | ||||
|         .mockResolvedValueOnce('250 Hello'); | ||||
| 
 | ||||
|       (transport as any).client.sendSMTPCommand = mockSendCommand; | ||||
| 
 | ||||
|       const result = await transport.verify(); | ||||
|       expect(result).toBe(true); | ||||
|       expect(mockSendCommand).toHaveBeenCalledWith('EHLO transport-verify\r\n'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw error on verification failure', async () => { | ||||
|       const mockSendCommand = jest.fn() | ||||
|         .mockRejectedValueOnce(new Error('Connection refused')); | ||||
| 
 | ||||
|       (transport as any).client.sendSMTPCommand = mockSendCommand; | ||||
| 
 | ||||
|       await expect(transport.verify()).rejects.toThrow('Transport verification failed'); | ||||
|     }); | ||||
| 
 | ||||
|     it('should throw error on missing API key during construction', async () => { | ||||
|       expect(() => createTransport({ | ||||
|         host: 'localhost', | ||||
|         auth: { user: '' }, | ||||
|         debug: false | ||||
|       })).toThrow('API key is required'); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('isIdle', () => { | ||||
|     it('should return true when transport is idle', () => { | ||||
|       expect(transport.isIdle()).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     it('should return false when transport has queued messages', () => { | ||||
|       // Mock queue size
 | ||||
|       (transport as any).client.getQueueSize = jest.fn().mockReturnValue(5); | ||||
|        | ||||
|       expect(transport.isIdle()).toBe(false); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('nodemailer integration', () => { | ||||
|     it('should work as nodemailer transport', async () => { | ||||
|       const transporter = nodemailer.createTransport(transport); | ||||
|       expect(transporter).toBeDefined(); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
							
								
								
									
										10
									
								
								tsconfig.cjs.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tsconfig.cjs.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| { | ||||
|   "extends": "./tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "module": "CommonJS", | ||||
|     "target": "ES2018", | ||||
|     "outDir": "./lib", | ||||
|     "declaration": false, | ||||
|     "declarationMap": false | ||||
|   } | ||||
| } | ||||
							
								
								
									
										10
									
								
								tsconfig.esm.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								tsconfig.esm.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,10 @@ | |||
| { | ||||
|   "extends": "./tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "module": "ES2020", | ||||
|     "target": "ES2020", | ||||
|     "outDir": "./lib-esm", | ||||
|     "declaration": false, | ||||
|     "declarationMap": false | ||||
|   } | ||||
| } | ||||
							
								
								
									
										37
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								tsconfig.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| { | ||||
|   "compilerOptions": { | ||||
|     "target": "ES2020", | ||||
|     "lib": ["ES2020"], | ||||
|     "module": "CommonJS", | ||||
|     "moduleResolution": "node", | ||||
|     "resolveJsonModule": true, | ||||
|     "allowSyntheticDefaultImports": true, | ||||
|     "esModuleInterop": true, | ||||
|     "allowJs": true, | ||||
|     "sourceMap": true, | ||||
|     "outDir": "./lib", | ||||
|     "rootDir": "./src", | ||||
|     "removeComments": false, | ||||
|     "strict": true, | ||||
|     "noImplicitAny": true, | ||||
|     "strictNullChecks": true, | ||||
|     "strictFunctionTypes": true, | ||||
|     "noImplicitReturns": true, | ||||
|     "noFallthroughCasesInSwitch": true, | ||||
|     "noUncheckedIndexedAccess": true, | ||||
|     "exactOptionalPropertyTypes": true, | ||||
|     "declaration": true, | ||||
|     "declarationMap": true, | ||||
|     "skipLibCheck": true, | ||||
|     "forceConsistentCasingInFileNames": true | ||||
|   }, | ||||
|   "include": [ | ||||
|     "src/**/*" | ||||
|   ], | ||||
|   "exclude": [ | ||||
|     "node_modules", | ||||
|     "lib", | ||||
|     "tests", | ||||
|     "examples" | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										9
									
								
								tsconfig.types.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								tsconfig.types.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| { | ||||
|   "extends": "./tsconfig.json", | ||||
|   "compilerOptions": { | ||||
|     "declaration": true, | ||||
|     "declarationMap": true, | ||||
|     "emitDeclarationOnly": true, | ||||
|     "outDir": "./lib" | ||||
|   } | ||||
| } | ||||
							
								
								
									
										71
									
								
								workflows/ci.yml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								workflows/ci.yml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| name: CI | ||||
| 
 | ||||
| on: | ||||
|   push: | ||||
|     branches: [ main, develop ] | ||||
|   pull_request: | ||||
|     branches: [ main, develop ] | ||||
| 
 | ||||
| jobs: | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
| 
 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         node-version: [16, 18, 20] | ||||
| 
 | ||||
|     steps: | ||||
|     - name: Checkout code | ||||
|       uses: actions/checkout@v4 | ||||
| 
 | ||||
|     - name: Use Node.js ${{ matrix.node-version }} | ||||
|       uses: actions/setup-node@v4 | ||||
|       with: | ||||
|         node-version: ${{ matrix.node-version }} | ||||
|         cache: 'npm' | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|       run: npm ci | ||||
| 
 | ||||
|     - name: Run linter | ||||
|       run: npm run lint | ||||
| 
 | ||||
|     - name: Run tests | ||||
|       run: npm test | ||||
| 
 | ||||
|     - name: Run build | ||||
|       run: npm run build | ||||
| 
 | ||||
|     - name: Upload coverage to Codecov | ||||
|       if: matrix.node-version == 18 | ||||
|       uses: codecov/codecov-action@v3 | ||||
|       with: | ||||
|         file: ./coverage/lcov.info | ||||
|         flags: unittests | ||||
|         name: codecov-umbrella | ||||
| 
 | ||||
|   publish: | ||||
|     needs: test | ||||
|     runs-on: ubuntu-latest | ||||
|     if: github.ref == 'refs/heads/main' && github.event_name == 'push' | ||||
| 
 | ||||
|     steps: | ||||
|     - name: Checkout code | ||||
|       uses: actions/checkout@v4 | ||||
| 
 | ||||
|     - name: Use Node.js 18 | ||||
|       uses: actions/setup-node@v4 | ||||
|       with: | ||||
|         node-version: 18 | ||||
|         registry-url: 'https://registry.npmjs.org' | ||||
| 
 | ||||
|     - name: Install dependencies | ||||
|       run: npm ci | ||||
| 
 | ||||
|     - name: Build | ||||
|       run: npm run build | ||||
| 
 | ||||
|     - name: Publish to NPM | ||||
|       run: npm publish | ||||
|       env: | ||||
|         NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue