From 79b8013b5e3d8e3edb8d6022ba8a0dba8f9ea249 Mon Sep 17 00:00:00 2001 From: Siwat Sirichai Date: Tue, 19 Aug 2025 02:30:52 +0700 Subject: [PATCH] feat: Update Nodemailer transport to support optional host and apiKey parameters, enhance error handling, and improve example usage --- CLAUDE.md | 35 ++- README.md | 521 ++++++++++++++----------------- examples/nodemailer-transport.ts | 13 +- src/transport.ts | 170 ++++++++-- 4 files changed, 395 insertions(+), 344 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 718e743..097c3c7 100644 --- a/CLAUDE.md +++ b/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 diff --git a/README.md b/README.md index 52c90e6..3a58562 100644 --- a/README.md +++ b/README.md @@ -1,142 +1,169 @@ -# @siwatsystem/smtp-ws-relay-client +# @siwatsystem/mxrelay-consumer -[![npm version](https://badge.fury.io/js/@siwatsystem%2Fsmtp-ws-relay-client.svg)](https://badge.fury.io/js/@siwatsystem%2Fsmtp-ws-relay-client) [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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: \\r\\n'); - const response3 = await client.sendSMTPCommand('RCPT TO: \\r\\n'); - - console.log('SMTP responses:', [response1, response2, response3]); + const response = await client.sendSMTPCommand(` + MAIL FROM: + RCPT TO: + 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: '

HTML version

', + 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: '); } 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` -- `getStats(): ClientStats` -- `getConnectionState(): ConnectionState` -- `getQueueSize(): number` -- `clearQueue(): void` -- `shutdown(timeout?: number): Promise` +- `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) \ No newline at end of file +Built by SiwatSystem \ No newline at end of file diff --git a/examples/nodemailer-transport.ts b/examples/nodemailer-transport.ts index e212af1..f783cb1 100644 --- a/examples/nodemailer-transport.ts +++ b/examples/nodemailer-transport.ts @@ -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: ` diff --git a/src/transport.ts b/src/transport.ts index 8ca72f6..90f26e2 100644 --- a/src/transport.ts +++ b/src/transport.ts @@ -13,7 +13,7 @@ import { Readable } from 'stream'; */ export interface TransportOptions extends Omit { /** WebSocket server URL */ - host: string; + host?: string; /** WebSocket server port */ port?: number; @@ -22,7 +22,7 @@ export interface TransportOptions extends Omit 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): SMTPWSTransport { + return new SMTPWSTransport({ apiKey, ...options }); } \ No newline at end of file