feat: Update Nodemailer transport to support optional host and apiKey parameters, enhance error handling, and improve example usage
This commit is contained in:
parent
26d11289ea
commit
79b8013b5e
4 changed files with 395 additions and 344 deletions
35
CLAUDE.md
35
CLAUDE.md
|
@ -8,27 +8,34 @@ This is **@siwatsystem/mxrelay-consumer**, a TypeScript client library for SMTP
|
||||||
|
|
||||||
## Development Commands
|
## 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
513
README.md
|
@ -1,142 +1,169 @@
|
||||||
# @siwatsystem/smtp-ws-relay-client
|
# @siwatsystem/mxrelay-consumer
|
||||||
|
|
||||||
[](https://badge.fury.io/js/@siwatsystem%2Fsmtp-ws-relay-client)
|
|
||||||
[](http://www.typescriptlang.org/)
|
[](http://www.typescriptlang.org/)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](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 {
|
||||||
|
|
||||||
// Graceful shutdown
|
|
||||||
await client.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
|
|
@ -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: `
|
||||||
|
|
168
src/transport.ts
168
src/transport.ts
|
@ -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 });
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue