This commit is contained in:
Philipp Kunz 2025-05-22 10:18:02 +00:00
parent 7c0f9b4e44
commit ac419e7b79
21 changed files with 4541 additions and 1868 deletions

View File

@ -1,224 +1,234 @@
# SMTP Server Refactoring Plan
# SMTP Client Refactoring Plan
## Problem Statement
The SMTP server implementation in `classes.smtpserver.ts` has grown to be too large and complex, with over 1300 lines of code. This makes it difficult to maintain, test, and extend. We need to refactor it into multiple smaller, focused files to improve maintainability and adhere to the single responsibility principle.
Following the successful SMTP server refactoring, the SMTP client implementation in `classes.smtp.client.ts` has grown to be too large and complex, with over 1,421 lines of code. This monolithic structure makes it difficult to maintain, test, and extend. We need to refactor it into multiple smaller, focused files to improve maintainability and achieve consistency with the server architecture.
## Refactoring Goals
1. Improve code organization by splitting the SMTPServer class into multiple focused modules
2. Enhance testability by making components more isolated and easier to mock
1. Improve code organization by splitting the SmtpClient class into multiple focused modules
2. Enhance testability by making components more isolated and easier to mock
3. Improve maintainability by reducing file sizes and complexity
4. Preserve existing functionality and behavior while improving the architecture
4. Achieve architectural consistency with the refactored SMTP server structure
5. Preserve existing functionality and behavior while improving the architecture
## Proposed File Structure
```
mail/
└── delivery/
├── smtp/
├── smtpclient/
│ ├── index.ts - Main export file
│ ├── interfaces.ts - All SMTP-related interfaces
│ ├── constants.ts - Constants and enums
│ ├── smtp-server.ts - Main server class (core functionality)
│ ├── session-manager.ts - Session creation and management
│ ├── command-handler.ts - SMTP command processing
│ ├── data-handler.ts - Email data processing
│ ├── tls-handler.ts - TLS and STARTTLS functionality
│ ├── security-handler.ts - Security checks and validations
│ ├── connection-manager.ts - Connection management and cleanup
│ ├── interfaces.ts - All SMTP client interfaces
│ ├── constants.ts - Constants and error codes
│ ├── smtp-client.ts - Main client class (core functionality)
│ ├── connection-manager.ts - Connection pooling and lifecycle
│ ├── command-handler.ts - SMTP command sending and parsing
│ ├── auth-handler.ts - Authentication mechanisms
│ ├── tls-handler.ts - TLS and STARTTLS client functionality
│ ├── error-handler.ts - Error classification and recovery
│ ├── create-client.ts - Factory function for client creation
│ └── utils/
│ ├── validation.ts - Validation utilities
│ ├── logging.ts - Logging utilities
│ └── helpers.ts - Other helper functions
├── classes.smtpserver.ts - Legacy file (will be deprecated)
│ ├── validation.ts - Input validation utilities
│ ├── logging.ts - Client-side logging utilities
│ └── helpers.ts - Protocol helper functions
├── classes.smtp.client.ts - Legacy file (will be deprecated)
└── interfaces.ts - Main delivery interfaces
```
## Module Responsibilities
### 1. smtp-server.ts (150-200 lines)
- Core server initialization and lifecycle management
- Server startup and shutdown
- Port binding and socket creation
### 1. smtp-client.ts (150-200 lines)
- Core client initialization and lifecycle management
- Client configuration and options handling
- Connection coordination and delegation
- High-level send operations
- Delegates to other modules for specific functionality
### 2. session-manager.ts (100-150 lines)
- Session creation and tracking
- Session timeout management
- Session cleanup
- Session state management
### 2. connection-manager.ts (150-200 lines)
- Connection pooling and reuse
- Socket lifecycle management
- Connection timeout handling
- Connection health monitoring
- Connection error recovery
### 3. command-handler.ts (200-250 lines)
- SMTP command parsing and routing
- Command validation
- Command implementation (EHLO, MAIL FROM, RCPT TO, etc.)
- Response formatting
- SMTP command formatting and sending
- Server response parsing and validation
- Command pipeline management
- Response code interpretation
- Protocol state tracking
### 4. data-handler.ts (150-200 lines)
- Email data collection and processing
- DATA command handling
- MIME parsing
- Email creation and forwarding
- Email storage
### 4. auth-handler.ts (150-200 lines)
- Authentication mechanism selection
- PLAIN, LOGIN, OAUTH2 implementation
- Credential management
- Authentication challenge handling
- Authentication error handling
### 5. tls-handler.ts (100-150 lines)
- TLS connection handling
- STARTTLS implementation
- Certificate management
- Secure socket creation
- TLS connection establishment
- STARTTLS client implementation
- Certificate validation
- Secure socket creation and management
- TLS error handling
### 6. security-handler.ts (100-150 lines)
- IP reputation checking
- Access control
- Rate limiting
- Security logging
- SPAM detection
### 6. error-handler.ts (150-200 lines)
- SMTP error code classification
- Error recovery strategies
- Retry logic implementation
- Error logging and reporting
- Connection failure handling
### 7. connection-manager.ts (100-150 lines)
- Connection tracking and limits
- Idle connection cleanup
- Connection error handling
- Socket event handling
### 7. create-client.ts (50-100 lines)
- Factory function for client creation
- Dependency injection setup
- Configuration validation
- Component initialization
- Default option handling
### 8. interfaces.ts (100-150 lines)
- All interface definitions
- Type aliases
- Type guards
- Enum definitions
- All client interface definitions
- Type aliases for client operations
- Error type definitions
- Configuration interfaces
- Result type definitions
## Implementation Strategy
### Phase 1: Initial Structure and Scaffolding (Days 1-2)
### Phase 1: Initial Structure and Scaffolding (Days 1-2)
1. Create the folder structure and empty files
- Create `mail/delivery/smtp` directory
- Create all module files with basic exports
- Set up barrel file (index.ts)
1. Create the folder structure and empty files
- Create `mail/delivery/smtpclient` directory
- Create all module files with basic exports
- Set up barrel file (index.ts)
2. Move interfaces to the new interfaces.ts file
- Extract all interfaces from current implementation
- Add proper documentation
- Add any missing interface properties
2. Move interfaces to the new interfaces.ts file
- Extract all interfaces from current client implementation
- Add proper documentation for client-specific interfaces
- Add any missing interface properties
3. Extract constants and enums to constants.ts
- Move all constants and enums to dedicated file
- Ensure proper typing and documentation
- Replace magic numbers with named constants
3. Extract constants and enums to constants.ts
- ✅ Move all SMTP client constants and error codes
- Ensure proper typing and documentation
- Replace magic numbers with named constants
4. Set up the basic structure for each module
- Define basic class skeletons
- Set up dependency injection structure
- Document interfaces for each module
4. Set up the basic structure for each module
- Define basic class skeletons for each handler
- Set up dependency injection structure
- Document interfaces for each module
### Phase 2: Gradual Implementation Transfer (Days 3-7)
### Phase 2: Gradual Implementation Transfer (Days 3-7)
1. Start with utility modules
- Implement validation.ts with email validation functions
- Create logging.ts with structured logging utilities
- Build helpers.ts with common helper functions
1. Start with utility modules
- ✅ Implement validation.ts with input validation functions
- ✅ Create logging.ts with client-side logging utilities
- ✅ Build helpers.ts with protocol helper functions
2. Implement session-manager.ts
- Extract session creation and management logic
- Implement timeout handling
- Add session state management functions
2. ✅ Implement error-handler.ts
- ✅ Extract error classification logic
- ✅ Implement retry strategies
- ✅ Add error logging and reporting
3. Implement connection-manager.ts
- Extract connection tracking code
- Implement connection limits
- Add socket event handling logic
3. Implement connection-manager.ts
- ✅ Extract connection pooling code
- ✅ Implement connection lifecycle management
- ✅ Add connection health monitoring
4. Implement command-handler.ts
- Extract command parsing and processing
- Split command handlers into separate methods
- Implement command validation and routing
4. Implement command-handler.ts
- ✅ Extract SMTP command formatting and sending
- ✅ Split response parsing into separate methods
- ✅ Implement command pipeline management
5. Implement data-handler.ts
- Extract email data processing logic
- Implement DATA command handling
- Add email storage and forwarding
5. ✅ Implement auth-handler.ts
- ✅ Extract authentication mechanism logic
- ✅ Implement PLAIN, LOGIN, OAUTH2 handlers
- ✅ Add credential management
6. Implement tls-handler.ts
- Extract TLS connection handling
- Implement STARTTLS functionality
- Add certificate management
6. Implement tls-handler.ts
- Extract TLS client connection handling
- Implement STARTTLS client functionality
- ✅ Add certificate validation
7. Implement security-handler.ts
- Extract IP reputation checking
- Implement security logging
- Add access control functionality
7. ✅ Implement create-client.ts
- ✅ Create factory function for client creation
- ✅ Implement dependency injection setup
- ✅ Add configuration validation
### Phase 3: Core Server Refactoring (Days 8-10)
### Phase 3: Core Client Refactoring (Days 8-10) ✅
1. Refactor the main SMTPServer class
- Update constructor to create and initialize components
- Implement dependency injection for all modules
- Delegate functionality to appropriate modules
- Reduce core class to server lifecycle management
1. ✅ Refactor the main SmtpClient class
- Update constructor to create and initialize components
- Implement dependency injection for all modules
- Delegate functionality to appropriate modules
- ✅ Reduce core class to client lifecycle management
2. Update event handling
- Ensure proper event propagation between modules
- Implement event delegation pattern
- Add missing event handlers
2. ✅ Update method delegation
- ✅ Ensure proper method delegation to handlers
- ✅ Implement proper error propagation
- ✅ Add missing method implementations
3. Implement cross-module communication
- Define clear interfaces for module interaction
- Ensure proper data flow between components
- Avoid circular dependencies
3. Implement cross-module communication
- Define clear interfaces for module interaction
- Ensure proper data flow between components
- Avoid circular dependencies
4. Verify functionality preservation
- Ensure all methods have equivalent implementations
- Add logging to trace execution flow
- Create test cases for edge conditions
4. Verify functionality preservation
- Ensure all methods have equivalent implementations
- Add logging to trace execution flow
- Create test cases for edge conditions
### Phase 4: Legacy Compatibility (Days 11-12)
### Phase 4: Legacy Compatibility and Testing (Days 11-12)
1. Create facade in classes.smtpserver.ts
- Keep original class signature
- Delegate to new implementation internally
- Ensure backward compatibility
1. ✅ Create facade in classes.smtp.client.ts
- Keep original class signature
- Delegate to new implementation internally
- Ensure backward compatibility
2. Add deprecation notices
- Mark legacy file with deprecation comments
- Add migration guide in comments
- Document breaking changes if any
2. Add deprecation notices
- Mark legacy file with deprecation comments
- Add migration guide in comments
- Document breaking changes if any
3. Update documentation
- Create detailed documentation for new architecture
- Add examples of how to use the new modules
- Document extension points
3. Update documentation
- Create detailed documentation for new architecture
- Add examples of how to use the new modules
- Document extension points
4. Create comprehensive tests
- Ensure all modules have proper unit tests
- Add integration tests between modules
- Verify backward compatibility with existing code
4. Create comprehensive tests
- Ensure all modules have proper unit tests
- Add integration tests between modules
- Verify backward compatibility with existing code
## Testing Strategy
1. Unit Testing
- Create unit tests for each module in isolation
- Mock dependencies for pure unit testing
- Test edge cases and error handling
- Create unit tests for each client module in isolation
- Mock socket connections and server responses
- Test authentication mechanisms independently
- Test error handling and recovery scenarios
2. Integration Testing
- Test interactions between modules
- Verify correct event propagation
- Test full SMTP communication flow
- Test interactions between client modules
- Verify proper command flow and response handling
- Test full SMTP client communication flow
- Test TLS/STARTTLS integration
3. Regression Testing
- Ensure all existing tests pass
- Verify no functionality is lost
- Compare performance metrics
3. Production Testing
- Create comprehensive client production test suite (similar to server tests)
- Test against real SMTP servers with various configurations
- Test authentication with different providers
- Performance and reliability testing
4. Compatibility Testing
- Test with existing code that uses the legacy class
- Verify backward compatibility
- Test with existing code that uses the legacy SmtpClient class
- Verify backward compatibility with EmailSendJob integration
- Document any necessary migration steps
## Timeline Estimate
- Phase 1: 1-2 days
- Phase 2: 3-5 days
- Phase 2: 3-5 days
- Phase 3: 2-3 days
- Phase 4: 1-2 days
@ -231,28 +241,36 @@ Total: 7-12 days of development time
3. Each file is less than 300 lines of code
4. Improved code organization and documentation
5. Better separation of concerns between modules
6. Easier maintenance and extension of SMTP functionality
6. Easier maintenance and extension of SMTP client functionality
7. Enhanced testability with modular components
8. Comprehensive production test suite for SMTP client
## Risks and Mitigations
### Risk: Breaking existing functionality
**Mitigation**: Comprehensive test coverage and backward compatibility facade
### Risk: Breaking existing EmailSendJob integration
**Mitigation**: Comprehensive test coverage and backward compatibility facade with existing interface
### Risk: Performance degradation due to additional indirection
**Mitigation**: Performance benchmarking before and after refactoring
**Mitigation**: Performance benchmarking before and after refactoring, optimize hot paths
### Risk: Increased complexity due to distributed code
**Mitigation**: Clear documentation and proper module interfaces
### Risk: Increased complexity due to distributed client code
**Mitigation**: Clear documentation and proper module interfaces, follow server refactoring patterns
### Risk: Time overrun due to unforeseen dependencies
**Mitigation**: Incremental approach with working checkpoints after each phase
### Risk: Connection pooling complexity in modular architecture
**Mitigation**: Careful design of connection-manager interface, thorough testing of connection lifecycle
### Risk: Time overrun due to authentication complexity
**Mitigation**: Incremental approach with working checkpoints, start with simpler auth mechanisms
## Future Extensions
Once this refactoring is complete, we can more easily:
1. Add support for additional SMTP extensions
2. Implement pluggable authentication mechanisms
3. Add more sophisticated security features
4. Improve performance with targeted optimizations
5. Create specialized versions for different use cases
1. Add support for additional SMTP authentication mechanisms (OAUTH2, SCRAM-SHA, etc.)
2. Implement advanced connection pooling strategies
3. Add comprehensive production test suite matching server coverage
4. Improve performance with targeted optimizations (pipelining, concurrent connections)
5. Create specialized client versions for different use cases (bulk sending, transactional)
6. Add client-side security features (connection validation, certificate pinning)
7. Implement advanced retry and fallback strategies
8. Add comprehensive monitoring and metrics collection

View File

@ -0,0 +1,154 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { SmtpClient } from '../ts/mail/delivery/classes.smtp.client.js';
import type { ISmtpClientOptions } from '../ts/mail/delivery/classes.smtp.client.js';
import { Email } from '../ts/mail/core/classes.email.js';
/**
* Compatibility tests for the legacy SMTP client facade
*/
tap.test('verify backward compatibility - client creation', async () => {
// Create test configuration
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false,
connectionTimeout: 10000,
domain: 'test.example.com'
};
// Create SMTP client instance using legacy constructor
const smtpClient = new SmtpClient(options);
// Verify instance was created correctly
expect(smtpClient).toBeTruthy();
expect(smtpClient.isConnected()).toBeFalsy(); // Should start disconnected
});
tap.test('verify backward compatibility - methods exist', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
const smtpClient = new SmtpClient(options);
// Verify all expected methods exist
expect(typeof smtpClient.sendMail === 'function').toBeTruthy();
expect(typeof smtpClient.verify === 'function').toBeTruthy();
expect(typeof smtpClient.isConnected === 'function').toBeTruthy();
expect(typeof smtpClient.getPoolStatus === 'function').toBeTruthy();
expect(typeof smtpClient.updateOptions === 'function').toBeTruthy();
expect(typeof smtpClient.close === 'function').toBeTruthy();
expect(typeof smtpClient.on === 'function').toBeTruthy();
expect(typeof smtpClient.off === 'function').toBeTruthy();
expect(typeof smtpClient.emit === 'function').toBeTruthy();
});
tap.test('verify backward compatibility - options update', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
const smtpClient = new SmtpClient(options);
// Test option updates don't throw
expect(() => smtpClient.updateOptions({
host: 'new-smtp.example.com',
port: 465,
secure: true
})).not.toThrow();
expect(() => smtpClient.updateOptions({
debug: true,
connectionTimeout: 5000
})).not.toThrow();
});
tap.test('verify backward compatibility - connection failure handling', async () => {
const options: ISmtpClientOptions = {
host: 'nonexistent.invalid.domain',
port: 587,
secure: false,
connectionTimeout: 1000 // Short timeout for faster test
};
const smtpClient = new SmtpClient(options);
// verify() should return false for invalid hosts
const isValid = await smtpClient.verify();
expect(isValid).toBeFalsy();
// sendMail should fail gracefully for invalid hosts
const email = new Email({
from: 'test@example.com',
to: 'recipient@example.com',
subject: 'Test Email',
text: 'This is a test email'
});
try {
const result = await smtpClient.sendMail(email);
expect(result.success).toBeFalsy();
expect(result.error).toBeTruthy();
} catch (error) {
// Connection errors are expected for invalid domains
expect(error).toBeTruthy();
}
});
tap.test('verify backward compatibility - pool status', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false,
pool: true,
maxConnections: 5
};
const smtpClient = new SmtpClient(options);
// Get pool status
const status = smtpClient.getPoolStatus();
expect(status).toBeTruthy();
expect(typeof status.total === 'number').toBeTruthy();
expect(typeof status.active === 'number').toBeTruthy();
expect(typeof status.idle === 'number').toBeTruthy();
expect(typeof status.pending === 'number').toBeTruthy();
// Initially should have no connections
expect(status.total).toEqual(0);
expect(status.active).toEqual(0);
expect(status.idle).toEqual(0);
expect(status.pending).toEqual(0);
});
tap.test('verify backward compatibility - event handling', async () => {
const options: ISmtpClientOptions = {
host: 'smtp.example.com',
port: 587,
secure: false
};
const smtpClient = new SmtpClient(options);
// Test event listener methods don't throw
const testListener = () => {};
expect(() => smtpClient.on('test', testListener)).not.toThrow();
expect(() => smtpClient.off('test', testListener)).not.toThrow();
expect(() => smtpClient.emit('test')).not.toThrow();
});
tap.test('clean up after compatibility tests', async () => {
// No-op - just to make sure everything is cleaned up properly
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();

View File

@ -1,7 +1,7 @@
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as paths from '../ts/paths.js';
import { SMTPServer } from '../ts/mail/delivery/classes.smtpserver.js';
import { createSmtpServer } from '../ts/mail/delivery/smtpserver/index.js';
import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js';
import { Email } from '../ts/mail/core/classes.email.js';
import type { ISmtpServerOptions } from '../ts/mail/delivery/interfaces.js';
@ -29,7 +29,7 @@ tap.test('verify SMTP server initialization', async () => {
};
// Create SMTP server instance
const smtpServer = new SMTPServer(mockEmailServer, options);
const smtpServer = createSmtpServer(mockEmailServer, options);
// Verify instance was created correctly
expect(smtpServer).toBeTruthy();
@ -62,7 +62,7 @@ tap.test('verify SMTP server listen method', async () => {
};
// Create SMTP server instance
const smtpServer = new SMTPServer(mockEmailServer, options);
const smtpServer = createSmtpServer(mockEmailServer, options);
// Mock net.Server.listen and net.Server.close to avoid actual networking
const originalListen = smtpServer.server.listen;
@ -118,7 +118,7 @@ tap.test('verify SMTP server error handling', async () => {
};
// Create SMTP server instance
const smtpServer = new SMTPServer(mockEmailServer, options);
const smtpServer = createSmtpServer(mockEmailServer, options);
// Mock server.listen to simulate an error
const originalListen = smtpServer.server.listen;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,304 +0,0 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { Email } from '../core/classes.email.js';
import type { UnifiedEmailServer } from '../routing/classes.unified.email.server.js';
import { logger } from '../../logger.js';
import {
SecurityLogger,
SecurityLogLevel,
SecurityEventType,
IPReputationChecker,
ReputationThreshold
} from '../../security/index.js';
import type {
ISmtpServerOptions,
ISmtpSession,
EmailProcessingMode
} from './interfaces.js';
import { SmtpState } from './interfaces.js';
// Import refactored SMTP server components
import {
SmtpServer,
createSmtpServer,
type ISmtpServer
} from './smtpserver/index.js';
/**
* Legacy SMTP Server implementation that uses the refactored modular version
* Maintains the original API for backward compatibility
*/
export class SMTPServer {
// Public properties used by existing code
public emailServerRef: UnifiedEmailServer;
// Protected properties for test access
protected server: plugins.net.Server;
protected secureServer?: plugins.tls.Server;
// Original properties maintained for compatibility
private smtpServerOptions: ISmtpServerOptions;
private sessions: Map<plugins.net.Socket | plugins.tls.TLSSocket, ISmtpSession>;
private sessionTimeouts: Map<string, NodeJS.Timeout>;
private hostname: string;
private sessionIdCounter: number = 0;
private connectionCount: number = 0;
private maxConnections: number = 100;
private cleanupInterval?: NodeJS.Timeout;
// New refactored server implementation
private smtpServerImpl: ISmtpServer;
constructor(emailServerRefArg: UnifiedEmailServer, optionsArg: ISmtpServerOptions) {
console.log('SMTPServer instance is being created (using refactored implementation)...');
// Store original arguments and properties for backward compatibility
this.emailServerRef = emailServerRefArg;
this.smtpServerOptions = optionsArg;
this.sessions = new Map();
this.sessionTimeouts = new Map();
this.hostname = optionsArg.hostname || 'mail.lossless.one';
this.maxConnections = optionsArg.maxConnections || 100;
// Log enhanced server configuration
const socketTimeout = optionsArg.socketTimeout || 300000;
const connectionTimeout = optionsArg.connectionTimeout || 30000;
const cleanupFrequency = optionsArg.cleanupInterval || 5000;
logger.log('info', 'SMTP server configuration', {
hostname: this.hostname,
maxConnections: this.maxConnections,
socketTimeout: socketTimeout,
connectionTimeout: connectionTimeout,
cleanupInterval: cleanupFrequency,
tlsEnabled: !!(optionsArg.key && optionsArg.cert),
starttlsEnabled: !!(optionsArg.key && optionsArg.cert),
securePort: optionsArg.securePort
});
// Create the refactored SMTP server implementation
this.smtpServerImpl = createSmtpServer(emailServerRefArg, optionsArg);
// Initialize server properties to support existing test code
// These will be properly set during the listen() call
this.server = new plugins.net.Server();
if (optionsArg.key && optionsArg.cert) {
try {
// Convert certificates to Buffer format for Node.js TLS
// This helps prevent ASN.1 encoding issues when Node parses the certificates
// Use explicit 'utf8' encoding to handle PEM certificates properly
const key = Buffer.from(optionsArg.key, 'utf8');
const cert = Buffer.from(optionsArg.cert, 'utf8');
const ca = optionsArg.ca ? Buffer.from(optionsArg.ca, 'utf8') : undefined;
logger.log('warn', 'SMTP SERVER: Creating TLS server with certificates', {
keyBufferLength: key.length,
certBufferLength: cert.length,
caBufferLength: ca ? ca.length : 0,
keyPreview: key.toString('utf8').substring(0, 50),
certPreview: cert.toString('utf8').substring(0, 50)
});
// TLS configuration for secure connections with broader compatibility
const tlsOptions: plugins.tls.TlsOptions = {
key: key,
cert: cert,
ca: ca,
// Support a wider range of TLS versions for better compatibility
// Note: this is a key fix for the "wrong version number" error
minVersion: 'TLSv1', // Support older TLS versions (minimum TLS 1.0)
maxVersion: 'TLSv1.3', // Support latest TLS version (1.3)
// Let the client choose the cipher for better compatibility
honorCipherOrder: false,
// Allow self-signed certificates for test environments
rejectUnauthorized: false,
// Enable session reuse for better performance
sessionTimeout: 600,
// Use a broader set of ciphers for maximum compatibility
ciphers: 'HIGH:MEDIUM:!aNULL:!eNULL:!NULL:!ADH:!RC4',
// TLS renegotiation option (removed - not supported in newer Node.js)
// Longer handshake timeout for reliability
handshakeTimeout: 30000,
// Disable secure options to allow more flexibility
secureOptions: 0,
// For debugging
enableTrace: true
};
this.secureServer = plugins.tls.createServer(tlsOptions);
logger.log('info', 'TLS server created successfully');
} catch (error) {
logger.log('error', `Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error.stack : String(error)
});
}
}
// Set up session events to maintain legacy behavior
const sessionManager = this.smtpServerImpl.getSessionManager();
// Track sessions for backward compatibility
sessionManager.on('created', (session, socket) => {
this.sessions.set(socket, session);
this.connectionCount++;
});
sessionManager.on('completed', (session, socket) => {
this.sessions.delete(socket);
this.connectionCount--;
});
}
/**
* Start the SMTP server and listen on the specified port
* @returns A promise that resolves when the server is listening
*/
public listen(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.smtpServerImpl.listen()
.then(() => {
// Get created servers for test compatibility
// Get the actual server instances for backward compatibility
const netServer = (this.smtpServerImpl as any).server;
if (netServer) {
this.server = netServer;
}
const tlsServer = (this.smtpServerImpl as any).secureServer;
if (tlsServer) {
this.secureServer = tlsServer;
}
resolve();
})
.catch(err => {
logger.log('error', `Failed to start SMTP server: ${err.message}`, {
stack: err.stack
});
reject(err);
});
});
}
/**
* Stop the SMTP server
* @returns A promise that resolves when the server has stopped
*/
public close(): Promise<void> {
return new Promise<void>((resolve, reject) => {
this.smtpServerImpl.close()
.then(() => {
// Clean up legacy resources
this.sessions.clear();
for (const timeoutId of this.sessionTimeouts.values()) {
clearTimeout(timeoutId);
}
this.sessionTimeouts.clear();
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = undefined;
}
resolve();
})
.catch(err => {
logger.log('error', `Failed to stop SMTP server: ${err.message}`, {
stack: err.stack
});
reject(err);
});
});
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private handleNewConnection(socket: plugins.net.Socket): void {
logger.log('warn', 'Using deprecated handleNewConnection method');
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private handleNewSecureConnection(socket: plugins.tls.TLSSocket): void {
logger.log('warn', 'Using deprecated handleNewSecureConnection method');
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private cleanupIdleSessions(): void {
// This is now handled by the session manager in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private generateSessionId(): string {
return `${Date.now()}-${++this.sessionIdCounter}`;
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// This is now handled by the session manager in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string): void {
// This is now handled by the command handler in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private handleDataChunk(socket: plugins.net.Socket | plugins.tls.TLSSocket, chunk: string): void {
// This is now handled by the data handler in the refactored implementation
}
/**
* @deprecated Use the refactored implementation directly
* Maintained for backward compatibility
*/
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}\r\n`);
} catch (error) {
logger.log('error', `Error sending response: ${error instanceof Error ? error.message : 'Unknown error'}`, {
response,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort
});
}
}
/**
* Get the current active connection count
* @returns Number of active connections
*/
public getConnectionCount(): number {
return this.connectionCount;
}
/**
* Get the refactored SMTP server implementation
* This provides access to the new implementation for future use
*/
public getSmtpServerImpl(): ISmtpServer {
return this.smtpServerImpl;
}
}

View File

@ -1,5 +1,5 @@
// Email delivery components
export * from './classes.smtpserver.js';
export * from './smtpserver/index.js';
export * from './classes.emailsignjob.js';
export * from './classes.delivery.queue.js';
export * from './classes.delivery.system.js';

View File

@ -0,0 +1,232 @@
/**
* SMTP Client Authentication Handler
* Authentication mechanisms implementation
*/
import { AUTH_METHODS } from './constants.js';
import type {
ISmtpConnection,
ISmtpAuthOptions,
ISmtpClientOptions,
ISmtpResponse,
IOAuth2Options
} from './interfaces.js';
import {
encodeAuthPlain,
encodeAuthLogin,
generateOAuth2String,
isSuccessCode
} from './utils/helpers.js';
import { logAuthentication, logDebug } from './utils/logging.js';
import type { CommandHandler } from './command-handler.js';
export class AuthHandler {
private options: ISmtpClientOptions;
private commandHandler: CommandHandler;
constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) {
this.options = options;
this.commandHandler = commandHandler;
}
/**
* Authenticate using the configured method
*/
public async authenticate(connection: ISmtpConnection): Promise<void> {
if (!this.options.auth) {
logDebug('No authentication configured', this.options);
return;
}
const authOptions = this.options.auth;
const capabilities = connection.capabilities;
if (!capabilities || capabilities.authMethods.size === 0) {
throw new Error('Server does not support authentication');
}
// Determine authentication method
const method = this.selectAuthMethod(authOptions, capabilities.authMethods);
logAuthentication('start', method, this.options);
try {
switch (method) {
case AUTH_METHODS.PLAIN:
await this.authenticatePlain(connection, authOptions);
break;
case AUTH_METHODS.LOGIN:
await this.authenticateLogin(connection, authOptions);
break;
case AUTH_METHODS.OAUTH2:
await this.authenticateOAuth2(connection, authOptions);
break;
default:
throw new Error(`Unsupported authentication method: ${method}`);
}
logAuthentication('success', method, this.options);
} catch (error) {
logAuthentication('failure', method, this.options, { error });
throw error;
}
}
/**
* Authenticate using AUTH PLAIN
*/
private async authenticatePlain(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> {
if (!auth.user || !auth.pass) {
throw new Error('Username and password required for PLAIN authentication');
}
const credentials = encodeAuthPlain(auth.user, auth.pass);
const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.PLAIN, credentials);
if (!isSuccessCode(response.code)) {
throw new Error(`PLAIN authentication failed: ${response.message}`);
}
}
/**
* Authenticate using AUTH LOGIN
*/
private async authenticateLogin(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> {
if (!auth.user || !auth.pass) {
throw new Error('Username and password required for LOGIN authentication');
}
// Step 1: Send AUTH LOGIN
let response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.LOGIN);
if (response.code !== 334) {
throw new Error(`LOGIN authentication initiation failed: ${response.message}`);
}
// Step 2: Send username
const encodedUser = encodeAuthLogin(auth.user);
response = await this.commandHandler.sendCommand(connection, encodedUser);
if (response.code !== 334) {
throw new Error(`LOGIN username failed: ${response.message}`);
}
// Step 3: Send password
const encodedPass = encodeAuthLogin(auth.pass);
response = await this.commandHandler.sendCommand(connection, encodedPass);
if (!isSuccessCode(response.code)) {
throw new Error(`LOGIN password failed: ${response.message}`);
}
}
/**
* Authenticate using OAuth2
*/
private async authenticateOAuth2(connection: ISmtpConnection, auth: ISmtpAuthOptions): Promise<void> {
if (!auth.oauth2) {
throw new Error('OAuth2 configuration required for OAUTH2 authentication');
}
let accessToken = auth.oauth2.accessToken;
// Refresh token if needed
if (!accessToken || this.isTokenExpired(auth.oauth2)) {
accessToken = await this.refreshOAuth2Token(auth.oauth2);
}
const authString = generateOAuth2String(auth.oauth2.user, accessToken);
const response = await this.commandHandler.sendAuth(connection, AUTH_METHODS.OAUTH2, authString);
if (!isSuccessCode(response.code)) {
throw new Error(`OAUTH2 authentication failed: ${response.message}`);
}
}
/**
* Select appropriate authentication method
*/
private selectAuthMethod(auth: ISmtpAuthOptions, serverMethods: Set<string>): string {
// If method is explicitly specified, use it
if (auth.method && auth.method !== 'AUTO') {
const method = auth.method === 'OAUTH2' ? AUTH_METHODS.OAUTH2 : auth.method;
if (serverMethods.has(method)) {
return method;
}
throw new Error(`Requested authentication method ${auth.method} not supported by server`);
}
// Auto-select based on available credentials and server support
if (auth.oauth2 && serverMethods.has(AUTH_METHODS.OAUTH2)) {
return AUTH_METHODS.OAUTH2;
}
if (auth.user && auth.pass) {
// Prefer PLAIN over LOGIN for simplicity
if (serverMethods.has(AUTH_METHODS.PLAIN)) {
return AUTH_METHODS.PLAIN;
}
if (serverMethods.has(AUTH_METHODS.LOGIN)) {
return AUTH_METHODS.LOGIN;
}
}
throw new Error('No compatible authentication method found');
}
/**
* Check if OAuth2 token is expired
*/
private isTokenExpired(oauth2: IOAuth2Options): boolean {
if (!oauth2.expires) {
return false; // No expiry information, assume valid
}
const now = Date.now();
const buffer = 300000; // 5 minutes buffer
return oauth2.expires < (now + buffer);
}
/**
* Refresh OAuth2 access token
*/
private async refreshOAuth2Token(oauth2: IOAuth2Options): Promise<string> {
// This is a simplified implementation
// In a real implementation, you would make an HTTP request to the OAuth2 provider
logDebug('OAuth2 token refresh required', this.options);
if (!oauth2.refreshToken) {
throw new Error('Refresh token required for OAuth2 token refresh');
}
// TODO: Implement actual OAuth2 token refresh
// For now, throw an error to indicate this needs to be implemented
throw new Error('OAuth2 token refresh not implemented. Please provide a valid access token.');
}
/**
* Validate authentication configuration
*/
public validateAuthConfig(auth: ISmtpAuthOptions): string[] {
const errors: string[] = [];
if (auth.method === 'OAUTH2' || auth.oauth2) {
if (!auth.oauth2) {
errors.push('OAuth2 configuration required when using OAUTH2 method');
} else {
if (!auth.oauth2.user) errors.push('OAuth2 user required');
if (!auth.oauth2.clientId) errors.push('OAuth2 clientId required');
if (!auth.oauth2.clientSecret) errors.push('OAuth2 clientSecret required');
if (!auth.oauth2.refreshToken && !auth.oauth2.accessToken) {
errors.push('OAuth2 refreshToken or accessToken required');
}
}
} else if (auth.method === 'PLAIN' || auth.method === 'LOGIN' || (!auth.method && (auth.user || auth.pass))) {
if (!auth.user) errors.push('Username required for basic authentication');
if (!auth.pass) errors.push('Password required for basic authentication');
}
return errors;
}
}

View File

@ -0,0 +1,336 @@
/**
* SMTP Client Command Handler
* SMTP command sending and response parsing
*/
import { EventEmitter } from 'node:events';
import { SMTP_COMMANDS, SMTP_CODES, LINE_ENDINGS } from './constants.js';
import type {
ISmtpConnection,
ISmtpResponse,
ISmtpClientOptions,
ISmtpCapabilities
} from './interfaces.js';
import {
parseSmtpResponse,
parseEhloResponse,
formatCommand,
isSuccessCode
} from './utils/helpers.js';
import { logCommand, logDebug } from './utils/logging.js';
export class CommandHandler extends EventEmitter {
private options: ISmtpClientOptions;
private responseBuffer: string = '';
private pendingCommand: { resolve: Function; reject: Function; command: string } | null = null;
private commandTimeout: NodeJS.Timeout | null = null;
constructor(options: ISmtpClientOptions) {
super();
this.options = options;
}
/**
* Send EHLO command and parse capabilities
*/
public async sendEhlo(connection: ISmtpConnection, domain?: string): Promise<ISmtpCapabilities> {
const hostname = domain || this.options.domain || 'localhost';
const command = `${SMTP_COMMANDS.EHLO} ${hostname}`;
const response = await this.sendCommand(connection, command);
if (!isSuccessCode(response.code)) {
throw new Error(`EHLO failed: ${response.message}`);
}
const capabilities = parseEhloResponse(response.raw);
connection.capabilities = capabilities;
logDebug('EHLO capabilities parsed', this.options, { capabilities });
return capabilities;
}
/**
* Send MAIL FROM command
*/
public async sendMailFrom(connection: ISmtpConnection, fromAddress: string): Promise<ISmtpResponse> {
const command = `${SMTP_COMMANDS.MAIL_FROM}:<${fromAddress}>`;
return this.sendCommand(connection, command);
}
/**
* Send RCPT TO command
*/
public async sendRcptTo(connection: ISmtpConnection, toAddress: string): Promise<ISmtpResponse> {
const command = `${SMTP_COMMANDS.RCPT_TO}:<${toAddress}>`;
return this.sendCommand(connection, command);
}
/**
* Send DATA command
*/
public async sendData(connection: ISmtpConnection): Promise<ISmtpResponse> {
return this.sendCommand(connection, SMTP_COMMANDS.DATA);
}
/**
* Send email data content
*/
public async sendDataContent(connection: ISmtpConnection, emailData: string): Promise<ISmtpResponse> {
// Ensure email data ends with CRLF.CRLF
let data = emailData;
if (!data.endsWith(LINE_ENDINGS.CRLF)) {
data += LINE_ENDINGS.CRLF;
}
data += '.' + LINE_ENDINGS.CRLF;
// Perform dot stuffing (escape lines starting with a dot)
data = data.replace(/\n\./g, '\n..');
return this.sendRawData(connection, data);
}
/**
* Send RSET command
*/
public async sendRset(connection: ISmtpConnection): Promise<ISmtpResponse> {
return this.sendCommand(connection, SMTP_COMMANDS.RSET);
}
/**
* Send NOOP command
*/
public async sendNoop(connection: ISmtpConnection): Promise<ISmtpResponse> {
return this.sendCommand(connection, SMTP_COMMANDS.NOOP);
}
/**
* Send QUIT command
*/
public async sendQuit(connection: ISmtpConnection): Promise<ISmtpResponse> {
return this.sendCommand(connection, SMTP_COMMANDS.QUIT);
}
/**
* Send STARTTLS command
*/
public async sendStartTls(connection: ISmtpConnection): Promise<ISmtpResponse> {
return this.sendCommand(connection, SMTP_COMMANDS.STARTTLS);
}
/**
* Send AUTH command
*/
public async sendAuth(connection: ISmtpConnection, method: string, credentials?: string): Promise<ISmtpResponse> {
const command = credentials ?
`${SMTP_COMMANDS.AUTH} ${method} ${credentials}` :
`${SMTP_COMMANDS.AUTH} ${method}`;
return this.sendCommand(connection, command);
}
/**
* Send a generic SMTP command
*/
public async sendCommand(connection: ISmtpConnection, command: string): Promise<ISmtpResponse> {
return new Promise((resolve, reject) => {
if (this.pendingCommand) {
reject(new Error('Another command is already pending'));
return;
}
this.pendingCommand = { resolve, reject, command };
// Set command timeout
const timeout = 30000; // 30 seconds
this.commandTimeout = setTimeout(() => {
this.pendingCommand = null;
this.commandTimeout = null;
reject(new Error(`Command timeout: ${command}`));
}, timeout);
// Set up data handler
const dataHandler = (data: Buffer) => {
this.handleIncomingData(data.toString());
};
connection.socket.on('data', dataHandler);
// Clean up function
const cleanup = () => {
connection.socket.removeListener('data', dataHandler);
if (this.commandTimeout) {
clearTimeout(this.commandTimeout);
this.commandTimeout = null;
}
};
// Send command
const formattedCommand = command.endsWith(LINE_ENDINGS.CRLF) ? command : formatCommand(command);
logCommand(command, undefined, this.options);
logDebug(`Sending command: ${command}`, this.options);
connection.socket.write(formattedCommand, (error) => {
if (error) {
cleanup();
this.pendingCommand = null;
reject(error);
}
});
// Override resolve/reject to include cleanup
const originalResolve = resolve;
const originalReject = reject;
this.pendingCommand.resolve = (response: ISmtpResponse) => {
cleanup();
this.pendingCommand = null;
logCommand(command, response, this.options);
originalResolve(response);
};
this.pendingCommand.reject = (error: Error) => {
cleanup();
this.pendingCommand = null;
originalReject(error);
};
});
}
/**
* Send raw data without command formatting
*/
public async sendRawData(connection: ISmtpConnection, data: string): Promise<ISmtpResponse> {
return new Promise((resolve, reject) => {
if (this.pendingCommand) {
reject(new Error('Another command is already pending'));
return;
}
this.pendingCommand = { resolve, reject, command: 'DATA_CONTENT' };
// Set data timeout
const timeout = 60000; // 60 seconds for data
this.commandTimeout = setTimeout(() => {
this.pendingCommand = null;
this.commandTimeout = null;
reject(new Error('Data transmission timeout'));
}, timeout);
// Set up data handler
const dataHandler = (chunk: Buffer) => {
this.handleIncomingData(chunk.toString());
};
connection.socket.on('data', dataHandler);
// Clean up function
const cleanup = () => {
connection.socket.removeListener('data', dataHandler);
if (this.commandTimeout) {
clearTimeout(this.commandTimeout);
this.commandTimeout = null;
}
};
// Override resolve/reject to include cleanup
const originalResolve = resolve;
const originalReject = reject;
this.pendingCommand.resolve = (response: ISmtpResponse) => {
cleanup();
this.pendingCommand = null;
originalResolve(response);
};
this.pendingCommand.reject = (error: Error) => {
cleanup();
this.pendingCommand = null;
originalReject(error);
};
// Send data
connection.socket.write(data, (error) => {
if (error) {
cleanup();
this.pendingCommand = null;
reject(error);
}
});
});
}
/**
* Wait for server greeting
*/
public async waitForGreeting(connection: ISmtpConnection): Promise<ISmtpResponse> {
return new Promise((resolve, reject) => {
const timeout = 30000; // 30 seconds
let timeoutHandler: NodeJS.Timeout;
const dataHandler = (data: Buffer) => {
this.responseBuffer += data.toString();
if (this.isCompleteResponse(this.responseBuffer)) {
clearTimeout(timeoutHandler);
connection.socket.removeListener('data', dataHandler);
const response = parseSmtpResponse(this.responseBuffer);
this.responseBuffer = '';
if (isSuccessCode(response.code)) {
resolve(response);
} else {
reject(new Error(`Server greeting failed: ${response.message}`));
}
}
};
timeoutHandler = setTimeout(() => {
connection.socket.removeListener('data', dataHandler);
reject(new Error('Greeting timeout'));
}, timeout);
connection.socket.on('data', dataHandler);
});
}
private handleIncomingData(data: string): void {
if (!this.pendingCommand) {
return;
}
this.responseBuffer += data;
if (this.isCompleteResponse(this.responseBuffer)) {
const response = parseSmtpResponse(this.responseBuffer);
this.responseBuffer = '';
if (isSuccessCode(response.code) || response.code >= 400) {
this.pendingCommand.resolve(response);
} else {
this.pendingCommand.reject(new Error(`Command failed: ${response.message}`));
}
}
}
private isCompleteResponse(buffer: string): boolean {
// Check if we have a complete response
const lines = buffer.split(/\r?\n/);
if (lines.length < 1) {
return false;
}
// Check the last non-empty line
for (let i = lines.length - 1; i >= 0; i--) {
const line = lines[i].trim();
if (line.length > 0) {
// Response is complete if line starts with "XXX " (space after code)
return /^\d{3} /.test(line);
}
}
return false;
}
}

View File

@ -0,0 +1,286 @@
/**
* SMTP Client Connection Manager
* Connection pooling and lifecycle management
*/
import * as net from 'node:net';
import * as tls from 'node:tls';
import { EventEmitter } from 'node:events';
import { DEFAULTS, CONNECTION_STATES } from './constants.js';
import type {
ISmtpClientOptions,
ISmtpConnection,
IConnectionPoolStatus,
ConnectionState
} from './interfaces.js';
import { logConnection, logDebug } from './utils/logging.js';
import { generateConnectionId } from './utils/helpers.js';
export class ConnectionManager extends EventEmitter {
private options: ISmtpClientOptions;
private connections: Map<string, ISmtpConnection> = new Map();
private pendingConnections: Set<string> = new Set();
private idleTimeout: NodeJS.Timeout | null = null;
constructor(options: ISmtpClientOptions) {
super();
this.options = options;
this.setupIdleCleanup();
}
/**
* Get or create a connection
*/
public async getConnection(): Promise<ISmtpConnection> {
// Try to reuse an idle connection if pooling is enabled
if (this.options.pool) {
const idleConnection = this.findIdleConnection();
if (idleConnection) {
const connectionId = this.getConnectionId(idleConnection) || 'unknown';
logDebug('Reusing idle connection', this.options, { connectionId });
return idleConnection;
}
// Check if we can create a new connection
if (this.getActiveConnectionCount() >= (this.options.maxConnections || DEFAULTS.MAX_CONNECTIONS)) {
throw new Error('Maximum number of connections reached');
}
}
return this.createConnection();
}
/**
* Create a new connection
*/
public async createConnection(): Promise<ISmtpConnection> {
const connectionId = generateConnectionId();
try {
this.pendingConnections.add(connectionId);
logConnection('connecting', this.options, { connectionId });
const socket = await this.establishSocket();
const connection: ISmtpConnection = {
socket,
state: CONNECTION_STATES.CONNECTED as ConnectionState,
options: this.options,
secure: this.options.secure || false,
createdAt: new Date(),
lastActivity: new Date(),
messageCount: 0
};
this.setupSocketHandlers(socket, connectionId);
this.connections.set(connectionId, connection);
this.pendingConnections.delete(connectionId);
logConnection('connected', this.options, { connectionId });
this.emit('connection', connection);
return connection;
} catch (error) {
this.pendingConnections.delete(connectionId);
logConnection('error', this.options, { connectionId, error });
throw error;
}
}
/**
* Release a connection back to the pool or close it
*/
public releaseConnection(connection: ISmtpConnection): void {
const connectionId = this.getConnectionId(connection);
if (!connectionId || !this.connections.has(connectionId)) {
return;
}
if (this.options.pool && this.shouldReuseConnection(connection)) {
// Return to pool
connection.state = CONNECTION_STATES.READY as ConnectionState;
connection.lastActivity = new Date();
logDebug('Connection returned to pool', this.options, { connectionId });
} else {
// Close connection
this.closeConnection(connection);
}
}
/**
* Close a specific connection
*/
public closeConnection(connection: ISmtpConnection): void {
const connectionId = this.getConnectionId(connection);
if (connectionId) {
this.connections.delete(connectionId);
}
connection.state = CONNECTION_STATES.CLOSING as ConnectionState;
try {
if (!connection.socket.destroyed) {
connection.socket.destroy();
}
} catch (error) {
logDebug('Error closing connection', this.options, { error });
}
logConnection('disconnected', this.options, { connectionId });
this.emit('disconnect', connection);
}
/**
* Close all connections
*/
public closeAllConnections(): void {
logDebug('Closing all connections', this.options);
for (const connection of this.connections.values()) {
this.closeConnection(connection);
}
this.connections.clear();
this.pendingConnections.clear();
if (this.idleTimeout) {
clearInterval(this.idleTimeout);
this.idleTimeout = null;
}
}
/**
* Get connection pool status
*/
public getPoolStatus(): IConnectionPoolStatus {
const total = this.connections.size;
const active = Array.from(this.connections.values())
.filter(conn => conn.state === CONNECTION_STATES.BUSY).length;
const idle = total - active;
const pending = this.pendingConnections.size;
return { total, active, idle, pending };
}
/**
* Update connection activity timestamp
*/
public updateActivity(connection: ISmtpConnection): void {
connection.lastActivity = new Date();
}
private async establishSocket(): Promise<net.Socket | tls.TLSSocket> {
return new Promise((resolve, reject) => {
const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
let socket: net.Socket | tls.TLSSocket;
if (this.options.secure) {
// Direct TLS connection
socket = tls.connect({
host: this.options.host,
port: this.options.port,
...this.options.tls
});
} else {
// Plain connection
socket = new net.Socket();
socket.connect(this.options.port, this.options.host);
}
const timeoutHandler = setTimeout(() => {
socket.destroy();
reject(new Error(`Connection timeout after ${timeout}ms`));
}, timeout);
socket.once('connect', () => {
clearTimeout(timeoutHandler);
resolve(socket);
});
socket.once('error', (error) => {
clearTimeout(timeoutHandler);
reject(error);
});
});
}
private setupSocketHandlers(socket: net.Socket | tls.TLSSocket, connectionId: string): void {
const socketTimeout = this.options.socketTimeout || DEFAULTS.SOCKET_TIMEOUT;
socket.setTimeout(socketTimeout);
socket.on('timeout', () => {
logDebug('Socket timeout', this.options, { connectionId });
socket.destroy();
});
socket.on('error', (error) => {
logConnection('error', this.options, { connectionId, error });
this.connections.delete(connectionId);
});
socket.on('close', () => {
this.connections.delete(connectionId);
logDebug('Socket closed', this.options, { connectionId });
});
}
private findIdleConnection(): ISmtpConnection | null {
for (const connection of this.connections.values()) {
if (connection.state === CONNECTION_STATES.READY) {
return connection;
}
}
return null;
}
private shouldReuseConnection(connection: ISmtpConnection): boolean {
const maxMessages = this.options.maxMessages || DEFAULTS.MAX_MESSAGES;
const maxAge = 300000; // 5 minutes
const age = Date.now() - connection.createdAt.getTime();
return connection.messageCount < maxMessages &&
age < maxAge &&
!connection.socket.destroyed;
}
private getActiveConnectionCount(): number {
return this.connections.size + this.pendingConnections.size;
}
private getConnectionId(connection: ISmtpConnection): string | null {
for (const [id, conn] of this.connections.entries()) {
if (conn === connection) {
return id;
}
}
return null;
}
private setupIdleCleanup(): void {
if (!this.options.pool) {
return;
}
const cleanupInterval = DEFAULTS.POOL_IDLE_TIMEOUT;
this.idleTimeout = setInterval(() => {
const now = Date.now();
const connectionsToClose: ISmtpConnection[] = [];
for (const connection of this.connections.values()) {
const idleTime = now - connection.lastActivity.getTime();
if (connection.state === CONNECTION_STATES.READY && idleTime > cleanupInterval) {
connectionsToClose.push(connection);
}
}
for (const connection of connectionsToClose) {
logDebug('Closing idle connection', this.options);
this.closeConnection(connection);
}
}, cleanupInterval);
}
}

View File

@ -0,0 +1,145 @@
/**
* SMTP Client Constants and Error Codes
* All constants, error codes, and enums for SMTP client operations
*/
/**
* SMTP response codes
*/
export const SMTP_CODES = {
// Positive completion replies
SERVICE_READY: 220,
SERVICE_CLOSING: 221,
AUTHENTICATION_SUCCESSFUL: 235,
REQUESTED_ACTION_OK: 250,
USER_NOT_LOCAL: 251,
CANNOT_VERIFY_USER: 252,
// Positive intermediate replies
START_MAIL_INPUT: 354,
// Transient negative completion replies
SERVICE_NOT_AVAILABLE: 421,
MAILBOX_BUSY: 450,
LOCAL_ERROR: 451,
INSUFFICIENT_STORAGE: 452,
UNABLE_TO_ACCOMMODATE: 455,
// Permanent negative completion replies
SYNTAX_ERROR: 500,
SYNTAX_ERROR_PARAMETERS: 501,
COMMAND_NOT_IMPLEMENTED: 502,
BAD_SEQUENCE: 503,
PARAMETER_NOT_IMPLEMENTED: 504,
MAILBOX_UNAVAILABLE: 550,
USER_NOT_LOCAL_TRY_FORWARD: 551,
EXCEEDED_STORAGE: 552,
MAILBOX_NAME_NOT_ALLOWED: 553,
TRANSACTION_FAILED: 554
} as const;
/**
* SMTP command names
*/
export const SMTP_COMMANDS = {
HELO: 'HELO',
EHLO: 'EHLO',
MAIL_FROM: 'MAIL FROM',
RCPT_TO: 'RCPT TO',
DATA: 'DATA',
RSET: 'RSET',
NOOP: 'NOOP',
QUIT: 'QUIT',
STARTTLS: 'STARTTLS',
AUTH: 'AUTH'
} as const;
/**
* Authentication methods
*/
export const AUTH_METHODS = {
PLAIN: 'PLAIN',
LOGIN: 'LOGIN',
OAUTH2: 'XOAUTH2',
CRAM_MD5: 'CRAM-MD5'
} as const;
/**
* Common SMTP extensions
*/
export const SMTP_EXTENSIONS = {
PIPELINING: 'PIPELINING',
SIZE: 'SIZE',
STARTTLS: 'STARTTLS',
AUTH: 'AUTH',
EIGHT_BIT_MIME: '8BITMIME',
CHUNKING: 'CHUNKING',
ENHANCED_STATUS_CODES: 'ENHANCEDSTATUSCODES',
DSN: 'DSN'
} as const;
/**
* Default configuration values
*/
export const DEFAULTS = {
CONNECTION_TIMEOUT: 60000, // 60 seconds
SOCKET_TIMEOUT: 300000, // 5 minutes
COMMAND_TIMEOUT: 30000, // 30 seconds
MAX_CONNECTIONS: 5,
MAX_MESSAGES: 100,
PORT_SMTP: 25,
PORT_SUBMISSION: 587,
PORT_SMTPS: 465,
RETRY_ATTEMPTS: 3,
RETRY_DELAY: 1000,
POOL_IDLE_TIMEOUT: 30000 // 30 seconds
} as const;
/**
* Error types for classification
*/
export enum SmtpErrorType {
CONNECTION_ERROR = 'CONNECTION_ERROR',
AUTHENTICATION_ERROR = 'AUTHENTICATION_ERROR',
PROTOCOL_ERROR = 'PROTOCOL_ERROR',
TIMEOUT_ERROR = 'TIMEOUT_ERROR',
TLS_ERROR = 'TLS_ERROR',
SYNTAX_ERROR = 'SYNTAX_ERROR',
MAILBOX_ERROR = 'MAILBOX_ERROR',
QUOTA_ERROR = 'QUOTA_ERROR',
UNKNOWN_ERROR = 'UNKNOWN_ERROR'
}
/**
* Regular expressions for parsing
*/
export const REGEX_PATTERNS = {
EMAIL_ADDRESS: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
RESPONSE_CODE: /^(\d{3})([ -])(.*)/,
ENHANCED_STATUS: /^(\d\.\d\.\d)\s/,
AUTH_CAPABILITIES: /AUTH\s+(.+)/i,
SIZE_EXTENSION: /SIZE\s+(\d+)/i
} as const;
/**
* Line endings and separators
*/
export const LINE_ENDINGS = {
CRLF: '\r\n',
LF: '\n',
CR: '\r'
} as const;
/**
* Connection states for internal use
*/
export const CONNECTION_STATES = {
DISCONNECTED: 'disconnected',
CONNECTING: 'connecting',
CONNECTED: 'connected',
AUTHENTICATED: 'authenticated',
READY: 'ready',
BUSY: 'busy',
CLOSING: 'closing',
ERROR: 'error'
} as const;

View File

@ -0,0 +1,94 @@
/**
* SMTP Client Factory
* Factory function for client creation and dependency injection
*/
import { SmtpClient } from './smtp-client.js';
import { ConnectionManager } from './connection-manager.js';
import { CommandHandler } from './command-handler.js';
import { AuthHandler } from './auth-handler.js';
import { TlsHandler } from './tls-handler.js';
import { SmtpErrorHandler } from './error-handler.js';
import type { ISmtpClientOptions } from './interfaces.js';
import { validateClientOptions } from './utils/validation.js';
import { DEFAULTS } from './constants.js';
/**
* Create a complete SMTP client with all components
*/
export function createSmtpClient(options: ISmtpClientOptions): SmtpClient {
// Validate options
const errors = validateClientOptions(options);
if (errors.length > 0) {
throw new Error(`Invalid client options: ${errors.join(', ')}`);
}
// Apply defaults
const clientOptions: ISmtpClientOptions = {
connectionTimeout: DEFAULTS.CONNECTION_TIMEOUT,
socketTimeout: DEFAULTS.SOCKET_TIMEOUT,
maxConnections: DEFAULTS.MAX_CONNECTIONS,
maxMessages: DEFAULTS.MAX_MESSAGES,
pool: false,
secure: false,
debug: false,
...options
};
// Create handlers
const errorHandler = new SmtpErrorHandler(clientOptions);
const connectionManager = new ConnectionManager(clientOptions);
const commandHandler = new CommandHandler(clientOptions);
const authHandler = new AuthHandler(clientOptions, commandHandler);
const tlsHandler = new TlsHandler(clientOptions, commandHandler);
// Create and return SMTP client
return new SmtpClient({
options: clientOptions,
connectionManager,
commandHandler,
authHandler,
tlsHandler,
errorHandler
});
}
/**
* Create SMTP client with connection pooling enabled
*/
export function createPooledSmtpClient(options: ISmtpClientOptions): SmtpClient {
return createSmtpClient({
...options,
pool: true,
maxConnections: options.maxConnections || DEFAULTS.MAX_CONNECTIONS,
maxMessages: options.maxMessages || DEFAULTS.MAX_MESSAGES
});
}
/**
* Create SMTP client for high-volume sending
*/
export function createBulkSmtpClient(options: ISmtpClientOptions): SmtpClient {
return createSmtpClient({
...options,
pool: true,
maxConnections: Math.max(options.maxConnections || 10, 10),
maxMessages: Math.max(options.maxMessages || 1000, 1000),
connectionTimeout: options.connectionTimeout || 30000,
socketTimeout: options.socketTimeout || 120000
});
}
/**
* Create SMTP client for transactional emails
*/
export function createTransactionalSmtpClient(options: ISmtpClientOptions): SmtpClient {
return createSmtpClient({
...options,
pool: false, // Use fresh connections for transactional emails
maxConnections: 1,
maxMessages: 1,
connectionTimeout: options.connectionTimeout || 10000,
socketTimeout: options.socketTimeout || 30000
});
}

View File

@ -0,0 +1,141 @@
/**
* SMTP Client Error Handler
* Error classification and recovery strategies
*/
import { SmtpErrorType } from './constants.js';
import type { ISmtpResponse, ISmtpErrorContext, ISmtpClientOptions } from './interfaces.js';
import { logDebug } from './utils/logging.js';
export class SmtpErrorHandler {
private options: ISmtpClientOptions;
constructor(options: ISmtpClientOptions) {
this.options = options;
}
/**
* Classify error type based on response or error
*/
public classifyError(error: Error | ISmtpResponse, context?: ISmtpErrorContext): SmtpErrorType {
logDebug('Classifying error', this.options, { errorMessage: error instanceof Error ? error.message : String(error), context });
// Handle Error objects
if (error instanceof Error) {
return this.classifyErrorByMessage(error);
}
// Handle SMTP response codes
if (typeof error === 'object' && 'code' in error) {
return this.classifyErrorByCode(error.code);
}
return SmtpErrorType.UNKNOWN_ERROR;
}
/**
* Determine if error is retryable
*/
public isRetryable(errorType: SmtpErrorType, response?: ISmtpResponse): boolean {
switch (errorType) {
case SmtpErrorType.CONNECTION_ERROR:
case SmtpErrorType.TIMEOUT_ERROR:
return true;
case SmtpErrorType.PROTOCOL_ERROR:
// Only retry on temporary failures (4xx codes)
return response ? response.code >= 400 && response.code < 500 : false;
case SmtpErrorType.AUTHENTICATION_ERROR:
case SmtpErrorType.TLS_ERROR:
case SmtpErrorType.SYNTAX_ERROR:
case SmtpErrorType.MAILBOX_ERROR:
case SmtpErrorType.QUOTA_ERROR:
return false;
default:
return false;
}
}
/**
* Get retry delay for error type
*/
public getRetryDelay(attempt: number, errorType: SmtpErrorType): number {
const baseDelay = 1000; // 1 second
const maxDelay = 30000; // 30 seconds
// Exponential backoff with jitter
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
const jitter = Math.random() * 0.1 * delay; // 10% jitter
return Math.floor(delay + jitter);
}
/**
* Create enhanced error with context
*/
public createError(
message: string,
errorType: SmtpErrorType,
context?: ISmtpErrorContext,
originalError?: Error
): Error {
const error = new Error(message);
(error as any).type = errorType;
(error as any).context = context;
(error as any).originalError = originalError;
return error;
}
private classifyErrorByMessage(error: Error): SmtpErrorType {
const message = error.message.toLowerCase();
if (message.includes('timeout') || message.includes('etimedout')) {
return SmtpErrorType.TIMEOUT_ERROR;
}
if (message.includes('connect') || message.includes('econnrefused') ||
message.includes('enotfound') || message.includes('enetunreach')) {
return SmtpErrorType.CONNECTION_ERROR;
}
if (message.includes('tls') || message.includes('ssl') ||
message.includes('certificate') || message.includes('handshake')) {
return SmtpErrorType.TLS_ERROR;
}
if (message.includes('auth')) {
return SmtpErrorType.AUTHENTICATION_ERROR;
}
return SmtpErrorType.UNKNOWN_ERROR;
}
private classifyErrorByCode(code: number): SmtpErrorType {
if (code >= 500) {
// Permanent failures
if (code === 550 || code === 551 || code === 553) {
return SmtpErrorType.MAILBOX_ERROR;
}
if (code === 552) {
return SmtpErrorType.QUOTA_ERROR;
}
if (code === 500 || code === 501 || code === 502 || code === 504) {
return SmtpErrorType.SYNTAX_ERROR;
}
return SmtpErrorType.PROTOCOL_ERROR;
}
if (code >= 400) {
// Temporary failures
if (code === 450 || code === 451 || code === 452) {
return SmtpErrorType.QUOTA_ERROR;
}
return SmtpErrorType.PROTOCOL_ERROR;
}
return SmtpErrorType.UNKNOWN_ERROR;
}
}

View File

@ -0,0 +1,24 @@
/**
* SMTP Client Module Exports
* Modular SMTP client implementation for robust email delivery
*/
// Main client class and factory
export * from './smtp-client.js';
export * from './create-client.js';
// Core handlers
export * from './connection-manager.js';
export * from './command-handler.js';
export * from './auth-handler.js';
export * from './tls-handler.js';
export * from './error-handler.js';
// Interfaces and types
export * from './interfaces.js';
export * from './constants.js';
// Utilities
export * from './utils/validation.js';
export * from './utils/logging.js';
export * from './utils/helpers.js';

View File

@ -0,0 +1,242 @@
/**
* SMTP Client Interfaces and Types
* All interface definitions for the modular SMTP client
*/
import type * as tls from 'node:tls';
import type * as net from 'node:net';
import type { Email } from '../../core/classes.email.js';
/**
* SMTP client connection options
*/
export interface ISmtpClientOptions {
/** Hostname of the SMTP server */
host: string;
/** Port to connect to */
port: number;
/** Whether to use TLS for the connection */
secure?: boolean;
/** Connection timeout in milliseconds */
connectionTimeout?: number;
/** Socket timeout in milliseconds */
socketTimeout?: number;
/** Domain name for EHLO command */
domain?: string;
/** Authentication options */
auth?: ISmtpAuthOptions;
/** TLS options */
tls?: tls.ConnectionOptions;
/** Maximum number of connections in pool */
pool?: boolean;
maxConnections?: number;
maxMessages?: number;
/** Enable debug logging */
debug?: boolean;
/** Proxy settings */
proxy?: string;
}
/**
* Authentication options for SMTP
*/
export interface ISmtpAuthOptions {
/** Username */
user?: string;
/** Password */
pass?: string;
/** OAuth2 settings */
oauth2?: IOAuth2Options;
/** Authentication method preference */
method?: 'PLAIN' | 'LOGIN' | 'OAUTH2' | 'AUTO';
}
/**
* OAuth2 authentication options
*/
export interface IOAuth2Options {
/** OAuth2 user identifier */
user: string;
/** OAuth2 client ID */
clientId: string;
/** OAuth2 client secret */
clientSecret: string;
/** OAuth2 refresh token */
refreshToken: string;
/** OAuth2 access token */
accessToken?: string;
/** Token expiry time */
expires?: number;
}
/**
* Result of an email send operation
*/
export interface ISmtpSendResult {
/** Whether the send was successful */
success: boolean;
/** Message ID from server */
messageId?: string;
/** List of accepted recipients */
acceptedRecipients: string[];
/** List of rejected recipients */
rejectedRecipients: string[];
/** Error information if failed */
error?: Error;
/** Server response */
response?: string;
/** Envelope information */
envelope?: ISmtpEnvelope;
}
/**
* SMTP envelope information
*/
export interface ISmtpEnvelope {
/** Sender address */
from: string;
/** Recipient addresses */
to: string[];
}
/**
* Connection pool status
*/
export interface IConnectionPoolStatus {
/** Total connections in pool */
total: number;
/** Active connections */
active: number;
/** Idle connections */
idle: number;
/** Pending connection requests */
pending: number;
}
/**
* SMTP command response
*/
export interface ISmtpResponse {
/** Response code */
code: number;
/** Response message */
message: string;
/** Enhanced status code */
enhancedCode?: string;
/** Raw response */
raw: string;
}
/**
* Connection state
*/
export enum ConnectionState {
DISCONNECTED = 'disconnected',
CONNECTING = 'connecting',
CONNECTED = 'connected',
AUTHENTICATED = 'authenticated',
READY = 'ready',
BUSY = 'busy',
CLOSING = 'closing',
ERROR = 'error'
}
/**
* SMTP capabilities
*/
export interface ISmtpCapabilities {
/** Supported extensions */
extensions: Set<string>;
/** Maximum message size */
maxSize?: number;
/** Supported authentication methods */
authMethods: Set<string>;
/** Support for pipelining */
pipelining: boolean;
/** Support for STARTTLS */
starttls: boolean;
/** Support for 8BITMIME */
eightBitMime: boolean;
}
/**
* Internal connection interface
*/
export interface ISmtpConnection {
/** Socket connection */
socket: net.Socket | tls.TLSSocket;
/** Connection state */
state: ConnectionState;
/** Server capabilities */
capabilities?: ISmtpCapabilities;
/** Connection options */
options: ISmtpClientOptions;
/** Whether connection is secure */
secure: boolean;
/** Connection creation time */
createdAt: Date;
/** Last activity time */
lastActivity: Date;
/** Number of messages sent */
messageCount: number;
}
/**
* Error context for detailed error reporting
*/
export interface ISmtpErrorContext {
/** Command that caused the error */
command?: string;
/** Server response */
response?: ISmtpResponse;
/** Connection state */
connectionState?: ConnectionState;
/** Additional context data */
data?: Record<string, any>;
}

View File

@ -0,0 +1,350 @@
/**
* SMTP Client Core Implementation
* Main client class with delegation to handlers
*/
import { EventEmitter } from 'node:events';
import type { Email } from '../../core/classes.email.js';
import type {
ISmtpClientOptions,
ISmtpSendResult,
ISmtpConnection,
IConnectionPoolStatus,
ConnectionState
} from './interfaces.js';
import { CONNECTION_STATES, SmtpErrorType } from './constants.js';
import type { ConnectionManager } from './connection-manager.js';
import type { CommandHandler } from './command-handler.js';
import type { AuthHandler } from './auth-handler.js';
import type { TlsHandler } from './tls-handler.js';
import type { SmtpErrorHandler } from './error-handler.js';
import { validateSender, validateRecipients } from './utils/validation.js';
import { logEmailSend, logPerformance, logDebug } from './utils/logging.js';
interface ISmtpClientDependencies {
options: ISmtpClientOptions;
connectionManager: ConnectionManager;
commandHandler: CommandHandler;
authHandler: AuthHandler;
tlsHandler: TlsHandler;
errorHandler: SmtpErrorHandler;
}
export class SmtpClient extends EventEmitter {
private options: ISmtpClientOptions;
private connectionManager: ConnectionManager;
private commandHandler: CommandHandler;
private authHandler: AuthHandler;
private tlsHandler: TlsHandler;
private errorHandler: SmtpErrorHandler;
private isShuttingDown: boolean = false;
constructor(dependencies: ISmtpClientDependencies) {
super();
this.options = dependencies.options;
this.connectionManager = dependencies.connectionManager;
this.commandHandler = dependencies.commandHandler;
this.authHandler = dependencies.authHandler;
this.tlsHandler = dependencies.tlsHandler;
this.errorHandler = dependencies.errorHandler;
this.setupEventForwarding();
}
/**
* Send an email
*/
public async sendMail(email: Email): Promise<ISmtpSendResult> {
const startTime = Date.now();
const fromAddress = email.from;
const recipients = Array.isArray(email.to) ? email.to : [email.to];
// Validate email addresses
if (!validateSender(fromAddress)) {
throw new Error(`Invalid sender address: ${fromAddress}`);
}
const recipientErrors = validateRecipients(recipients);
if (recipientErrors.length > 0) {
throw new Error(`Invalid recipients: ${recipientErrors.join(', ')}`);
}
logEmailSend('start', recipients, this.options);
let connection: ISmtpConnection | null = null;
const result: ISmtpSendResult = {
success: false,
acceptedRecipients: [],
rejectedRecipients: [],
envelope: {
from: fromAddress,
to: recipients
}
};
try {
// Get connection
connection = await this.connectionManager.getConnection();
connection.state = CONNECTION_STATES.BUSY as ConnectionState;
// Wait for greeting if new connection
if (!connection.capabilities) {
await this.commandHandler.waitForGreeting(connection);
}
// Perform EHLO
await this.commandHandler.sendEhlo(connection, this.options.domain);
// Upgrade to TLS if needed
if (this.tlsHandler.shouldUseTLS(connection)) {
await this.tlsHandler.upgradeToTLS(connection);
// Re-send EHLO after TLS upgrade
await this.commandHandler.sendEhlo(connection, this.options.domain);
}
// Authenticate if needed
if (this.options.auth) {
await this.authHandler.authenticate(connection);
}
// Send MAIL FROM
const mailFromResponse = await this.commandHandler.sendMailFrom(connection, fromAddress);
if (mailFromResponse.code >= 400) {
throw new Error(`MAIL FROM failed: ${mailFromResponse.message}`);
}
// Send RCPT TO for each recipient
for (const recipient of recipients) {
try {
const rcptResponse = await this.commandHandler.sendRcptTo(connection, recipient);
if (rcptResponse.code >= 400) {
result.rejectedRecipients.push(recipient);
logDebug(`Recipient rejected: ${recipient}`, this.options, { response: rcptResponse });
} else {
result.acceptedRecipients.push(recipient);
}
} catch (error) {
result.rejectedRecipients.push(recipient);
logDebug(`Recipient error: ${recipient}`, this.options, { error });
}
}
// Check if we have any accepted recipients
if (result.acceptedRecipients.length === 0) {
throw new Error('All recipients were rejected');
}
// Send DATA command
const dataResponse = await this.commandHandler.sendData(connection);
if (dataResponse.code !== 354) {
throw new Error(`DATA command failed: ${dataResponse.message}`);
}
// Send email content
const emailData = await this.formatEmailData(email);
const sendResponse = await this.commandHandler.sendDataContent(connection, emailData);
if (sendResponse.code >= 400) {
throw new Error(`Email data rejected: ${sendResponse.message}`);
}
// Success
result.success = true;
result.messageId = this.extractMessageId(sendResponse.message);
result.response = sendResponse.message;
connection.messageCount++;
logEmailSend('success', recipients, this.options, {
messageId: result.messageId,
duration: Date.now() - startTime
});
} catch (error) {
result.success = false;
result.error = error instanceof Error ? error : new Error(String(error));
// Classify error and determine if we should retry
const errorType = this.errorHandler.classifyError(result.error);
result.error = this.errorHandler.createError(
result.error.message,
errorType,
{ command: 'SEND_MAIL' },
result.error
);
logEmailSend('failure', recipients, this.options, {
error: result.error,
duration: Date.now() - startTime
});
} finally {
// Release connection
if (connection) {
connection.state = CONNECTION_STATES.READY as ConnectionState;
this.connectionManager.updateActivity(connection);
this.connectionManager.releaseConnection(connection);
}
logPerformance('sendMail', Date.now() - startTime, this.options);
}
return result;
}
/**
* Test connection to SMTP server
*/
public async verify(): Promise<boolean> {
let connection: ISmtpConnection | null = null;
try {
connection = await this.connectionManager.createConnection();
await this.commandHandler.waitForGreeting(connection);
await this.commandHandler.sendEhlo(connection, this.options.domain);
if (this.tlsHandler.shouldUseTLS(connection)) {
await this.tlsHandler.upgradeToTLS(connection);
await this.commandHandler.sendEhlo(connection, this.options.domain);
}
if (this.options.auth) {
await this.authHandler.authenticate(connection);
}
await this.commandHandler.sendQuit(connection);
return true;
} catch (error) {
logDebug('Connection verification failed', this.options, { error });
return false;
} finally {
if (connection) {
this.connectionManager.closeConnection(connection);
}
}
}
/**
* Check if client is connected
*/
public isConnected(): boolean {
const status = this.connectionManager.getPoolStatus();
return status.total > 0;
}
/**
* Get connection pool status
*/
public getPoolStatus(): IConnectionPoolStatus {
return this.connectionManager.getPoolStatus();
}
/**
* Update client options
*/
public updateOptions(newOptions: Partial<ISmtpClientOptions>): void {
this.options = { ...this.options, ...newOptions };
logDebug('Client options updated', this.options);
}
/**
* Close all connections and shutdown client
*/
public async close(): Promise<void> {
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
logDebug('Shutting down SMTP client', this.options);
try {
this.connectionManager.closeAllConnections();
this.emit('close');
} catch (error) {
logDebug('Error during client shutdown', this.options, { error });
}
}
private async formatEmailData(email: Email): Promise<string> {
// Convert Email object to raw SMTP data
const headers: string[] = [];
// Required headers
headers.push(`From: ${email.from}`);
headers.push(`To: ${Array.isArray(email.to) ? email.to.join(', ') : email.to}`);
headers.push(`Subject: ${email.subject || ''}`);
headers.push(`Date: ${new Date().toUTCString()}`);
headers.push(`Message-ID: <${Date.now()}.${Math.random().toString(36)}@${this.options.host}>`);
// Optional headers
if (email.cc) {
const cc = Array.isArray(email.cc) ? email.cc.join(', ') : email.cc;
headers.push(`Cc: ${cc}`);
}
if (email.bcc) {
const bcc = Array.isArray(email.bcc) ? email.bcc.join(', ') : email.bcc;
headers.push(`Bcc: ${bcc}`);
}
// Content headers
if (email.html && email.text) {
// Multipart message
const boundary = `boundary_${Date.now()}_${Math.random().toString(36)}`;
headers.push(`Content-Type: multipart/alternative; boundary="${boundary}"`);
headers.push('MIME-Version: 1.0');
const body = [
`--${boundary}`,
'Content-Type: text/plain; charset=utf-8',
'Content-Transfer-Encoding: quoted-printable',
'',
email.text,
'',
`--${boundary}`,
'Content-Type: text/html; charset=utf-8',
'Content-Transfer-Encoding: quoted-printable',
'',
email.html,
'',
`--${boundary}--`
].join('\r\n');
return headers.join('\r\n') + '\r\n\r\n' + body;
} else if (email.html) {
headers.push('Content-Type: text/html; charset=utf-8');
headers.push('MIME-Version: 1.0');
return headers.join('\r\n') + '\r\n\r\n' + email.html;
} else {
headers.push('Content-Type: text/plain; charset=utf-8');
headers.push('MIME-Version: 1.0');
return headers.join('\r\n') + '\r\n\r\n' + (email.text || '');
}
}
private extractMessageId(response: string): string | undefined {
// Try to extract message ID from server response
const match = response.match(/queued as ([^\s]+)/i) ||
response.match(/id=([^\s]+)/i) ||
response.match(/Message-ID: <([^>]+)>/i);
return match ? match[1] : undefined;
}
private setupEventForwarding(): void {
// Forward events from connection manager
this.connectionManager.on('connection', (connection) => {
this.emit('connection', connection);
});
this.connectionManager.on('disconnect', (connection) => {
this.emit('disconnect', connection);
});
this.connectionManager.on('error', (error) => {
this.emit('error', error);
});
}
}

View File

@ -0,0 +1,254 @@
/**
* SMTP Client TLS Handler
* TLS and STARTTLS client functionality
*/
import * as tls from 'node:tls';
import * as net from 'node:net';
import { DEFAULTS } from './constants.js';
import type {
ISmtpConnection,
ISmtpClientOptions,
ConnectionState
} from './interfaces.js';
import { CONNECTION_STATES } from './constants.js';
import { logTLS, logDebug } from './utils/logging.js';
import { isSuccessCode } from './utils/helpers.js';
import type { CommandHandler } from './command-handler.js';
export class TlsHandler {
private options: ISmtpClientOptions;
private commandHandler: CommandHandler;
constructor(options: ISmtpClientOptions, commandHandler: CommandHandler) {
this.options = options;
this.commandHandler = commandHandler;
}
/**
* Upgrade connection to TLS using STARTTLS
*/
public async upgradeToTLS(connection: ISmtpConnection): Promise<void> {
if (connection.secure) {
logDebug('Connection already secure', this.options);
return;
}
// Check if STARTTLS is supported
if (!connection.capabilities?.starttls) {
throw new Error('Server does not support STARTTLS');
}
logTLS('starttls_start', this.options);
try {
// Send STARTTLS command
const response = await this.commandHandler.sendStartTls(connection);
if (!isSuccessCode(response.code)) {
throw new Error(`STARTTLS command failed: ${response.message}`);
}
// Upgrade the socket to TLS
await this.performTLSUpgrade(connection);
// Clear capabilities as they may have changed after TLS
connection.capabilities = undefined;
connection.secure = true;
logTLS('starttls_success', this.options);
} catch (error) {
logTLS('starttls_failure', this.options, { error });
throw error;
}
}
/**
* Create a direct TLS connection
*/
public async createTLSConnection(host: string, port: number): Promise<tls.TLSSocket> {
return new Promise((resolve, reject) => {
const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
const tlsOptions: tls.ConnectionOptions = {
host,
port,
...this.options.tls,
// Default TLS options for email
secureProtocol: 'TLS_method',
ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
rejectUnauthorized: this.options.tls?.rejectUnauthorized !== false
};
logTLS('tls_connected', this.options, { host, port });
const socket = tls.connect(tlsOptions);
const timeoutHandler = setTimeout(() => {
socket.destroy();
reject(new Error(`TLS connection timeout after ${timeout}ms`));
}, timeout);
socket.once('secureConnect', () => {
clearTimeout(timeoutHandler);
if (!socket.authorized && this.options.tls?.rejectUnauthorized !== false) {
socket.destroy();
reject(new Error(`TLS certificate verification failed: ${socket.authorizationError}`));
return;
}
logDebug('TLS connection established', this.options, {
authorized: socket.authorized,
protocol: socket.getProtocol(),
cipher: socket.getCipher()
});
resolve(socket);
});
socket.once('error', (error) => {
clearTimeout(timeoutHandler);
reject(error);
});
});
}
/**
* Validate TLS certificate
*/
public validateCertificate(socket: tls.TLSSocket): boolean {
if (!socket.authorized) {
logDebug('TLS certificate not authorized', this.options, {
error: socket.authorizationError
});
// Allow self-signed certificates if explicitly configured
if (this.options.tls?.rejectUnauthorized === false) {
logDebug('Accepting unauthorized certificate (rejectUnauthorized: false)', this.options);
return true;
}
return false;
}
const cert = socket.getPeerCertificate();
if (!cert) {
logDebug('No peer certificate available', this.options);
return false;
}
// Additional certificate validation
const now = new Date();
if (cert.valid_from && new Date(cert.valid_from) > now) {
logDebug('Certificate not yet valid', this.options, { validFrom: cert.valid_from });
return false;
}
if (cert.valid_to && new Date(cert.valid_to) < now) {
logDebug('Certificate expired', this.options, { validTo: cert.valid_to });
return false;
}
logDebug('TLS certificate validated', this.options, {
subject: cert.subject,
issuer: cert.issuer,
validFrom: cert.valid_from,
validTo: cert.valid_to
});
return true;
}
/**
* Get TLS connection information
*/
public getTLSInfo(socket: tls.TLSSocket): any {
if (!(socket instanceof tls.TLSSocket)) {
return null;
}
return {
authorized: socket.authorized,
authorizationError: socket.authorizationError,
protocol: socket.getProtocol(),
cipher: socket.getCipher(),
peerCertificate: socket.getPeerCertificate(),
alpnProtocol: socket.alpnProtocol
};
}
/**
* Check if TLS upgrade is required or recommended
*/
public shouldUseTLS(connection: ISmtpConnection): boolean {
// Already secure
if (connection.secure) {
return false;
}
// Direct TLS connection configured
if (this.options.secure) {
return false; // Already handled in connection establishment
}
// STARTTLS available and not explicitly disabled
if (connection.capabilities?.starttls) {
return this.options.tls !== null && this.options.tls !== undefined; // Use TLS if configured
}
return false;
}
private async performTLSUpgrade(connection: ISmtpConnection): Promise<void> {
return new Promise((resolve, reject) => {
const plainSocket = connection.socket as net.Socket;
const timeout = this.options.connectionTimeout || DEFAULTS.CONNECTION_TIMEOUT;
const tlsOptions: tls.ConnectionOptions = {
socket: plainSocket,
host: this.options.host,
...this.options.tls,
// Default TLS options for STARTTLS
secureProtocol: 'TLS_method',
ciphers: 'HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA',
rejectUnauthorized: this.options.tls?.rejectUnauthorized !== false
};
const timeoutHandler = setTimeout(() => {
reject(new Error(`TLS upgrade timeout after ${timeout}ms`));
}, timeout);
// Create TLS socket from existing connection
const tlsSocket = tls.connect(tlsOptions);
tlsSocket.once('secureConnect', () => {
clearTimeout(timeoutHandler);
// Validate certificate if required
if (!this.validateCertificate(tlsSocket)) {
tlsSocket.destroy();
reject(new Error('TLS certificate validation failed'));
return;
}
// Replace the socket in the connection
connection.socket = tlsSocket;
connection.secure = true;
logDebug('STARTTLS upgrade completed', this.options, {
protocol: tlsSocket.getProtocol(),
cipher: tlsSocket.getCipher()
});
resolve();
});
tlsSocket.once('error', (error) => {
clearTimeout(timeoutHandler);
reject(error);
});
});
}
}

View File

@ -0,0 +1,224 @@
/**
* SMTP Client Helper Functions
* Protocol helper functions and utilities
*/
import { SMTP_CODES, REGEX_PATTERNS, LINE_ENDINGS } from '../constants.js';
import type { ISmtpResponse, ISmtpCapabilities } from '../interfaces.js';
/**
* Parse SMTP server response
*/
export function parseSmtpResponse(data: string): ISmtpResponse {
const lines = data.trim().split(/\r?\n/);
const firstLine = lines[0];
const match = firstLine.match(REGEX_PATTERNS.RESPONSE_CODE);
if (!match) {
return {
code: 500,
message: 'Invalid server response',
raw: data
};
}
const code = parseInt(match[1], 10);
const separator = match[2];
const message = lines.map(line => line.substring(4)).join(' ');
// Check for enhanced status code
const enhancedMatch = message.match(REGEX_PATTERNS.ENHANCED_STATUS);
const enhancedCode = enhancedMatch ? enhancedMatch[1] : undefined;
return {
code,
message: enhancedCode ? message.substring(enhancedCode.length + 1) : message,
enhancedCode,
raw: data
};
}
/**
* Parse EHLO response and extract capabilities
*/
export function parseEhloResponse(response: string): ISmtpCapabilities {
const lines = response.trim().split(/\r?\n/);
const capabilities: ISmtpCapabilities = {
extensions: new Set(),
authMethods: new Set(),
pipelining: false,
starttls: false,
eightBitMime: false
};
for (const line of lines.slice(1)) { // Skip first line (greeting)
const extensionLine = line.substring(4); // Remove "250-" or "250 "
const parts = extensionLine.split(/\s+/);
const extension = parts[0].toUpperCase();
capabilities.extensions.add(extension);
switch (extension) {
case 'PIPELINING':
capabilities.pipelining = true;
break;
case 'STARTTLS':
capabilities.starttls = true;
break;
case '8BITMIME':
capabilities.eightBitMime = true;
break;
case 'SIZE':
if (parts[1]) {
capabilities.maxSize = parseInt(parts[1], 10);
}
break;
case 'AUTH':
// Parse authentication methods
for (let i = 1; i < parts.length; i++) {
capabilities.authMethods.add(parts[i].toUpperCase());
}
break;
}
}
return capabilities;
}
/**
* Format SMTP command with proper line ending
*/
export function formatCommand(command: string, ...args: string[]): string {
const fullCommand = args.length > 0 ? `${command} ${args.join(' ')}` : command;
return fullCommand + LINE_ENDINGS.CRLF;
}
/**
* Encode authentication string for AUTH PLAIN
*/
export function encodeAuthPlain(username: string, password: string): string {
const authString = `\0${username}\0${password}`;
return Buffer.from(authString, 'utf8').toString('base64');
}
/**
* Encode authentication string for AUTH LOGIN
*/
export function encodeAuthLogin(value: string): string {
return Buffer.from(value, 'utf8').toString('base64');
}
/**
* Generate OAuth2 authentication string
*/
export function generateOAuth2String(username: string, accessToken: string): string {
const authString = `user=${username}\x01auth=Bearer ${accessToken}\x01\x01`;
return Buffer.from(authString, 'utf8').toString('base64');
}
/**
* Check if response code indicates success
*/
export function isSuccessCode(code: number): boolean {
return code >= 200 && code < 300;
}
/**
* Check if response code indicates temporary failure
*/
export function isTemporaryFailure(code: number): boolean {
return code >= 400 && code < 500;
}
/**
* Check if response code indicates permanent failure
*/
export function isPermanentFailure(code: number): boolean {
return code >= 500;
}
/**
* Escape email address for SMTP commands
*/
export function escapeEmailAddress(email: string): string {
return `<${email.trim()}>`;
}
/**
* Extract email address from angle brackets
*/
export function extractEmailAddress(email: string): string {
const match = email.match(/^<(.+)>$/);
return match ? match[1] : email.trim();
}
/**
* Generate unique connection ID
*/
export function generateConnectionId(): string {
return `smtp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Format timeout duration for human readability
*/
export function formatTimeout(milliseconds: number): string {
if (milliseconds < 1000) {
return `${milliseconds}ms`;
} else if (milliseconds < 60000) {
return `${Math.round(milliseconds / 1000)}s`;
} else {
return `${Math.round(milliseconds / 60000)}m`;
}
}
/**
* Validate and normalize email data size
*/
export function validateEmailSize(emailData: string, maxSize?: number): boolean {
const size = Buffer.byteLength(emailData, 'utf8');
return !maxSize || size <= maxSize;
}
/**
* Clean sensitive data from logs
*/
export function sanitizeForLogging(data: any): any {
if (typeof data !== 'object' || data === null) {
return data;
}
const sanitized = { ...data };
const sensitiveFields = ['password', 'pass', 'accessToken', 'refreshToken', 'clientSecret'];
for (const field of sensitiveFields) {
if (field in sanitized) {
sanitized[field] = '[REDACTED]';
}
}
return sanitized;
}
/**
* Calculate exponential backoff delay
*/
export function calculateBackoffDelay(attempt: number, baseDelay: number = 1000): number {
return Math.min(baseDelay * Math.pow(2, attempt - 1), 30000); // Max 30 seconds
}
/**
* Parse enhanced status code
*/
export function parseEnhancedStatusCode(code: string): { class: number; subject: number; detail: number } | null {
const match = code.match(/^(\d)\.(\d)\.(\d)$/);
if (!match) {
return null;
}
return {
class: parseInt(match[1], 10),
subject: parseInt(match[2], 10),
detail: parseInt(match[3], 10)
};
}

View File

@ -0,0 +1,212 @@
/**
* SMTP Client Logging Utilities
* Client-side logging utilities for SMTP operations
*/
import { logger } from '../../../../logger.js';
import type { ISmtpResponse, ISmtpClientOptions } from '../interfaces.js';
export interface ISmtpClientLogData {
component: string;
host?: string;
port?: number;
secure?: boolean;
command?: string;
response?: ISmtpResponse;
error?: Error;
connectionId?: string;
messageId?: string;
duration?: number;
[key: string]: any;
}
/**
* Log SMTP client connection events
*/
export function logConnection(
event: 'connecting' | 'connected' | 'disconnected' | 'error',
options: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
const logData: ISmtpClientLogData = {
component: 'smtp-client',
event,
host: options.host,
port: options.port,
secure: options.secure,
...data
};
switch (event) {
case 'connecting':
logger.info('SMTP client connecting', logData);
break;
case 'connected':
logger.info('SMTP client connected', logData);
break;
case 'disconnected':
logger.info('SMTP client disconnected', logData);
break;
case 'error':
logger.error('SMTP client connection error', logData);
break;
}
}
/**
* Log SMTP command execution
*/
export function logCommand(
command: string,
response?: ISmtpResponse,
options?: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
const logData: ISmtpClientLogData = {
component: 'smtp-client',
command,
response,
host: options?.host,
port: options?.port,
...data
};
if (response && response.code >= 400) {
logger.warn('SMTP command failed', logData);
} else {
logger.debug('SMTP command executed', logData);
}
}
/**
* Log authentication events
*/
export function logAuthentication(
event: 'start' | 'success' | 'failure',
method: string,
options: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
const logData: ISmtpClientLogData = {
component: 'smtp-client',
event: `auth_${event}`,
authMethod: method,
host: options.host,
port: options.port,
...data
};
switch (event) {
case 'start':
logger.debug('SMTP authentication started', logData);
break;
case 'success':
logger.info('SMTP authentication successful', logData);
break;
case 'failure':
logger.error('SMTP authentication failed', logData);
break;
}
}
/**
* Log TLS/STARTTLS events
*/
export function logTLS(
event: 'starttls_start' | 'starttls_success' | 'starttls_failure' | 'tls_connected',
options: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
const logData: ISmtpClientLogData = {
component: 'smtp-client',
event,
host: options.host,
port: options.port,
...data
};
if (event.includes('failure')) {
logger.error('SMTP TLS operation failed', logData);
} else {
logger.info('SMTP TLS operation', logData);
}
}
/**
* Log email sending events
*/
export function logEmailSend(
event: 'start' | 'success' | 'failure',
recipients: string[],
options: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
const logData: ISmtpClientLogData = {
component: 'smtp-client',
event: `send_${event}`,
recipientCount: recipients.length,
recipients: recipients.slice(0, 5), // Only log first 5 recipients for privacy
host: options.host,
port: options.port,
...data
};
switch (event) {
case 'start':
logger.info('SMTP email send started', logData);
break;
case 'success':
logger.info('SMTP email send successful', logData);
break;
case 'failure':
logger.error('SMTP email send failed', logData);
break;
}
}
/**
* Log performance metrics
*/
export function logPerformance(
operation: string,
duration: number,
options: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
const logData: ISmtpClientLogData = {
component: 'smtp-client',
operation,
duration,
host: options.host,
port: options.port,
...data
};
if (duration > 10000) { // Log slow operations (>10s)
logger.warn('SMTP slow operation detected', logData);
} else {
logger.debug('SMTP operation performance', logData);
}
}
/**
* Log debug information (only when debug is enabled)
*/
export function logDebug(
message: string,
options: ISmtpClientOptions,
data?: Partial<ISmtpClientLogData>
): void {
if (!options.debug) {
return;
}
const logData: ISmtpClientLogData = {
component: 'smtp-client-debug',
host: options.host,
port: options.port,
...data
};
logger.debug(`[SMTP Client Debug] ${message}`, logData);
}

View File

@ -0,0 +1,154 @@
/**
* SMTP Client Validation Utilities
* Input validation functions for SMTP client operations
*/
import { REGEX_PATTERNS } from '../constants.js';
import type { ISmtpClientOptions, ISmtpAuthOptions } from '../interfaces.js';
/**
* Validate email address format
*/
export function validateEmailAddress(email: string): boolean {
if (!email || typeof email !== 'string') {
return false;
}
return REGEX_PATTERNS.EMAIL_ADDRESS.test(email.trim());
}
/**
* Validate SMTP client options
*/
export function validateClientOptions(options: ISmtpClientOptions): string[] {
const errors: string[] = [];
// Required fields
if (!options.host || typeof options.host !== 'string') {
errors.push('Host is required and must be a string');
}
if (!options.port || typeof options.port !== 'number' || options.port < 1 || options.port > 65535) {
errors.push('Port must be a number between 1 and 65535');
}
// Optional field validation
if (options.connectionTimeout !== undefined) {
if (typeof options.connectionTimeout !== 'number' || options.connectionTimeout < 1000) {
errors.push('Connection timeout must be a number >= 1000ms');
}
}
if (options.socketTimeout !== undefined) {
if (typeof options.socketTimeout !== 'number' || options.socketTimeout < 1000) {
errors.push('Socket timeout must be a number >= 1000ms');
}
}
if (options.maxConnections !== undefined) {
if (typeof options.maxConnections !== 'number' || options.maxConnections < 1) {
errors.push('Max connections must be a positive number');
}
}
if (options.maxMessages !== undefined) {
if (typeof options.maxMessages !== 'number' || options.maxMessages < 1) {
errors.push('Max messages must be a positive number');
}
}
// Validate authentication options
if (options.auth) {
errors.push(...validateAuthOptions(options.auth));
}
return errors;
}
/**
* Validate authentication options
*/
export function validateAuthOptions(auth: ISmtpAuthOptions): string[] {
const errors: string[] = [];
if (auth.method && !['PLAIN', 'LOGIN', 'OAUTH2', 'AUTO'].includes(auth.method)) {
errors.push('Invalid authentication method');
}
// For basic auth, require user and pass
if ((auth.user || auth.pass) && (!auth.user || !auth.pass)) {
errors.push('Both user and pass are required for basic authentication');
}
// For OAuth2, validate required fields
if (auth.oauth2) {
const oauth = auth.oauth2;
if (!oauth.user || !oauth.clientId || !oauth.clientSecret || !oauth.refreshToken) {
errors.push('OAuth2 requires user, clientId, clientSecret, and refreshToken');
}
if (oauth.user && !validateEmailAddress(oauth.user)) {
errors.push('OAuth2 user must be a valid email address');
}
}
return errors;
}
/**
* Validate hostname format
*/
export function validateHostname(hostname: string): boolean {
if (!hostname || typeof hostname !== 'string') {
return false;
}
// Basic hostname validation (allow IP addresses and domain names)
const hostnameRegex = /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$|^(?:\d{1,3}\.){3}\d{1,3}$|^\[(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\]$/;
return hostnameRegex.test(hostname);
}
/**
* Validate port number
*/
export function validatePort(port: number): boolean {
return typeof port === 'number' && port >= 1 && port <= 65535;
}
/**
* Sanitize and validate domain name for EHLO
*/
export function validateAndSanitizeDomain(domain: string): string {
if (!domain || typeof domain !== 'string') {
return 'localhost';
}
const sanitized = domain.trim().toLowerCase();
if (validateHostname(sanitized)) {
return sanitized;
}
return 'localhost';
}
/**
* Validate recipient list
*/
export function validateRecipients(recipients: string | string[]): string[] {
const errors: string[] = [];
const recipientList = Array.isArray(recipients) ? recipients : [recipients];
for (const recipient of recipientList) {
if (!validateEmailAddress(recipient)) {
errors.push(`Invalid email address: ${recipient}`);
}
}
return errors;
}
/**
* Validate sender address
*/
export function validateSender(sender: string): boolean {
return validateEmailAddress(sender);
}

View File

@ -25,7 +25,7 @@ import { BounceManager, BounceType, BounceCategory } from '../core/classes.bounc
import * as net from 'node:net';
import * as tls from 'node:tls';
import * as stream from 'node:stream';
import { SMTPServer as MtaSmtpServer } from '../delivery/classes.smtpserver.js';
import { createSmtpServer } from '../delivery/smtpserver/index.js';
import { MultiModeDeliverySystem, type IMultiModeDeliveryOptions } from '../delivery/classes.delivery.system.js';
import { UnifiedDeliveryQueue, type IQueueOptions } from '../delivery/classes.delivery.queue.js';
import { SmtpState } from '../delivery/interfaces.js';
@ -141,7 +141,7 @@ export interface IServerStats {
export class UnifiedEmailServer extends EventEmitter {
private options: IUnifiedEmailServerOptions;
private domainRouter: DomainRouter;
private servers: MtaSmtpServer[] = [];
private servers: any[] = [];
private stats: IServerStats;
private processingTimes: number[] = [];
@ -361,7 +361,7 @@ export class UnifiedEmailServer extends EventEmitter {
};
// Create and start the SMTP server
const smtpServer = new MtaSmtpServer(mtaRef as any, serverOptions);
const smtpServer = createSmtpServer(mtaRef as any, serverOptions);
this.servers.push(smtpServer);
// Start the server