feat: Update Nodemailer transport to support optional host and apiKey parameters, enhance error handling, and improve example usage

This commit is contained in:
Siwat Sirichai 2025-08-19 02:30:52 +07:00
parent 26d11289ea
commit 79b8013b5e
4 changed files with 395 additions and 344 deletions

View file

@ -8,27 +8,34 @@ This is **@siwatsystem/mxrelay-consumer**, a TypeScript client library for SMTP
## Development Commands ## Development Commands
**This project uses Bun as the primary runtime and package manager. TypeScript files can be run directly without building.**
### Build Commands ### Build Commands
- `npm run build` - Build all formats (CommonJS, ESM, and type declarations) - `bun run build` - Build all formats (CommonJS, ESM, and type declarations)
- `npm run build:cjs` - Build CommonJS format only - `bun run build:cjs` - Build CommonJS format only
- `npm run build:esm` - Build ES modules format only - `bun run build:esm` - Build ES modules format only
- `npm run build:types` - Build TypeScript declarations only - `bun run build:types` - Build TypeScript declarations only
- `npm run dev` - Watch mode for development - `bun run dev` - Watch mode for development
- `npm run clean` - Remove build artifacts - `bun run clean` - Remove build artifacts
### Testing Commands ### Testing Commands
- `npm test` - Run all tests - `bun test` - Run all tests
- `npm run test:watch` - Run tests in watch mode - `bun run test:watch` - Run tests in watch mode
- `npm run test:coverage` - Run tests with coverage report - `bun run test:coverage` - Run tests with coverage report
### Code Quality Commands ### Code Quality Commands
- `npm run lint` - Lint TypeScript files - `bun run lint` - Lint TypeScript files
- `npm run lint:fix` - Lint and auto-fix issues - `bun run lint:fix` - Lint and auto-fix issues
- `npm run format` - Format code with Prettier - `bun run format` - Format code with Prettier
### Example Commands ### Example Commands
- `npm run example:basic` - Run basic usage example - `bun run examples/nodemailer-transport.ts` - Run Nodemailer transport example directly
- `npm run example:queue` - Run queue management example - `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 ## Architecture

513
README.md
View file

@ -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/) [![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) [![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 ## Features
🚀 **Intelligent Queue Management** **Intelligent Queue Management**
- Automatic WebSocket connection when messages are queued - Automatic WebSocket connection when messages are queued
- Priority-based message processing - Priority-based message processing (CRITICAL > HIGH > NORMAL > LOW)
- Configurable batch processing - Configurable queue limits and overflow protection
- Queue size limits and overflow protection - Auto-disconnect when queue is empty
🔄 **Robust Connection Handling** **Robust Connection Handling**
- Automatic reconnection with exponential backoff - Automatic reconnection with exponential backoff
- Connection state management - Connection state management and lifecycle
- Heartbeat monitoring - Heartbeat monitoring and timeout handling
- Graceful connection lifecycle - Graceful connection recovery
⚡ **High Performance** **High Performance**
- Efficient SMTP channel cycling - Efficient SMTP channel cycling per message
- Minimal resource usage with smart connection management
- Concurrent message processing support - Concurrent message processing support
- Optimized resource usage - Optimized WebSocket communication
- Minimal memory footprint
🛡️ **Enterprise-Grade Reliability** **Enterprise-Grade Reliability**
- Comprehensive error handling and classification - Comprehensive SMTP error handling with meaningful messages
- Timeout management for all operations - Timeout management for all operations
- Retry logic with configurable attempts - Retry logic with configurable attempts
- Graceful degradation and recovery - Structured error classification
📊 **Monitoring & Observability** **Nodemailer Integration**
- Real-time statistics and metrics - Full Nodemailer transport compatibility
- Structured logging with configurable levels - Transparent bridge for all email features
- Event-driven architecture for monitoring - Support for attachments, HTML, multipart messages
- Performance tracking and analytics - Standard Nodemailer API compatibility
## Installation ## Installation
```bash ```bash
npm install @siwatsystem/smtp-ws-relay-client npm install @siwatsystem/mxrelay-consumer
# or
bun add @siwatsystem/mxrelay-consumer
``` ```
## Quick Start ## Quick Start
```typescript ### Direct Client Usage
import { SMTPOverWSClient, MessagePriority } from '@siwatsystem/smtp-ws-relay-client';
```typescript
import { SMTPOverWSClient } from '@siwatsystem/mxrelay-consumer';
// Create client instance
const client = new SMTPOverWSClient({ const client = new SMTPOverWSClient({
url: 'ws://your-smtp-relay-server:3000/smtp', url: 'wss://api.siwatsystem.com/smtp',
apiKey: 'your-api-key', apiKey: 'your-api-key',
debug: true debug: true
}); });
// Send SMTP commands // Send SMTP commands directly
try { try {
const response1 = await client.sendSMTPCommand('EHLO example.com\\r\\n'); const response = await client.sendSMTPCommand(`
const response2 = await client.sendSMTPCommand('MAIL FROM: <test@example.com>\\r\\n'); MAIL FROM: <sender@example.com>
const response3 = await client.sendSMTPCommand('RCPT TO: <recipient@example.com>\\r\\n'); RCPT TO: <recipient@example.com>
DATA
Subject: Test Email
console.log('SMTP responses:', [response1, response2, response3]); Hello from SMTP over WebSocket!
.
QUIT
`);
console.log('Email sent:', response);
} catch (error) { } catch (error) {
console.error('SMTP error:', error); console.error('SMTP error:', error.message);
} finally {
await client.shutdown();
} }
// Graceful shutdown
await client.shutdown();
``` ```
## Configuration Options ### Nodemailer Transport
```typescript
import nodemailer from 'nodemailer';
import { createTransport } from '@siwatsystem/mxrelay-consumer';
// Create transport (uses defaults: api.siwatsystem.com:443 secure)
const transport = createTransport('your-api-key');
// Or with custom options
const transport = createTransport('your-api-key', {
host: 'custom.server.com',
port: 80,
secure: false,
debug: true
});
const transporter = nodemailer.createTransporter(transport);
// Send email using standard Nodemailer API
const info = await transporter.sendMail({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Test Email via SMTP WebSocket',
text: 'Plain text version',
html: '<h1>HTML version</h1>',
attachments: [
{
filename: 'document.pdf',
path: './document.pdf'
}
]
});
console.log('Message sent:', info.messageId);
await transport.close();
```
## Configuration
### Client Configuration
```typescript ```typescript
interface SMTPClientConfig { interface SMTPClientConfig {
/** WebSocket server URL */ url: string; // WebSocket server URL
url: string; 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)
}
```
/** API key for authentication */ ### Transport Configuration
apiKey: string;
/** Interval between reconnection attempts (default: 5000ms) */ ```typescript
reconnectInterval?: number; interface TransportOptions {
host?: string; // Server host (default: 'api.siwatsystem.com')
/** Maximum reconnection attempts (default: 10) */ port?: number; // Server port (default: 443)
maxReconnectAttempts?: number; secure?: boolean; // Use wss:// (default: true)
debug?: boolean; // Enable debug mode (default: false)
/** Authentication timeout (default: 30000ms) */ // ... other SMTPClientConfig options
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 ## Advanced Usage
### Priority-Based Message Queuing ### Priority-Based Messaging
```typescript ```typescript
import { MessagePriority } from '@siwatsystem/smtp-ws-relay-client'; import { MessagePriority } from '@siwatsystem/mxrelay-consumer';
// High priority message (processed first) // High priority (processed first)
await client.sendSMTPCommand('URGENT EMAIL DATA\\r\\n', { await client.sendSMTPCommand('URGENT EMAIL DATA', {
priority: MessagePriority.HIGH, priority: MessagePriority.HIGH,
timeout: 30000 timeout: 30000
}); });
// Normal priority message // Critical priority (highest)
await client.sendSMTPCommand('NORMAL EMAIL DATA\\r\\n', { await client.sendSMTPCommand('CRITICAL ALERT EMAIL', {
priority: MessagePriority.NORMAL priority: MessagePriority.CRITICAL
});
// Low priority message (processed last)
await client.sendSMTPCommand('NEWSLETTER DATA\\r\\n', {
priority: MessagePriority.LOW
}); });
``` ```
@ -144,17 +171,9 @@ await client.sendSMTPCommand('NEWSLETTER DATA\\r\\n', {
```typescript ```typescript
// Connection events // Connection events
client.on('connected', () => { client.on('connected', () => console.log('WebSocket connected'));
console.log('WebSocket connected'); client.on('authenticated', () => console.log('Authentication successful'));
}); client.on('disconnected', () => console.log('Connection lost'));
client.on('authenticated', () => {
console.log('Authentication successful');
});
client.on('disconnected', (reason) => {
console.log('Disconnected:', reason);
});
// Queue events // Queue events
client.on('messageQueued', (messageId, queueSize) => { client.on('messageQueued', (messageId, queueSize) => {
@ -165,152 +184,140 @@ client.on('messageProcessed', (messageId, responseTime) => {
console.log(`Message ${messageId} processed in ${responseTime}ms`); console.log(`Message ${messageId} processed in ${responseTime}ms`);
}); });
client.on('queueProcessingStarted', (queueSize) => {
console.log(`Processing ${queueSize} queued messages`);
});
// Error events // Error events
client.on('error', (error) => { client.on('error', (error) => console.error('Client error:', error));
console.error('Client error:', error);
});
client.on('messageFailed', (messageId, error) => {
console.error(`Message ${messageId} failed:`, error);
});
``` ```
### Statistics and Monitoring ### Statistics and Monitoring
```typescript ```typescript
// Get real-time statistics
const stats = client.getStats(); const stats = client.getStats();
console.log('Client Statistics:', { console.log('Client Statistics:', {
messagesQueued: stats.messagesQueued, messagesQueued: stats.messagesQueued,
messagesProcessed: stats.messagesProcessed, messagesProcessed: stats.messagesProcessed,
messagesFailed: stats.messagesFailed, messagesFailed: stats.messagesFailed,
averageResponseTime: stats.averageResponseTime, averageResponseTime: stats.averageResponseTime,
connectionUptime: stats.connectionUptime,
queueSize: stats.queueSize 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 ## 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 ```typescript
import { import {
ConnectionError, ConnectionError,
AuthenticationError, AuthenticationError,
ChannelError, MessageError,
TimeoutError, TimeoutError
QueueError, } from '@siwatsystem/mxrelay-consumer';
MessageError
} from '@siwatsystem/smtp-ws-relay-client';
try { try {
await client.sendSMTPCommand('EHLO example.com\\r\\n'); await client.sendSMTPCommand('MAIL FROM: <test@example.com>');
} catch (error) { } catch (error) {
if (error instanceof ConnectionError) { if (error instanceof ConnectionError) {
console.error('Connection failed:', error.message); console.error('Connection failed:', error.message);
} else if (error instanceof AuthenticationError) { } else if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message); 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) { } else if (error instanceof TimeoutError) {
console.error('Operation timed out:', error.message); 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 ## 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 - `CONNECTING` - Establishing WebSocket connection
- `CONNECTED` - WebSocket connected, authentication pending - `CONNECTED` - WebSocket connected, authentication pending
- `AUTHENTICATING` - Sending authentication credentials - `AUTHENTICATING` - Sending credentials
- `AUTHENTICATED` - Ready to open SMTP channels - `AUTHENTICATED` - Ready for SMTP operations
- `CHANNEL_OPENING` - Opening SMTP channel - `CHANNEL_OPENING` - Opening SMTP channel
- `CHANNEL_READY` - SMTP channel active, ready for data - `CHANNEL_READY` - SMTP channel active
- `CHANNEL_CLOSED` - SMTP channel closed - `CHANNEL_CLOSED` - SMTP channel closed
- `CHANNEL_ERROR` - SMTP channel error occurred - `RECONNECTING` - Attempting reconnection
- `RECONNECTING` - Attempting to reconnect
- `FAILED` - Connection failed, max retries reached - `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 - `AUTHENTICATE` / `AUTHENTICATE_RESPONSE` - Authentication flow
- `SMTP_CHANNEL_OPEN` / `SMTP_CHANNEL_READY` - Channel management - `SMTP_CHANNEL_OPEN` / `SMTP_CHANNEL_READY` - Channel management
- `SMTP_CHANNEL_CLOSED` / `SMTP_CHANNEL_ERROR` - Channel lifecycle - `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 ### Connection Flow
1. **WebSocket Connection** - Establish WebSocket to relay server 1. **WebSocket Connection** - Connect to relay server
2. **Authentication** - Authenticate using API key 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 4. **Data Transfer** - Exchange SMTP commands and responses
5. **Cleanup** - Close channels and connection when queue empty 5. **Cleanup** - Close channel and disconnect 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 ## Best Practices
@ -318,41 +325,15 @@ This client implements the SMTP over WebSocket protocol specification:
```typescript ```typescript
const client = new SMTPOverWSClient({ const client = new SMTPOverWSClient({
url: 'ws://your-production-server/smtp', url: 'wss://api.siwatsystem.com/smtp',
apiKey: process.env.SMTP_RELAY_API_KEY!, apiKey: process.env.MXRELAY_API_KEY,
// Connection settings // Production settings
debug: false,
maxQueueSize: 5000,
reconnectInterval: 10000, reconnectInterval: 10000,
maxReconnectAttempts: 5, maxReconnectAttempts: 5,
heartbeatInterval: 30000, messageTimeout: 120000
// 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
}
}); });
``` ```
@ -360,99 +341,51 @@ client.on('error', async (error) => {
```typescript ```typescript
process.on('SIGTERM', async () => { process.on('SIGTERM', async () => {
console.log('Shutting down SMTP client...'); console.log('Shutting down...');
try { try {
await client.shutdown(30000); // 30 second timeout await client.shutdown(30000); // 30 second timeout
console.log('SMTP client shutdown complete'); console.log('Shutdown complete');
} catch (error) { } catch (error) {
console.error('Forced shutdown due to timeout'); console.error('Forced shutdown');
} }
process.exit(0); 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 ## API Reference
### createTransport(apiKey, options?)
Creates a Nodemailer-compatible transport.
- `apiKey` - Your API key for authentication
- `options` - Optional transport configuration
### SMTPOverWSClient ### SMTPOverWSClient
#### Constructor Main client class for direct SMTP operations.
- `new SMTPOverWSClient(config: SMTPClientConfig)`
#### Methods #### Methods
- `sendSMTPCommand(data: string, options?: SendOptions): Promise<string>` - `sendSMTPCommand(data, options?)` - Send SMTP command
- `getStats(): ClientStats` - `getStats()` - Get client statistics
- `getConnectionState(): ConnectionState` - `getConnectionState()` - Get current state
- `getQueueSize(): number` - `getQueueSize()` - Get queue size
- `clearQueue(): void` - `shutdown(timeout?)` - Graceful shutdown
- `shutdown(timeout?: number): Promise<void>`
#### Events #### Events
- `connecting`, `connected`, `authenticated`, `disconnected` - Connection: `connecting`, `connected`, `authenticated`, `disconnected`
- `reconnecting`, `reconnected`, `error` - Queue: `messageQueued`, `messageProcessed`, `messageFailed`
- `messageQueued`, `messageProcessed`, `messageFailed` - State: `stateChanged`, `error`
- `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 ## 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 ## Support
- 📧 Email: [support@siwatsystem.com](mailto:support@siwatsystem.com) - Issues: [Git Repository Issues](https://git.siwatsystem.com/siwat/mxrelay-consumer/issues)
- 🐛 Issues: [GitHub Issues](https://github.com/siwatsystem/smtp-ws-relay-client/issues) - Documentation: [Project Repository](https://git.siwatsystem.com/siwat/mxrelay-consumer)
- 📖 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) Built by SiwatSystem

View file

@ -11,13 +11,8 @@ async function nodemailerTransportExample() {
console.log('Nodemailer SMTP WebSocket Transport Example\n'); console.log('Nodemailer SMTP WebSocket Transport Example\n');
// Create the WebSocket transport // Create the WebSocket transport
const transport = createTransport({ const apiKey = process.env.MXRELAY_API_KEY || 'your-api-key-here';
host: '192.168.0.62', const transport = createTransport(apiKey, {debug: false });
apiKey: 'cebc9a7f-4e0c-4fda-9dd0-85f48c02800c',
port: 80,
secure: false, // Set to true for wss://
debug: true
});
// Create Nodemailer transporter // Create Nodemailer transporter
const transporter = nodemailer.createTransport(transport); const transporter = nodemailer.createTransport(transport);
@ -37,8 +32,8 @@ async function nodemailerTransportExample() {
console.log('Sending test email...'); console.log('Sending test email...');
const info = await transporter.sendMail({ const info = await transporter.sendMail({
from: 'cudconnex@satitm.chula.ac.th', from: 'sender@example.com',
to: 'siwat.s@chula.ac.th', to: 'recipient@example.com',
subject: 'Test Email via SMTP WebSocket', subject: 'Test Email via SMTP WebSocket',
text: 'This email was sent using the SMTP WebSocket transport!', text: 'This email was sent using the SMTP WebSocket transport!',
html: ` html: `

View file

@ -13,7 +13,7 @@ import { Readable } from 'stream';
*/ */
export interface TransportOptions extends Omit<SMTPClientConfig, 'url' | 'apiKey'> { export interface TransportOptions extends Omit<SMTPClientConfig, 'url' | 'apiKey'> {
/** WebSocket server URL */ /** WebSocket server URL */
host: string; host?: string;
/** WebSocket server port */ /** WebSocket server port */
port?: number; port?: number;
@ -22,7 +22,7 @@ export interface TransportOptions extends Omit<SMTPClientConfig, 'url' | 'apiKey
secure?: boolean; secure?: boolean;
/** API key for authentication */ /** API key for authentication */
apiKey: string; apiKey?: string;
/** Transport name */ /** Transport name */
name?: string; name?: string;
@ -87,26 +87,33 @@ export class SMTPWSTransport extends EventEmitter {
constructor(options: TransportOptions) { constructor(options: TransportOptions) {
super(); super();
this.options = options; // Set defaults
this.options = {
host: 'api.siwatsystem.com',
port: 443,
secure: true,
debug: false,
...options
};
// Convert transport options to client config // Convert transport options to client config
const protocol = options.secure ? 'wss' : 'ws'; const protocol = this.options.secure ? 'wss' : 'ws';
const port = options.port || (options.secure ? 443 : 3000); const port = this.options.port || (this.options.secure ? 443 : 3000);
const url = `${protocol}://${options.host}:${port}/smtp`; const url = `${protocol}://${this.options.host}:${port}/smtp`;
const clientConfig: SMTPClientConfig = { const clientConfig: SMTPClientConfig = {
url, url,
apiKey: options.apiKey, apiKey: this.options.apiKey!,
...(options.debug !== undefined && { debug: options.debug }), ...(this.options.debug !== undefined && { debug: this.options.debug }),
...(options.maxQueueSize !== undefined && { maxQueueSize: options.maxQueueSize }), ...(this.options.maxQueueSize !== undefined && { maxQueueSize: this.options.maxQueueSize }),
...(options.reconnectInterval !== undefined && { reconnectInterval: options.reconnectInterval }), ...(this.options.reconnectInterval !== undefined && { reconnectInterval: this.options.reconnectInterval }),
...(options.maxReconnectAttempts !== undefined && { maxReconnectAttempts: options.maxReconnectAttempts }), ...(this.options.maxReconnectAttempts !== undefined && { maxReconnectAttempts: this.options.maxReconnectAttempts }),
...(options.authTimeout !== undefined && { authTimeout: options.authTimeout }), ...(this.options.authTimeout !== undefined && { authTimeout: this.options.authTimeout }),
...(options.channelTimeout !== undefined && { channelTimeout: options.channelTimeout }), ...(this.options.channelTimeout !== undefined && { channelTimeout: this.options.channelTimeout }),
...(options.messageTimeout !== undefined && { messageTimeout: options.messageTimeout }), ...(this.options.messageTimeout !== undefined && { messageTimeout: this.options.messageTimeout }),
...(options.maxConcurrentMessages !== undefined && { maxConcurrentMessages: options.maxConcurrentMessages }), ...(this.options.maxConcurrentMessages !== undefined && { maxConcurrentMessages: this.options.maxConcurrentMessages }),
...(options.logger !== undefined && { logger: options.logger }), ...(this.options.logger !== undefined && { logger: this.options.logger }),
...(options.heartbeatInterval !== undefined && { heartbeatInterval: options.heartbeatInterval }) ...(this.options.heartbeatInterval !== undefined && { heartbeatInterval: this.options.heartbeatInterval })
}; };
this.client = new SMTPOverWSClient(clientConfig); this.client = new SMTPOverWSClient(clientConfig);
@ -233,14 +240,25 @@ export class SMTPWSTransport extends EventEmitter {
// Send complete SMTP transaction in one session // Send complete SMTP transaction in one session
const response = await this.client.sendSMTPCommand(smtpTransaction); const response = await this.client.sendSMTPCommand(smtpTransaction);
return { // Parse SMTP response for success/failure
envelope, const result = this.parseSmtpResponse(response, envelope, messageId);
// If there were SMTP errors, throw an appropriate error
if (result.rejected.length > 0 || !this.isSuccessfulResponse(response)) {
const errorDetails = this.extractSmtpError(response);
throw new MessageError(
errorDetails.message,
messageId, messageId,
accepted: [...envelope.to], 0,
rejected: [], {
pending: [], smtpCode: errorDetails.code,
response 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 * Create transport instance
*/ */
export function createTransport(options: TransportOptions): SMTPWSTransport { export function createTransport(apiKey: string, options?: Omit<TransportOptions, 'apiKey'>): SMTPWSTransport {
return new SMTPWSTransport(options); return new SMTPWSTransport({ apiKey, ...options });
} }