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