This commit is contained in:
Philipp Kunz 2025-05-21 12:52:24 +00:00
parent b0a0078ad0
commit 3f220996ee
14 changed files with 4773 additions and 197 deletions

View File

@ -1,76 +1,258 @@
# DCRouter Project Improvement Plan
# SMTP Server Refactoring Plan
## Type Safety Enhancement Plan
## Problem Statement
Our goal is to improve type safety across the DCRouter codebase to reduce runtime errors, improve developer experience, and make the code more maintainable. This document outlines the specific changes we'll implement.
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.
### 1. Platform Service Interface Improvements
## Refactoring Goals
- [ ] Create a comprehensive `IPlatformService` interface to replace `any` type usage
- [ ] Define specific methods and properties that platform services should implement
- [ ] Update all references to use the new interface
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
3. Improve maintainability by reducing file sizes and complexity
4. Preserve existing functionality and behavior while improving the architecture
### 2. SMTP Session Type Safety
## Proposed File Structure
- [ ] Define a proper `ISmtpSession` interface with all required properties
- [ ] Ensure consistent usage across SMTPServer implementation
- [ ] Add proper validation for session properties
```
mail/
└── delivery/
├── smtp/
│ ├── 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
│ └── utils/
│ ├── validation.ts - Validation utilities
│ ├── logging.ts - Logging utilities
│ └── helpers.ts - Other helper functions
├── classes.smtpserver.ts - Legacy file (will be deprecated)
└── interfaces.ts - Main delivery interfaces
```
### 3. Configuration Type Enhancements
## Module Responsibilities
- [ ] Replace loose config objects with strictly typed interfaces
- [ ] Add validation functions for all configuration objects
- [ ] Create union types for configuration options with specific values
### 1. smtp-server.ts (150-200 lines)
- Core server initialization and lifecycle management
- Server startup and shutdown
- Port binding and socket creation
- Delegates to other modules for specific functionality
### 4. Function Return Types
### 2. session-manager.ts (100-150 lines)
- Session creation and tracking
- Session timeout management
- Session cleanup
- Session state management
- [ ] Audit all async functions to ensure they have explicit return types
- [ ] Replace `Promise<any>` with specific return type interfaces
- [ ] Add proper error types for rejected promises
### 3. command-handler.ts (200-250 lines)
- SMTP command parsing and routing
- Command validation
- Command implementation (EHLO, MAIL FROM, RCPT TO, etc.)
- Response formatting
### 5. String Literal Types and Enums
### 4. data-handler.ts (150-200 lines)
- Email data collection and processing
- DATA command handling
- MIME parsing
- Email creation and forwarding
- Email storage
- [ ] Replace string constants with proper TypeScript enums or string literal unions
- [ ] Create specific types for email status values, priorities, etc.
- [ ] Ensure consistent usage throughout the codebase
### 5. tls-handler.ts (100-150 lines)
- TLS connection handling
- STARTTLS implementation
- Certificate management
- Secure socket creation
### 6. Event System Types
### 6. security-handler.ts (100-150 lines)
- IP reputation checking
- Access control
- Rate limiting
- Security logging
- SPAM detection
- [ ] Create typed event emitters for all event-based components
- [ ] Define specific event payload interfaces for each event type
- [ ] Ensure type safety for event handlers
### 7. connection-manager.ts (100-150 lines)
- Connection tracking and limits
- Idle connection cleanup
- Connection error handling
- Socket event handling
### 7. Authentication Data Types
### 8. interfaces.ts (100-150 lines)
- All interface definitions
- Type aliases
- Type guards
- Enum definitions
- [ ] Create proper interfaces for authentication data
- [ ] Replace generic Record types with specific property interfaces
- [ ] Add validation for auth data objects
## Implementation Strategy
### 8. Email Attachment Type Safety
### Phase 1: Initial Structure and Scaffolding (Days 1-2)
- [ ] Define comprehensive interfaces for email attachments
- [ ] Ensure consistent usage between different email handling components
- [ ] Add validation for attachment properties
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)
### 9. Processing Mode Type Safety
2. Move interfaces to the new interfaces.ts file
- Extract all interfaces from current implementation
- Add proper documentation
- Add any missing interface properties
- [ ] Create discriminated unions for different email processing modes
- [ ] Add type guards to ensure safe handling of mode-specific properties
- [ ] Ensure proper validation of processing mode values
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
### 10. Third-Party Integration Types
4. Set up the basic structure for each module
- Define basic class skeletons
- Set up dependency injection structure
- Document interfaces for each module
- [ ] Review and update type definitions for third-party dependencies
- [ ] Create additional type definitions where missing
- [ ] Ensure consistent usage of external libraries
### Phase 2: Gradual Implementation Transfer (Days 3-7)
## Implementation Order
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
We'll implement these improvements in the following order:
2. Implement session-manager.ts
- Extract session creation and management logic
- Implement timeout handling
- Add session state management functions
1. First, focus on the core interfaces (Platform Service, SMTP Session)
2. Next, improve configuration types as they affect multiple components
3. Then, address function return types and string literals
4. Finally, handle the remaining specialized types (auth, attachments, etc.)
3. Implement connection-manager.ts
- Extract connection tracking code
- Implement connection limits
- Add socket event handling logic
This approach allows us to tackle the most widely-used types first, providing the greatest immediate benefit while establishing patterns for the rest of the implementation.
4. Implement command-handler.ts
- Extract command parsing and processing
- Split command handlers into separate methods
- Implement command validation and routing
5. Implement data-handler.ts
- Extract email data processing logic
- Implement DATA command handling
- Add email storage and forwarding
6. Implement tls-handler.ts
- Extract TLS connection handling
- Implement STARTTLS functionality
- Add certificate management
7. Implement security-handler.ts
- Extract IP reputation checking
- Implement security logging
- Add access control functionality
### Phase 3: Core Server 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
2. Update event handling
- Ensure proper event propagation between modules
- Implement event delegation pattern
- Add missing event handlers
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
### Phase 4: Legacy Compatibility (Days 11-12)
1. Create facade in classes.smtpserver.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
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
## 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
2. Integration Testing
- Test interactions between modules
- Verify correct event propagation
- Test full SMTP communication flow
3. Regression Testing
- Ensure all existing tests pass
- Verify no functionality is lost
- Compare performance metrics
4. Compatibility Testing
- Test with existing code that uses the legacy class
- Verify backward compatibility
- Document any necessary migration steps
## Timeline Estimate
- Phase 1: 1-2 days
- Phase 2: 3-5 days
- Phase 3: 2-3 days
- Phase 4: 1-2 days
Total: 7-12 days of development time
## Success Criteria
1. All existing tests pass with the new implementation
2. No regression in functionality or performance
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
## Risks and Mitigations
### Risk: Breaking existing functionality
**Mitigation**: Comprehensive test coverage and backward compatibility facade
### Risk: Performance degradation due to additional indirection
**Mitigation**: Performance benchmarking before and after refactoring
### Risk: Increased complexity due to distributed code
**Mitigation**: Clear documentation and proper module interfaces
### Risk: Time overrun due to unforeseen dependencies
**Mitigation**: Incremental approach with working checkpoints after each phase
## 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

File diff suppressed because it is too large Load Diff

View File

@ -140,6 +140,11 @@ export interface ISmtpSession {
* Timestamp of last activity for session timeout tracking
*/
lastActivity?: number;
/**
* Timeout ID for DATA command timeout
*/
dataTimeoutId?: NodeJS.Timeout;
}
/**
@ -186,11 +191,31 @@ export interface ISmtpServerOptions {
*/
hostname?: string;
/**
* Host address to bind to (defaults to all interfaces)
*/
host?: string;
/**
* Secure port for dedicated TLS connections
*/
securePort?: number;
/**
* CA certificates for TLS (PEM format)
*/
ca?: string;
/**
* Maximum size of messages in bytes
*/
maxSize?: number;
/**
* Maximum number of concurrent connections
*/
maxConnections?: number;
/**
* Authentication options
*/
@ -207,14 +232,37 @@ export interface ISmtpServerOptions {
};
/**
* Socket timeout in milliseconds (default: 5 minutes)
* Socket timeout in milliseconds (default: 5 minutes / 300000ms)
*/
socketTimeout?: number;
/**
* Initial connection timeout in milliseconds (default: 30 seconds)
* Initial connection timeout in milliseconds (default: 30 seconds / 30000ms)
*/
connectionTimeout?: number;
/**
* Interval for checking idle sessions in milliseconds (default: 5 seconds / 5000ms)
* For testing, can be set lower (e.g. 1000ms) to detect timeouts more quickly
*/
cleanupInterval?: number;
/**
* Maximum number of recipients allowed per message (default: 100)
*/
maxRecipients?: number;
/**
* Maximum message size in bytes (default: 10MB / 10485760 bytes)
* This is advertised in the EHLO SIZE extension
*/
size?: number;
/**
* Timeout for the DATA command in milliseconds (default: 60000ms / 1 minute)
* This controls how long to wait for the complete email data
*/
dataTimeout?: number;
}
/**

View File

@ -0,0 +1,632 @@
/**
* SMTP Command Handler
* Responsible for parsing and handling SMTP commands
*/
import * as plugins from '../../../plugins.js';
import { SmtpState, ISmtpSession, IEnvelopeRecipient } from '../interfaces.js';
import { ICommandHandler, ISessionManager, IDataHandler, ITlsHandler, ISecurityHandler } from './interfaces.js';
import { SmtpCommand, SmtpResponseCode, SMTP_DEFAULTS, SMTP_EXTENSIONS } from './constants.js';
import { SmtpLogger } from './utils/logging.js';
import { extractCommandName, extractCommandArgs, formatMultilineResponse } from './utils/helpers.js';
import { validateEhlo, validateMailFrom, validateRcptTo, isValidCommandSequence } from './utils/validation.js';
/**
* Handles SMTP commands and responses
*/
export class CommandHandler implements ICommandHandler {
/**
* Session manager instance
*/
private sessionManager: ISessionManager;
/**
* Data handler instance (optional, injected when processing DATA command)
*/
private dataHandler?: IDataHandler;
/**
* TLS handler instance (optional, injected when processing STARTTLS command)
*/
private tlsHandler?: ITlsHandler;
/**
* Security handler instance (optional, used for IP reputation and authentication)
*/
private securityHandler?: ISecurityHandler;
/**
* SMTP server options
*/
private options: {
hostname: string;
size?: number;
maxRecipients: number;
auth?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
};
};
/**
* Creates a new command handler
* @param sessionManager - Session manager instance
* @param options - Command handler options
* @param dataHandler - Optional data handler instance
* @param tlsHandler - Optional TLS handler instance
* @param securityHandler - Optional security handler instance
*/
constructor(
sessionManager: ISessionManager,
options: {
hostname?: string;
size?: number;
maxRecipients?: number;
auth?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
};
} = {},
dataHandler?: IDataHandler,
tlsHandler?: ITlsHandler,
securityHandler?: ISecurityHandler
) {
this.sessionManager = sessionManager;
this.dataHandler = dataHandler;
this.tlsHandler = tlsHandler;
this.securityHandler = securityHandler;
this.options = {
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME,
size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE,
maxRecipients: options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS,
auth: options.auth
};
}
/**
* Process a command from the client
* @param socket - Client socket
* @param commandLine - Command line from client
*/
public processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
SmtpLogger.warn(`No session found for socket from ${socket.remoteAddress}`);
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
socket.end();
return;
}
// Handle data state differently - pass to data handler
if (session.state === SmtpState.DATA_RECEIVING) {
if (this.dataHandler) {
// Let the data handler process the line
this.dataHandler.processEmailData(socket, commandLine)
.catch(error => {
SmtpLogger.error(`Error processing email data: ${error.message}`, {
sessionId: session.id,
error
});
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email data: ${error.message}`);
this.resetSession(session);
});
} else {
// No data handler available
SmtpLogger.error('Data handler not available', { sessionId: session.id });
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - data handler not available`);
this.resetSession(session);
}
return;
}
// Log received command
SmtpLogger.logCommand(commandLine, socket, session);
// Extract command and arguments
const command = extractCommandName(commandLine);
const args = extractCommandArgs(commandLine);
// Validate command sequence
if (!this.validateCommandSequence(command, session)) {
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} Bad sequence of commands`);
return;
}
// Process the command
switch (command) {
case SmtpCommand.EHLO:
case SmtpCommand.HELO:
this.handleEhlo(socket, args);
break;
case SmtpCommand.MAIL_FROM:
this.handleMailFrom(socket, args);
break;
case SmtpCommand.RCPT_TO:
this.handleRcptTo(socket, args);
break;
case SmtpCommand.DATA:
this.handleData(socket);
break;
case SmtpCommand.RSET:
this.handleRset(socket);
break;
case SmtpCommand.NOOP:
this.handleNoop(socket);
break;
case SmtpCommand.QUIT:
this.handleQuit(socket);
break;
case SmtpCommand.STARTTLS:
if (this.tlsHandler && this.tlsHandler.isTlsEnabled()) {
this.tlsHandler.handleStartTls(socket);
} else {
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} STARTTLS not available`);
}
break;
case SmtpCommand.AUTH:
this.handleAuth(socket, args);
break;
case SmtpCommand.HELP:
this.handleHelp(socket, args);
break;
default:
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Command not implemented`);
break;
}
}
/**
* Send a response to the client
* @param socket - Client socket
* @param response - Response to send
*/
public sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
SmtpLogger.logResponse(response, socket);
} catch (error) {
// Log error and destroy socket
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
response,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: error instanceof Error ? error : new Error(String(error))
});
socket.destroy();
}
}
/**
* Handle EHLO command
* @param socket - Client socket
* @param clientHostname - Client hostname from EHLO command
*/
public handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Validate EHLO hostname
const validation = validateEhlo(clientHostname);
if (!validation.isValid) {
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`);
return;
}
// Update session state and client hostname
session.clientHostname = validation.hostname || clientHostname;
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO);
// Set up EHLO response lines
const responseLines = [
`${this.options.hostname} greets ${session.clientHostname}`,
SMTP_EXTENSIONS.PIPELINING,
SMTP_EXTENSIONS.formatExtension(SMTP_EXTENSIONS.SIZE, this.options.size),
SMTP_EXTENSIONS.EIGHTBITMIME,
SMTP_EXTENSIONS.ENHANCEDSTATUSCODES
];
// Add TLS extension if available and not already using TLS
if (this.tlsHandler && this.tlsHandler.isTlsEnabled() && !session.useTLS) {
responseLines.push(SMTP_EXTENSIONS.STARTTLS);
}
// Add AUTH extension if configured
if (this.options.auth && this.options.auth.methods && this.options.auth.methods.length > 0) {
responseLines.push(`${SMTP_EXTENSIONS.AUTH} ${this.options.auth.methods.join(' ')}`);
}
// Send multiline response
this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.OK, responseLines));
}
/**
* Handle MAIL FROM command
* @param socket - Client socket
* @param args - Command arguments
*/
public handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Check if authentication is required but not provided
if (this.options.auth && this.options.auth.required && !session.authenticated) {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_REQUIRED} Authentication required`);
return;
}
// Validate MAIL FROM syntax
const validation = validateMailFrom(args);
if (!validation.isValid) {
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`);
return;
}
// Check size parameter if provided
if (validation.params && validation.params.SIZE) {
const size = parseInt(validation.params.SIZE, 10);
if (isNaN(size)) {
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} Invalid SIZE parameter`);
return;
}
if (size > this.options.size!) {
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message size exceeds limit`);
return;
}
}
// Reset email data and recipients for new transaction
session.mailFrom = validation.address || '';
session.rcptTo = [];
session.emailData = '';
session.emailDataChunks = [];
// Update envelope information
session.envelope = {
mailFrom: {
address: validation.address || '',
args: validation.params || {}
},
rcptTo: []
};
// Update session state
this.sessionManager.updateSessionState(session, SmtpState.MAIL_FROM);
// Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
}
/**
* Handle RCPT TO command
* @param socket - Client socket
* @param args - Command arguments
*/
public handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Validate RCPT TO syntax
const validation = validateRcptTo(args);
if (!validation.isValid) {
this.sendResponse(socket, `${SmtpResponseCode.SYNTAX_ERROR_PARAMETERS} ${validation.errorMessage}`);
return;
}
// Check if we've reached maximum recipients
if (session.rcptTo.length >= this.options.maxRecipients) {
this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Too many recipients`);
return;
}
// Create recipient object
const recipient: IEnvelopeRecipient = {
address: validation.address || '',
args: validation.params || {}
};
// Add to session data
session.rcptTo.push(validation.address || '');
session.envelope.rcptTo.push(recipient);
// Update session state
this.sessionManager.updateSessionState(session, SmtpState.RCPT_TO);
// Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} Recipient ok`);
}
/**
* Handle DATA command
* @param socket - Client socket
*/
public handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Check if we have recipients
if (!session.rcptTo.length) {
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} No recipients specified`);
return;
}
// Update session state
this.sessionManager.updateSessionState(session, SmtpState.DATA_RECEIVING);
// Reset email data storage
session.emailData = '';
session.emailDataChunks = [];
// Set up timeout for DATA command
const dataTimeout = SMTP_DEFAULTS.DATA_TIMEOUT;
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
}
session.dataTimeoutId = setTimeout(() => {
if (session.state === SmtpState.DATA_RECEIVING) {
SmtpLogger.warn(`DATA command timeout for session ${session.id}`, {
sessionId: session.id,
timeout: dataTimeout
});
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`);
this.resetSession(session);
}
}, dataTimeout);
// Send intermediate response to signal start of data
this.sendResponse(socket, `${SmtpResponseCode.START_MAIL_INPUT} Start mail input; end with <CRLF>.<CRLF>`);
}
/**
* Handle RSET command
* @param socket - Client socket
*/
public handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Reset the transaction state
this.resetSession(session);
// Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
}
/**
* Handle NOOP command
* @param socket - Client socket
*/
public handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Update session activity timestamp
this.sessionManager.updateSessionActivity(session);
// Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} OK`);
}
/**
* Handle QUIT command
* @param socket - Client socket
*/
public handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
// Send goodbye message
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`);
// End the connection
socket.end();
// Clean up session if we have one
if (session) {
this.sessionManager.removeSession(socket);
}
}
/**
* Handle AUTH command
* @param socket - Client socket
* @param args - Command arguments
*/
private handleAuth(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Check if we have auth config
if (!this.options.auth || !this.options.auth.methods || !this.options.auth.methods.length) {
this.sendResponse(socket, `${SmtpResponseCode.COMMAND_NOT_IMPLEMENTED} Authentication not supported`);
return;
}
// Check if TLS is required for authentication
if (!session.useTLS) {
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication requires TLS`);
return;
}
// Simple response for now - authentication would be implemented in the security handler
this.sendResponse(socket, `${SmtpResponseCode.AUTH_FAILED} Authentication not implemented yet`);
}
/**
* Handle HELP command
* @param socket - Client socket
* @param args - Command arguments
*/
private handleHelp(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Update session activity timestamp
this.sessionManager.updateSessionActivity(session);
// Provide help information based on arguments
const helpCommand = args.trim().toUpperCase();
if (!helpCommand) {
// General help
const helpLines = [
'Supported commands:',
'EHLO/HELO domain - Identify yourself to the server',
'MAIL FROM:<address> - Start a new mail transaction',
'RCPT TO:<address> - Specify recipients for the message',
'DATA - Start message data input',
'RSET - Reset the transaction',
'NOOP - No operation',
'QUIT - Close the connection',
'HELP [command] - Show help'
];
// Add conditional commands
if (this.tlsHandler && this.tlsHandler.isTlsEnabled()) {
helpLines.push('STARTTLS - Start TLS negotiation');
}
if (this.options.auth && this.options.auth.methods.length) {
helpLines.push('AUTH mechanism - Authenticate with the server');
}
this.sendResponse(socket, formatMultilineResponse(SmtpResponseCode.HELP_MESSAGE, helpLines));
return;
}
// Command-specific help
let helpText: string;
switch (helpCommand) {
case 'EHLO':
case 'HELO':
helpText = 'EHLO/HELO domain - Identify yourself to the server';
break;
case 'MAIL':
helpText = 'MAIL FROM:<address> [SIZE=size] - Start a new mail transaction';
break;
case 'RCPT':
helpText = 'RCPT TO:<address> - Specify a recipient for the message';
break;
case 'DATA':
helpText = 'DATA - Start message data input, end with <CRLF>.<CRLF>';
break;
case 'RSET':
helpText = 'RSET - Reset the transaction';
break;
case 'NOOP':
helpText = 'NOOP - No operation';
break;
case 'QUIT':
helpText = 'QUIT - Close the connection';
break;
case 'STARTTLS':
helpText = 'STARTTLS - Start TLS negotiation';
break;
case 'AUTH':
helpText = `AUTH mechanism - Authenticate with the server. Supported methods: ${this.options.auth?.methods.join(', ')}`;
break;
default:
helpText = `Unknown command: ${helpCommand}`;
break;
}
this.sendResponse(socket, `${SmtpResponseCode.HELP_MESSAGE} ${helpText}`);
}
/**
* Reset session to after-EHLO state
* @param session - SMTP session to reset
*/
private resetSession(session: ISmtpSession): void {
// Clear any data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
// Reset data fields but keep authentication state
session.mailFrom = '';
session.rcptTo = [];
session.emailData = '';
session.emailDataChunks = [];
session.envelope = {
mailFrom: { address: '', args: {} },
rcptTo: []
};
// Reset state to after EHLO
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO);
}
/**
* Validate command sequence based on current state
* @param command - Command to validate
* @param session - Current session
* @returns Whether the command is valid in the current state
*/
private validateCommandSequence(command: string, session: ISmtpSession): boolean {
return isValidCommandSequence(command, session.state);
}
}

View File

@ -0,0 +1,363 @@
/**
* SMTP Connection Manager
* Responsible for managing socket connections to the SMTP server
*/
import * as plugins from '../../../plugins.js';
import { IConnectionManager } from './interfaces.js';
import { ISessionManager } from './interfaces.js';
import { SmtpResponseCode, SMTP_DEFAULTS } from './constants.js';
import { SmtpLogger } from './utils/logging.js';
import { getSocketDetails, formatMultilineResponse } from './utils/helpers.js';
/**
* Manager for SMTP connections
* Handles connection setup, event listeners, and lifecycle management
*/
export class ConnectionManager implements IConnectionManager {
/**
* Set of active socket connections
*/
private activeConnections: Set<plugins.net.Socket | plugins.tls.TLSSocket> = new Set();
/**
* Reference to the session manager
*/
private sessionManager: ISessionManager;
/**
* SMTP server options
*/
private options: {
hostname: string;
maxConnections: number;
socketTimeout: number;
};
/**
* Command handler function
*/
private commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void;
/**
* Creates a new connection manager
* @param sessionManager - Session manager instance
* @param commandHandler - Command handler function
* @param options - Connection manager options
*/
constructor(
sessionManager: ISessionManager,
commandHandler: (socket: plugins.net.Socket | plugins.tls.TLSSocket, line: string) => void,
options: {
hostname?: string;
maxConnections?: number;
socketTimeout?: number;
} = {}
) {
this.sessionManager = sessionManager;
this.commandHandler = commandHandler;
this.options = {
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME,
maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS,
socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT
};
}
/**
* Handle a new connection
* @param socket - Client socket
*/
public handleNewConnection(socket: plugins.net.Socket): void {
// Check if maximum connections reached
if (this.hasReachedMaxConnections()) {
this.rejectConnection(socket, 'Too many connections');
return;
}
// Add socket to active connections
this.activeConnections.add(socket);
// Set up socket options
socket.setKeepAlive(true);
socket.setTimeout(this.options.socketTimeout);
// Set up event handlers
this.setupSocketEventHandlers(socket);
// Create a session for this connection
this.sessionManager.createSession(socket, false);
// Log the new connection
const socketDetails = getSocketDetails(socket);
SmtpLogger.logConnection(socket, 'connect');
// Send greeting
this.sendGreeting(socket);
}
/**
* Handle a new secure TLS connection
* @param socket - Client TLS socket
*/
public handleNewSecureConnection(socket: plugins.tls.TLSSocket): void {
// Check if maximum connections reached
if (this.hasReachedMaxConnections()) {
this.rejectConnection(socket, 'Too many connections');
return;
}
// Add socket to active connections
this.activeConnections.add(socket);
// Set up socket options
socket.setKeepAlive(true);
socket.setTimeout(this.options.socketTimeout);
// Set up event handlers
this.setupSocketEventHandlers(socket);
// Create a session for this connection
this.sessionManager.createSession(socket, true);
// Log the new secure connection
SmtpLogger.logConnection(socket, 'connect');
// Send greeting
this.sendGreeting(socket);
}
/**
* Set up event handlers for a socket
* @param socket - Client socket
*/
public setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Store existing socket event handlers before adding new ones
const existingDataHandler = socket.listeners('data')[0];
const existingCloseHandler = socket.listeners('close')[0];
const existingErrorHandler = socket.listeners('error')[0];
const existingTimeoutHandler = socket.listeners('timeout')[0];
// Remove existing event handlers if they exist
if (existingDataHandler) socket.removeListener('data', existingDataHandler);
if (existingCloseHandler) socket.removeListener('close', existingCloseHandler);
if (existingErrorHandler) socket.removeListener('error', existingErrorHandler);
if (existingTimeoutHandler) socket.removeListener('timeout', existingTimeoutHandler);
// Data event - process incoming data from the client
let buffer = '';
socket.on('data', (data) => {
// Get current session and update activity timestamp
const session = this.sessionManager.getSession(socket);
if (session) {
this.sessionManager.updateSessionActivity(session);
}
// Buffer incoming data
buffer += data.toString();
// Process complete lines
let lineEndPos;
while ((lineEndPos = buffer.indexOf(SMTP_DEFAULTS.CRLF)) !== -1) {
// Extract a complete line
const line = buffer.substring(0, lineEndPos);
buffer = buffer.substring(lineEndPos + 2); // +2 to skip CRLF
// Process non-empty lines
if (line.length > 0) {
// In DATA state, the command handler will process the data differently
this.commandHandler(socket, line);
}
}
});
// Close event - clean up when connection is closed
socket.on('close', (hadError) => {
this.handleSocketClose(socket, hadError);
});
// Error event - handle socket errors
socket.on('error', (err) => {
this.handleSocketError(socket, err);
});
// Timeout event - handle socket timeouts
socket.on('timeout', () => {
this.handleSocketTimeout(socket);
});
}
/**
* Get the current connection count
* @returns Number of active connections
*/
public getConnectionCount(): number {
return this.activeConnections.size;
}
/**
* Check if the server has reached the maximum number of connections
* @returns True if max connections reached
*/
public hasReachedMaxConnections(): boolean {
return this.activeConnections.size >= this.options.maxConnections;
}
/**
* Close all active connections
*/
public closeAllConnections(): void {
const connectionCount = this.activeConnections.size;
if (connectionCount === 0) {
return;
}
SmtpLogger.info(`Closing all connections (count: ${connectionCount})`);
for (const socket of this.activeConnections) {
try {
// Send service closing notification
this.sendServiceClosing(socket);
// End the socket
socket.end();
} catch (error) {
SmtpLogger.error(`Error closing connection: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Clear active connections
this.activeConnections.clear();
}
/**
* Handle socket close event
* @param socket - Client socket
* @param hadError - Whether the socket was closed due to error
*/
private handleSocketClose(socket: plugins.net.Socket | plugins.tls.TLSSocket, hadError: boolean): void {
// Remove from active connections
this.activeConnections.delete(socket);
// Get the session before removing it
const session = this.sessionManager.getSession(socket);
// Remove from session manager
this.sessionManager.removeSession(socket);
// Log connection close
SmtpLogger.logConnection(socket, 'close', session);
}
/**
* Handle socket error event
* @param socket - Client socket
* @param error - Error object
*/
private handleSocketError(socket: plugins.net.Socket | plugins.tls.TLSSocket, error: Error): void {
// Get the session
const session = this.sessionManager.getSession(socket);
// Log the error
SmtpLogger.logConnection(socket, 'error', session, error);
// Close the socket if not already closed
if (!socket.destroyed) {
socket.destroy();
}
}
/**
* Handle socket timeout event
* @param socket - Client socket
*/
private handleSocketTimeout(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session
const session = this.sessionManager.getSession(socket);
if (session) {
// Log the timeout
SmtpLogger.warn(`Socket timeout from ${session.remoteAddress}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress,
state: session.state,
timeout: this.options.socketTimeout
});
// Send timeout notification
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} Connection timeout - closing connection`);
} else {
// Log timeout without session context
const socketDetails = getSocketDetails(socket);
SmtpLogger.warn(`Socket timeout without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`);
}
// Close the socket
try {
socket.end();
} catch (error) {
SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Reject a connection
* @param socket - Client socket
* @param reason - Reason for rejection
*/
private rejectConnection(socket: plugins.net.Socket | plugins.tls.TLSSocket, reason: string): void {
// Log the rejection
const socketDetails = getSocketDetails(socket);
SmtpLogger.warn(`Connection rejected from ${socketDetails.remoteAddress}:${socketDetails.remotePort}: ${reason}`);
// Send rejection message
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_NOT_AVAILABLE} ${this.options.hostname} Service temporarily unavailable - ${reason}`);
// Close the socket
try {
socket.end();
} catch (error) {
SmtpLogger.error(`Error ending rejected socket: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Send greeting message
* @param socket - Client socket
*/
private sendGreeting(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const greeting = `${SmtpResponseCode.SERVICE_READY} ${this.options.hostname} ESMTP service ready`;
this.sendResponse(socket, greeting);
}
/**
* Send service closing notification
* @param socket - Client socket
*/
private sendServiceClosing(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const message = `${SmtpResponseCode.SERVICE_CLOSING} ${this.options.hostname} Service closing transmission channel`;
this.sendResponse(socket, message);
}
/**
* Send response to client
* @param socket - Client socket
* @param response - Response to send
*/
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
SmtpLogger.logResponse(response, socket);
} catch (error) {
// Log error and destroy socket
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
response,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: error instanceof Error ? error : new Error(String(error))
});
socket.destroy();
}
}
}

View File

@ -0,0 +1,176 @@
/**
* SMTP Server Constants
* This file contains all constants and enums used by the SMTP server
*/
import { SmtpState } from '../interfaces.js';
// Re-export SmtpState enum from the main interfaces file
export { SmtpState };
/**
* SMTP Response Codes
* Based on RFC 5321 and common SMTP practice
*/
export enum SmtpResponseCode {
// Success codes (2xx)
SUCCESS = 250, // Requested mail action okay, completed
SYSTEM_STATUS = 211, // System status, or system help reply
HELP_MESSAGE = 214, // Help message
SERVICE_READY = 220, // <domain> Service ready
SERVICE_CLOSING = 221, // <domain> Service closing transmission channel
AUTHENTICATION_SUCCESSFUL = 235, // Authentication successful
OK = 250, // Requested mail action okay, completed
FORWARD = 251, // User not local; will forward to <forward-path>
CANNOT_VRFY = 252, // Cannot VRFY user, but will accept message and attempt delivery
// Intermediate codes (3xx)
MORE_INFO_NEEDED = 334, // Server challenge for authentication
START_MAIL_INPUT = 354, // Start mail input; end with <CRLF>.<CRLF>
// Temporary error codes (4xx)
SERVICE_NOT_AVAILABLE = 421, // <domain> Service not available, closing transmission channel
MAILBOX_TEMPORARILY_UNAVAILABLE = 450, // Requested mail action not taken: mailbox unavailable
LOCAL_ERROR = 451, // Requested action aborted: local error in processing
INSUFFICIENT_STORAGE = 452, // Requested action not taken: insufficient system storage
TLS_UNAVAILABLE_TEMP = 454, // TLS not available due to temporary reason
// Permanent error codes (5xx)
SYNTAX_ERROR = 500, // Syntax error, command unrecognized
SYNTAX_ERROR_PARAMETERS = 501, // Syntax error in parameters or arguments
COMMAND_NOT_IMPLEMENTED = 502, // Command not implemented
BAD_SEQUENCE = 503, // Bad sequence of commands
COMMAND_PARAMETER_NOT_IMPLEMENTED = 504, // Command parameter not implemented
AUTH_REQUIRED = 530, // Authentication required
AUTH_FAILED = 535, // Authentication credentials invalid
MAILBOX_UNAVAILABLE = 550, // Requested action not taken: mailbox unavailable
USER_NOT_LOCAL = 551, // User not local; please try <forward-path>
EXCEEDED_STORAGE = 552, // Requested mail action aborted: exceeded storage allocation
MAILBOX_NAME_INVALID = 553, // Requested action not taken: mailbox name not allowed
TRANSACTION_FAILED = 554, // Transaction failed
MAIL_RCPT_PARAMETERS_INVALID = 555, // MAIL FROM/RCPT TO parameters not recognized or not implemented
}
/**
* SMTP Command Types
*/
export enum SmtpCommand {
HELO = 'HELO',
EHLO = 'EHLO',
MAIL_FROM = 'MAIL',
RCPT_TO = 'RCPT',
DATA = 'DATA',
RSET = 'RSET',
NOOP = 'NOOP',
QUIT = 'QUIT',
STARTTLS = 'STARTTLS',
AUTH = 'AUTH',
HELP = 'HELP',
VRFY = 'VRFY',
EXPN = 'EXPN',
}
/**
* Security log event types
*/
export enum SecurityEventType {
CONNECTION = 'connection',
AUTHENTICATION = 'authentication',
COMMAND = 'command',
DATA = 'data',
IP_REPUTATION = 'ip_reputation',
TLS_NEGOTIATION = 'tls_negotiation',
DKIM = 'dkim',
SPF = 'spf',
DMARC = 'dmarc',
EMAIL_VALIDATION = 'email_validation',
SPAM = 'spam',
ACCESS_CONTROL = 'access_control',
}
/**
* Security log levels
*/
export enum SecurityLogLevel {
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}
/**
* SMTP Server Defaults
*/
export const SMTP_DEFAULTS = {
// Default timeouts in milliseconds
CONNECTION_TIMEOUT: 30000, // 30 seconds
SOCKET_TIMEOUT: 300000, // 5 minutes
DATA_TIMEOUT: 60000, // 1 minute
CLEANUP_INTERVAL: 5000, // 5 seconds
// Default limits
MAX_CONNECTIONS: 100,
MAX_RECIPIENTS: 100,
MAX_MESSAGE_SIZE: 10485760, // 10MB
// Default ports
SMTP_PORT: 25,
SUBMISSION_PORT: 587,
SECURE_PORT: 465,
// Default hostname
HOSTNAME: 'mail.lossless.one',
// CRLF line ending required by SMTP protocol
CRLF: '\r\n',
};
/**
* SMTP Command Patterns
* Regular expressions for parsing SMTP commands
*/
export const SMTP_PATTERNS = {
// Match EHLO/HELO command: "EHLO example.com"
EHLO: /^(?:EHLO|HELO)\s+(.+)$/i,
// Match MAIL FROM command: "MAIL FROM:<user@example.com> [PARAM=VALUE]"
MAIL_FROM: /^MAIL\s+FROM:<([^>]*)>((?:\s+\w+(?:=\w+)?)*)$/i,
// Match RCPT TO command: "RCPT TO:<user@example.com> [PARAM=VALUE]"
RCPT_TO: /^RCPT\s+TO:<([^>]*)>((?:\s+\w+(?:=\w+)?)*)$/i,
// Match parameter format: "PARAM=VALUE"
PARAM: /\s+([A-Za-z0-9][A-Za-z0-9\-]*)(?:=([^\s]+))?/g,
// Match email address format
EMAIL: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
// Match end of DATA marker: \r\n.\r\n
END_DATA: /\r\n\.\r\n$/,
};
/**
* SMTP Extension List
* These extensions are advertised in the EHLO response
*/
export const SMTP_EXTENSIONS = {
// Basic extensions (RFC 1869)
PIPELINING: 'PIPELINING',
SIZE: 'SIZE',
EIGHTBITMIME: '8BITMIME',
// Security extensions
STARTTLS: 'STARTTLS',
AUTH: 'AUTH',
// Additional extensions
ENHANCEDSTATUSCODES: 'ENHANCEDSTATUSCODES',
HELP: 'HELP',
CHUNKING: 'CHUNKING',
DSN: 'DSN',
// Format an extension with a parameter
formatExtension(name: string, parameter?: string | number): string {
return parameter !== undefined ? `${name} ${parameter}` : name;
}
};

View File

@ -0,0 +1,386 @@
/**
* SMTP Data Handler
* Responsible for processing email data during and after DATA command
*/
import * as plugins from '../../../plugins.js';
import * as fs from 'fs';
import * as path from 'path';
import { SmtpState, ISmtpSession, ISmtpTransactionResult } from '../interfaces.js';
import { IDataHandler, ISessionManager } from './interfaces.js';
import { SmtpResponseCode, SMTP_PATTERNS, SMTP_DEFAULTS } from './constants.js';
import { SmtpLogger } from './utils/logging.js';
import { Email } from '../../core/classes.email.js';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
/**
* Handles SMTP DATA command and email data processing
*/
export class DataHandler implements IDataHandler {
/**
* Session manager instance
*/
private sessionManager: ISessionManager;
/**
* Email server reference
*/
private emailServer: UnifiedEmailServer;
/**
* SMTP server options
*/
private options: {
size: number;
tempDir?: string;
};
/**
* Creates a new data handler
* @param sessionManager - Session manager instance
* @param emailServer - Email server reference
* @param options - Data handler options
*/
constructor(
sessionManager: ISessionManager,
emailServer: UnifiedEmailServer,
options: {
size?: number;
tempDir?: string;
} = {}
) {
this.sessionManager = sessionManager;
this.emailServer = emailServer;
this.options = {
size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE,
tempDir: options.tempDir
};
// Create temp directory if specified and doesn't exist
if (this.options.tempDir) {
try {
if (!fs.existsSync(this.options.tempDir)) {
fs.mkdirSync(this.options.tempDir, { recursive: true });
}
} catch (error) {
SmtpLogger.error(`Failed to create temp directory: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error)),
tempDir: this.options.tempDir
});
this.options.tempDir = undefined;
}
}
}
/**
* Process incoming email data
* @param socket - Client socket
* @param data - Data chunk
* @returns Promise that resolves when the data is processed
*/
public async processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void> {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Clear any existing timeout and set a new one
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
}
session.dataTimeoutId = setTimeout(() => {
if (session.state === SmtpState.DATA_RECEIVING) {
SmtpLogger.warn(`DATA timeout for session ${session.id}`, { sessionId: session.id });
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Data timeout`);
this.resetSession(session);
}
}, SMTP_DEFAULTS.DATA_TIMEOUT);
// Update activity timestamp
this.sessionManager.updateSessionActivity(session);
// Store data in chunks for better memory efficiency
if (!session.emailDataChunks) {
session.emailDataChunks = [];
}
session.emailDataChunks.push(data);
// Check if we've reached the max size
let totalSize = 0;
for (const chunk of session.emailDataChunks) {
totalSize += chunk.length;
}
if (totalSize > this.options.size) {
SmtpLogger.warn(`Message size exceeds limit for session ${session.id}`, {
sessionId: session.id,
size: totalSize,
limit: this.options.size
});
this.sendResponse(socket, `${SmtpResponseCode.EXCEEDED_STORAGE} Message too big, size limit is ${this.options.size} bytes`);
this.resetSession(session);
return;
}
// Check for end of data marker
const lastChunk = session.emailDataChunks[session.emailDataChunks.length - 1] || '';
if (SMTP_PATTERNS.END_DATA.test(lastChunk)) {
// End of data marker found
await this.handleEndOfData(socket, session);
}
}
/**
* Process a complete email
* @param session - SMTP session
* @returns Promise that resolves with the result of the transaction
*/
public async processEmail(session: ISmtpSession): Promise<ISmtpTransactionResult> {
// Combine all chunks and remove end of data marker
session.emailData = (session.emailDataChunks || []).join('');
// Remove trailing end-of-data marker: \r\n.\r\n
session.emailData = session.emailData.replace(/\r\n\.\r\n$/, '');
// Remove dot-stuffing (RFC 5321, section 4.5.2)
session.emailData = session.emailData.replace(/\r\n\.\./g, '\r\n.');
try {
// Parse email into Email object
const email = await this.parseEmail(session);
// Process the email based on the processing mode
const processingMode = session.processingMode || 'mta';
let result: ISmtpTransactionResult = {
success: false,
error: 'Email processing failed'
};
switch (processingMode) {
case 'mta':
// Process through the MTA system
try {
SmtpLogger.debug(`Processing email in MTA mode for session ${session.id}`, {
sessionId: session.id,
messageId: email.getMessageId()
});
// Queue the email for further processing by the email server
const messageId = await this.emailServer.queueEmail(email);
result = {
success: true,
messageId,
email
};
} catch (error) {
SmtpLogger.error(`Failed to queue email: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
error: error instanceof Error ? error : new Error(String(error))
});
result = {
success: false,
error: `Failed to queue email: ${error instanceof Error ? error.message : String(error)}`
};
}
break;
case 'forward':
// Forward email to another server
SmtpLogger.debug(`Processing email in FORWARD mode for session ${session.id}`, {
sessionId: session.id,
messageId: email.getMessageId()
});
// Forward logic would be implemented here
result = {
success: true,
messageId: email.getMessageId(),
email
};
break;
case 'process':
// Process the email immediately
SmtpLogger.debug(`Processing email in PROCESS mode for session ${session.id}`, {
sessionId: session.id,
messageId: email.getMessageId()
});
// Direct processing logic would be implemented here
result = {
success: true,
messageId: email.getMessageId(),
email
};
break;
default:
SmtpLogger.warn(`Unknown processing mode: ${processingMode}`, { sessionId: session.id });
result = {
success: false,
error: `Unknown processing mode: ${processingMode}`
};
}
return result;
} catch (error) {
SmtpLogger.error(`Failed to parse email: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
error: error instanceof Error ? error : new Error(String(error))
});
return {
success: false,
error: `Failed to parse email: ${error instanceof Error ? error.message : String(error)}`
};
}
}
/**
* Save an email to disk
* @param session - SMTP session
*/
public saveEmail(session: ISmtpSession): void {
if (!this.options.tempDir) {
return;
}
try {
const timestamp = Date.now();
const filename = `${session.id}-${timestamp}.eml`;
const filePath = path.join(this.options.tempDir, filename);
fs.writeFileSync(filePath, session.emailData);
SmtpLogger.debug(`Saved email to disk: ${filePath}`, {
sessionId: session.id,
filePath
});
} catch (error) {
SmtpLogger.error(`Failed to save email to disk: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
error: error instanceof Error ? error : new Error(String(error))
});
}
}
/**
* Parse an email into an Email object
* @param session - SMTP session
* @returns Promise that resolves with the parsed Email object
*/
public async parseEmail(session: ISmtpSession): Promise<Email> {
// Create a new Email object
const email = new Email();
// Set envelope information from SMTP session
email.setFrom(session.envelope.mailFrom.address);
for (const recipient of session.envelope.rcptTo) {
email.addTo(recipient.address);
}
// Parse the raw email data
await email.parseFromRaw(session.emailData);
return email;
}
/**
* Handle end of data marker received
* @param socket - Client socket
* @param session - SMTP session
*/
private async handleEndOfData(socket: plugins.net.Socket | plugins.tls.TLSSocket, session: ISmtpSession): Promise<void> {
// Clear the data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
try {
// Update session state
this.sessionManager.updateSessionState(session, SmtpState.FINISHED);
// Optionally save email to disk
this.saveEmail(session);
// Process the email
const result = await this.processEmail(session);
if (result.success) {
// Send success response
this.sendResponse(socket, `${SmtpResponseCode.OK} OK message queued as ${result.messageId}`);
} else {
// Send error response
this.sendResponse(socket, `${SmtpResponseCode.TRANSACTION_FAILED} Failed to process email: ${result.error}`);
}
// Reset session for new transaction
this.resetSession(session);
} catch (error) {
SmtpLogger.error(`Error processing email: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
error: error instanceof Error ? error : new Error(String(error))
});
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Error processing email: ${error instanceof Error ? error.message : String(error)}`);
this.resetSession(session);
}
}
/**
* Reset session after email processing
* @param session - SMTP session
*/
private resetSession(session: ISmtpSession): void {
// Clear any data timeout
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
// Reset data fields but keep authentication state
session.mailFrom = '';
session.rcptTo = [];
session.emailData = '';
session.emailDataChunks = [];
session.envelope = {
mailFrom: { address: '', args: {} },
rcptTo: []
};
// Reset state to after EHLO
this.sessionManager.updateSessionState(session, SmtpState.AFTER_EHLO);
}
/**
* Send a response to the client
* @param socket - Client socket
* @param response - Response message
*/
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
SmtpLogger.logResponse(response, socket);
} catch (error) {
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
response,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: error instanceof Error ? error : new Error(String(error))
});
socket.destroy();
}
}
}

View File

@ -0,0 +1,348 @@
/**
* SMTP Server Module Interfaces
* This file contains all interfaces for the refactored SMTP server implementation
*/
import * as plugins from '../../../plugins.js';
import type { Email } from '../../core/classes.email.js';
import type { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
import { SmtpState, EmailProcessingMode, IEnvelopeRecipient, ISmtpEnvelope, ISmtpSession, ISmtpAuth, ISmtpServerOptions, ISmtpTransactionResult } from '../interfaces.js';
// Re-export the basic interfaces from the main interfaces file
export {
SmtpState,
EmailProcessingMode,
IEnvelopeRecipient,
ISmtpEnvelope,
ISmtpSession,
ISmtpAuth,
ISmtpServerOptions,
ISmtpTransactionResult
};
/**
* Interface for SMTP session events
* These events are emitted by the session manager
*/
export interface ISessionEvents {
created: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void;
stateChanged: (session: ISmtpSession, previousState: SmtpState, newState: SmtpState) => void;
timeout: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void;
completed: (session: ISmtpSession, socket: plugins.net.Socket | plugins.tls.TLSSocket) => void;
error: (session: ISmtpSession, error: Error) => void;
}
/**
* Interface for the session manager component
*/
export interface ISessionManager {
/**
* Creates a new session for a socket connection
*/
createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession;
/**
* Updates the session state
*/
updateSessionState(session: ISmtpSession, newState: SmtpState): void;
/**
* Updates the session's last activity timestamp
*/
updateSessionActivity(session: ISmtpSession): void;
/**
* Removes a session
*/
removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Gets a session for a socket
*/
getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined;
/**
* Cleans up idle sessions
*/
cleanupIdleSessions(): void;
/**
* Gets the current number of active sessions
*/
getSessionCount(): number;
/**
* Clears all sessions (used when shutting down)
*/
clearAllSessions(): void;
/**
* Register an event listener
*/
on<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void;
/**
* Remove an event listener
*/
off<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void;
}
/**
* Interface for the connection manager component
*/
export interface IConnectionManager {
/**
* Handle a new connection
*/
handleNewConnection(socket: plugins.net.Socket): void;
/**
* Handle a new secure TLS connection
*/
handleNewSecureConnection(socket: plugins.tls.TLSSocket): void;
/**
* Set up event handlers for a socket
*/
setupSocketEventHandlers(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Get the current connection count
*/
getConnectionCount(): number;
/**
* Check if the server has reached the maximum number of connections
*/
hasReachedMaxConnections(): boolean;
/**
* Close all active connections
*/
closeAllConnections(): void;
}
/**
* Interface for the command handler component
*/
export interface ICommandHandler {
/**
* Process a command from the client
*/
processCommand(socket: plugins.net.Socket | plugins.tls.TLSSocket, commandLine: string): void;
/**
* Send a response to the client
*/
sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void;
/**
* Handle EHLO command
*/
handleEhlo(socket: plugins.net.Socket | plugins.tls.TLSSocket, clientHostname: string): void;
/**
* Handle MAIL FROM command
*/
handleMailFrom(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void;
/**
* Handle RCPT TO command
*/
handleRcptTo(socket: plugins.net.Socket | plugins.tls.TLSSocket, args: string): void;
/**
* Handle DATA command
*/
handleData(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Handle RSET command
*/
handleRset(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Handle NOOP command
*/
handleNoop(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Handle QUIT command
*/
handleQuit(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
}
/**
* Interface for the data handler component
*/
export interface IDataHandler {
/**
* Process incoming email data
*/
processEmailData(socket: plugins.net.Socket | plugins.tls.TLSSocket, data: string): Promise<void>;
/**
* Process a complete email
*/
processEmail(session: ISmtpSession): Promise<ISmtpTransactionResult>;
/**
* Save an email to disk
*/
saveEmail(session: ISmtpSession): void;
/**
* Parse an email into an Email object
*/
parseEmail(session: ISmtpSession): Promise<Email>;
}
/**
* Interface for the TLS handler component
*/
export interface ITlsHandler {
/**
* Handle STARTTLS command
*/
handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void;
/**
* Upgrade a connection to TLS
*/
startTLS(socket: plugins.net.Socket): void;
/**
* Create a secure server
*/
createSecureServer(): plugins.tls.Server | undefined;
/**
* Check if TLS is enabled
*/
isTlsEnabled(): boolean;
}
/**
* Interface for the security handler component
*/
export interface ISecurityHandler {
/**
* Check IP reputation for a connection
*/
checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise<boolean>;
/**
* Validate an email address
*/
isValidEmail(email: string): boolean;
/**
* Validate authentication credentials
*/
authenticate(session: ISmtpSession, username: string, password: string, method: string): Promise<boolean>;
/**
* Log a security event
*/
logSecurityEvent(event: string, level: string, details: Record<string, any>): void;
}
/**
* Interface for the SMTP server component
*/
export interface ISmtpServer {
/**
* Start the SMTP server
*/
listen(): Promise<void>;
/**
* Stop the SMTP server
*/
close(): Promise<void>;
/**
* Get the session manager
*/
getSessionManager(): ISessionManager;
/**
* Get the connection manager
*/
getConnectionManager(): IConnectionManager;
/**
* Get the command handler
*/
getCommandHandler(): ICommandHandler;
/**
* Get the data handler
*/
getDataHandler(): IDataHandler;
/**
* Get the TLS handler
*/
getTlsHandler(): ITlsHandler;
/**
* Get the security handler
*/
getSecurityHandler(): ISecurityHandler;
/**
* Get the server options
*/
getOptions(): ISmtpServerOptions;
/**
* Get the email server reference
*/
getEmailServer(): UnifiedEmailServer;
}
/**
* Configuration for creating an SMTP server
*/
export interface ISmtpServerConfig {
/**
* Email server reference
*/
emailServer: UnifiedEmailServer;
/**
* SMTP server options
*/
options: ISmtpServerOptions;
/**
* Optional session manager
*/
sessionManager?: ISessionManager;
/**
* Optional connection manager
*/
connectionManager?: IConnectionManager;
/**
* Optional command handler
*/
commandHandler?: ICommandHandler;
/**
* Optional data handler
*/
dataHandler?: IDataHandler;
/**
* Optional TLS handler
*/
tlsHandler?: ITlsHandler;
/**
* Optional security handler
*/
securityHandler?: ISecurityHandler;
}

View File

@ -0,0 +1,342 @@
/**
* SMTP Security Handler
* Responsible for security aspects including IP reputation checking,
* email validation, and authentication
*/
import * as plugins from '../../../plugins.js';
import { ISmtpSession, ISmtpAuth } from '../interfaces.js';
import { ISecurityHandler } from './interfaces.js';
import { SmtpLogger } from './utils/logging.js';
import { SecurityEventType, SecurityLogLevel } from './constants.js';
import { isValidEmail } from './utils/validation.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
/**
* Interface for IP denylist entry
*/
interface IIpDenylistEntry {
ip: string;
reason: string;
expiresAt?: number;
}
/**
* Handles security aspects for SMTP server
*/
export class SecurityHandler implements ISecurityHandler {
/**
* Email server reference
*/
private emailServer: UnifiedEmailServer;
/**
* IP reputation service
*/
private ipReputationService?: any;
/**
* Authentication options
*/
private authOptions?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
validateUser?: (username: string, password: string) => Promise<boolean>;
};
/**
* Simple in-memory IP denylist
*/
private ipDenylist: IIpDenylistEntry[] = [];
/**
* Creates a new security handler
* @param emailServer - Email server reference
* @param ipReputationService - Optional IP reputation service
* @param authOptions - Authentication options
*/
constructor(
emailServer: UnifiedEmailServer,
ipReputationService?: any,
authOptions?: {
required: boolean;
methods: ('PLAIN' | 'LOGIN' | 'OAUTH2')[];
validateUser?: (username: string, password: string) => Promise<boolean>;
}
) {
this.emailServer = emailServer;
this.ipReputationService = ipReputationService;
this.authOptions = authOptions;
// Clean expired denylist entries periodically
setInterval(() => this.cleanExpiredDenylistEntries(), 60000); // Every minute
}
/**
* Check IP reputation for a connection
* @param socket - Client socket
* @returns Promise that resolves to true if IP is allowed, false if blocked
*/
public async checkIpReputation(socket: plugins.net.Socket | plugins.tls.TLSSocket): Promise<boolean> {
const socketDetails = getSocketDetails(socket);
const ip = socketDetails.remoteAddress;
// Check local denylist first
if (this.isIpDenylisted(ip)) {
// Log the blocked connection
this.logSecurityEvent(
SecurityEventType.IP_REPUTATION,
SecurityLogLevel.WARN,
`Connection blocked from denylisted IP: ${ip}`,
{ reason: this.getDenylistReason(ip) }
);
return false;
}
// If no reputation service, allow by default
if (!this.ipReputationService) {
return true;
}
try {
// Check with IP reputation service
const reputationResult = await this.ipReputationService.checkIp(ip);
if (!reputationResult.allowed) {
// Add to local denylist temporarily
this.addToDenylist(ip, reputationResult.reason, 3600000); // 1 hour
// Log the blocked connection
this.logSecurityEvent(
SecurityEventType.IP_REPUTATION,
SecurityLogLevel.WARN,
`Connection blocked by reputation service: ${ip}`,
{
reason: reputationResult.reason,
score: reputationResult.score,
categories: reputationResult.categories
}
);
return false;
}
// Log the allowed connection
this.logSecurityEvent(
SecurityEventType.IP_REPUTATION,
SecurityLogLevel.INFO,
`IP reputation check passed: ${ip}`,
{
score: reputationResult.score,
categories: reputationResult.categories
}
);
return true;
} catch (error) {
// Log the error
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
ip,
error: error instanceof Error ? error : new Error(String(error))
});
// Allow the connection on error (fail open)
return true;
}
}
/**
* Validate an email address
* @param email - Email address to validate
* @returns Whether the email address is valid
*/
public isValidEmail(email: string): boolean {
return isValidEmail(email);
}
/**
* Validate authentication credentials
* @param session - SMTP session
* @param username - Username
* @param password - Password
* @param method - Authentication method
* @returns Promise that resolves to true if authenticated
*/
public async authenticate(session: ISmtpSession, username: string, password: string, method: string): Promise<boolean> {
// Check if authentication is enabled
if (!this.authOptions) {
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN,
'Authentication attempt when auth is disabled',
{ username, method, sessionId: session.id, ip: session.remoteAddress }
);
return false;
}
// Check if method is supported
if (!this.authOptions.methods.includes(method as any)) {
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN,
`Unsupported authentication method: ${method}`,
{ username, method, sessionId: session.id, ip: session.remoteAddress }
);
return false;
}
// Check if TLS is active (should be required for auth)
if (!session.useTLS) {
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
SecurityLogLevel.WARN,
'Authentication attempt without TLS',
{ username, method, sessionId: session.id, ip: session.remoteAddress }
);
return false;
}
try {
let authenticated = false;
// Use custom validation function if provided
if (this.authOptions.validateUser) {
authenticated = await this.authOptions.validateUser(username, password);
} else {
// Default behavior - no authentication
authenticated = false;
}
// Log the authentication result
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
authenticated ? SecurityLogLevel.INFO : SecurityLogLevel.WARN,
authenticated ? 'Authentication successful' : 'Authentication failed',
{ username, method, sessionId: session.id, ip: session.remoteAddress }
);
return authenticated;
} catch (error) {
// Log authentication error
this.logSecurityEvent(
SecurityEventType.AUTHENTICATION,
SecurityLogLevel.ERROR,
`Authentication error: ${error instanceof Error ? error.message : String(error)}`,
{ username, method, sessionId: session.id, ip: session.remoteAddress, error: error instanceof Error ? error.message : String(error) }
);
return false;
}
}
/**
* Log a security event
* @param event - Event type
* @param level - Log level
* @param details - Event details
*/
public logSecurityEvent(event: string, level: string, message: string, details: Record<string, any>): void {
SmtpLogger.logSecurityEvent(
level as SecurityLogLevel,
event as SecurityEventType,
message,
details,
details.ip,
details.domain,
details.success
);
}
/**
* Add an IP to the denylist
* @param ip - IP address
* @param reason - Reason for denylisting
* @param duration - Duration in milliseconds (optional, indefinite if not specified)
*/
private addToDenylist(ip: string, reason: string, duration?: number): void {
// Remove existing entry if present
this.ipDenylist = this.ipDenylist.filter(entry => entry.ip !== ip);
// Create new entry
const entry: IIpDenylistEntry = {
ip,
reason,
expiresAt: duration ? Date.now() + duration : undefined
};
// Add to denylist
this.ipDenylist.push(entry);
// Log the action
this.logSecurityEvent(
SecurityEventType.ACCESS_CONTROL,
SecurityLogLevel.INFO,
`Added IP to denylist: ${ip}`,
{
ip,
reason,
duration: duration ? `${duration / 1000} seconds` : 'indefinite'
}
);
}
/**
* Check if an IP is denylisted
* @param ip - IP address
* @returns Whether the IP is denylisted
*/
private isIpDenylisted(ip: string): boolean {
const entry = this.ipDenylist.find(e => e.ip === ip);
if (!entry) {
return false;
}
// Check if entry has expired
if (entry.expiresAt && entry.expiresAt < Date.now()) {
// Remove expired entry
this.ipDenylist = this.ipDenylist.filter(e => e !== entry);
return false;
}
return true;
}
/**
* Get the reason an IP was denylisted
* @param ip - IP address
* @returns Reason for denylisting or undefined if not denylisted
*/
private getDenylistReason(ip: string): string | undefined {
const entry = this.ipDenylist.find(e => e.ip === ip);
return entry?.reason;
}
/**
* Clean expired denylist entries
*/
private cleanExpiredDenylistEntries(): void {
const now = Date.now();
const initialCount = this.ipDenylist.length;
this.ipDenylist = this.ipDenylist.filter(entry => {
return !entry.expiresAt || entry.expiresAt > now;
});
const removedCount = initialCount - this.ipDenylist.length;
if (removedCount > 0) {
this.logSecurityEvent(
SecurityEventType.ACCESS_CONTROL,
SecurityLogLevel.INFO,
`Cleaned up ${removedCount} expired denylist entries`,
{ remainingCount: this.ipDenylist.length }
);
}
}
}

View File

@ -0,0 +1,392 @@
/**
* SMTP Session Manager
* Responsible for creating, managing, and cleaning up SMTP sessions
*/
import * as plugins from '../../../plugins.js';
import { SmtpState, ISmtpSession, ISmtpEnvelope } from '../interfaces.js';
import { ISessionManager, ISessionEvents } from './interfaces.js';
import { SMTP_DEFAULTS } from './constants.js';
import { generateSessionId, getSocketDetails } from './utils/helpers.js';
import { SmtpLogger } from './utils/logging.js';
/**
* Manager for SMTP sessions
* Handles session creation, tracking, timeout management, and cleanup
*/
export class SessionManager implements ISessionManager {
/**
* Map of socket ID to session
*/
private sessions: Map<string, ISmtpSession> = new Map();
/**
* Map of socket to socket ID
*/
private socketIds: Map<plugins.net.Socket | plugins.tls.TLSSocket, string> = new Map();
/**
* SMTP server options
*/
private options: {
socketTimeout: number;
connectionTimeout: number;
cleanupInterval: number;
};
/**
* Event listeners
*/
private eventListeners: {
[K in keyof ISessionEvents]?: Set<ISessionEvents[K]>;
} = {};
/**
* Timer for cleanup interval
*/
private cleanupTimer: NodeJS.Timeout | null = null;
/**
* Creates a new session manager
* @param options - Session manager options
*/
constructor(options: {
socketTimeout?: number;
connectionTimeout?: number;
cleanupInterval?: number;
} = {}) {
this.options = {
socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT,
connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT,
cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL
};
// Start the cleanup timer
this.startCleanupTimer();
}
/**
* Creates a new session for a socket connection
* @param socket - Client socket
* @param secure - Whether the connection is secure (TLS)
* @returns New SMTP session
*/
public createSession(socket: plugins.net.Socket | plugins.tls.TLSSocket, secure: boolean): ISmtpSession {
const sessionId = generateSessionId();
const socketDetails = getSocketDetails(socket);
// Create a new session
const session: ISmtpSession = {
id: sessionId,
state: SmtpState.GREETING,
clientHostname: '',
mailFrom: '',
rcptTo: [],
emailData: '',
emailDataChunks: [],
useTLS: secure || false,
connectionEnded: false,
remoteAddress: socketDetails.remoteAddress,
secure: secure || false,
authenticated: false,
envelope: {
mailFrom: { address: '', args: {} },
rcptTo: []
},
lastActivity: Date.now()
};
// Store session with unique ID
const socketKey = this.getSocketKey(socket);
this.socketIds.set(socket, socketKey);
this.sessions.set(socketKey, session);
// Set socket timeout
socket.setTimeout(this.options.socketTimeout);
// Emit session created event
this.emitEvent('created', session, socket);
// Log session creation
SmtpLogger.info(`Created SMTP session ${sessionId}`, {
sessionId,
remoteAddress: session.remoteAddress,
remotePort: socketDetails.remotePort,
secure: session.secure
});
return session;
}
/**
* Updates the session state
* @param session - SMTP session
* @param newState - New state
*/
public updateSessionState(session: ISmtpSession, newState: SmtpState): void {
if (session.state === newState) {
return;
}
const previousState = session.state;
session.state = newState;
// Update activity timestamp
this.updateSessionActivity(session);
// Emit state changed event
this.emitEvent('stateChanged', session, previousState, newState);
// Log state change
SmtpLogger.debug(`Session ${session.id} state changed from ${previousState} to ${newState}`, {
sessionId: session.id,
previousState,
newState,
remoteAddress: session.remoteAddress
});
}
/**
* Updates the session's last activity timestamp
* @param session - SMTP session
*/
public updateSessionActivity(session: ISmtpSession): void {
session.lastActivity = Date.now();
}
/**
* Removes a session
* @param socket - Client socket
*/
public removeSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const socketKey = this.socketIds.get(socket);
if (!socketKey) {
return;
}
const session = this.sessions.get(socketKey);
if (session) {
// Mark the session as ended
session.connectionEnded = true;
// Clear any data timeout if it exists
if (session.dataTimeoutId) {
clearTimeout(session.dataTimeoutId);
session.dataTimeoutId = undefined;
}
// Emit session completed event
this.emitEvent('completed', session, socket);
// Log session removal
SmtpLogger.info(`Removed SMTP session ${session.id}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress,
finalState: session.state
});
}
// Remove from maps
this.sessions.delete(socketKey);
this.socketIds.delete(socket);
}
/**
* Gets a session for a socket
* @param socket - Client socket
* @returns SMTP session or undefined if not found
*/
public getSession(socket: plugins.net.Socket | plugins.tls.TLSSocket): ISmtpSession | undefined {
const socketKey = this.socketIds.get(socket);
if (!socketKey) {
return undefined;
}
return this.sessions.get(socketKey);
}
/**
* Cleans up idle sessions
*/
public cleanupIdleSessions(): void {
const now = Date.now();
let timedOutCount = 0;
for (const [socketKey, session] of this.sessions.entries()) {
if (session.connectionEnded) {
// Session already marked as ended, but still in map
this.sessions.delete(socketKey);
continue;
}
// Calculate how long the session has been idle
const lastActivity = session.lastActivity || 0;
const idleTime = now - lastActivity;
// Use appropriate timeout based on session state
const timeout = session.state === SmtpState.DATA_RECEIVING
? this.options.socketTimeout * 2 // Double timeout for data receiving
: session.state === SmtpState.GREETING
? this.options.connectionTimeout // Initial connection timeout
: this.options.socketTimeout; // Standard timeout for other states
// Check if session has timed out
if (idleTime > timeout) {
// Find the socket for this session
let timedOutSocket: plugins.net.Socket | plugins.tls.TLSSocket | undefined;
for (const [socket, key] of this.socketIds.entries()) {
if (key === socketKey) {
timedOutSocket = socket;
break;
}
}
if (timedOutSocket) {
// Emit timeout event
this.emitEvent('timeout', session, timedOutSocket);
// Log timeout
SmtpLogger.warn(`Session ${session.id} timed out after ${Math.round(idleTime / 1000)}s of inactivity`, {
sessionId: session.id,
remoteAddress: session.remoteAddress,
state: session.state,
idleTime
});
// End the socket connection
try {
timedOutSocket.end();
} catch (error) {
SmtpLogger.error(`Error ending timed out socket: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress,
error: error instanceof Error ? error : new Error(String(error))
});
}
// Remove from maps
this.sessions.delete(socketKey);
this.socketIds.delete(timedOutSocket);
timedOutCount++;
}
}
}
if (timedOutCount > 0) {
SmtpLogger.info(`Cleaned up ${timedOutCount} timed out sessions`, {
totalSessions: this.sessions.size
});
}
}
/**
* Gets the current number of active sessions
* @returns Number of active sessions
*/
public getSessionCount(): number {
return this.sessions.size;
}
/**
* Clears all sessions (used when shutting down)
*/
public clearAllSessions(): void {
// Log the action
SmtpLogger.info(`Clearing all sessions (count: ${this.sessions.size})`);
// Clear the sessions and socket IDs maps
this.sessions.clear();
this.socketIds.clear();
// Stop the cleanup timer
this.stopCleanupTimer();
}
/**
* Register an event listener
* @param event - Event name
* @param listener - Event listener function
*/
public on<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void {
if (!this.eventListeners[event]) {
this.eventListeners[event] = new Set();
}
(this.eventListeners[event] as Set<ISessionEvents[K]>).add(listener);
}
/**
* Remove an event listener
* @param event - Event name
* @param listener - Event listener function
*/
public off<K extends keyof ISessionEvents>(event: K, listener: ISessionEvents[K]): void {
if (!this.eventListeners[event]) {
return;
}
(this.eventListeners[event] as Set<ISessionEvents[K]>).delete(listener);
}
/**
* Emit an event to registered listeners
* @param event - Event name
* @param args - Event arguments
*/
private emitEvent<K extends keyof ISessionEvents>(event: K, ...args: Parameters<ISessionEvents[K]>): void {
const listeners = this.eventListeners[event] as Set<ISessionEvents[K]> | undefined;
if (!listeners) {
return;
}
for (const listener of listeners) {
try {
listener(...args);
} catch (error) {
SmtpLogger.error(`Error in session event listener for ${String(event)}: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error))
});
}
}
}
/**
* Start the cleanup timer
*/
private startCleanupTimer(): void {
if (this.cleanupTimer) {
return;
}
this.cleanupTimer = setInterval(() => {
this.cleanupIdleSessions();
}, this.options.cleanupInterval);
// Prevent the timer from keeping the process alive
if (this.cleanupTimer.unref) {
this.cleanupTimer.unref();
}
}
/**
* Stop the cleanup timer
*/
private stopCleanupTimer(): void {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = null;
}
}
/**
* Gets a unique key for a socket
* @param socket - Client socket
* @returns Socket key
*/
private getSocketKey(socket: plugins.net.Socket | plugins.tls.TLSSocket): string {
const details = getSocketDetails(socket);
return `${details.remoteAddress}:${details.remotePort}-${Date.now()}`;
}
}

View File

@ -0,0 +1,284 @@
/**
* SMTP TLS Handler
* Responsible for handling TLS-related SMTP functionality
*/
import * as plugins from '../../../plugins.js';
import { ITlsHandler, ISessionManager } from './interfaces.js';
import { SmtpResponseCode, SecurityEventType, SecurityLogLevel } from './constants.js';
import { SmtpLogger } from './utils/logging.js';
import { getSocketDetails, getTlsDetails } from './utils/helpers.js';
/**
* Handles TLS functionality for SMTP server
*/
export class TlsHandler implements ITlsHandler {
/**
* Session manager instance
*/
private sessionManager: ISessionManager;
/**
* TLS options
*/
private options: {
key: string;
cert: string;
ca?: string;
rejectUnauthorized?: boolean;
};
/**
* Creates a new TLS handler
* @param sessionManager - Session manager instance
* @param options - TLS options
*/
constructor(
sessionManager: ISessionManager,
options: {
key: string;
cert: string;
ca?: string;
rejectUnauthorized?: boolean;
}
) {
this.sessionManager = sessionManager;
this.options = options;
}
/**
* Handle STARTTLS command
* @param socket - Client socket
*/
public handleStartTls(socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
if (!session) {
this.sendResponse(socket, `${SmtpResponseCode.LOCAL_ERROR} Internal server error - session not found`);
return;
}
// Check if already using TLS
if (session.useTLS) {
this.sendResponse(socket, `${SmtpResponseCode.BAD_SEQUENCE} TLS already active`);
return;
}
// Check if we have the necessary TLS certificates
if (!this.isTlsEnabled()) {
this.sendResponse(socket, `${SmtpResponseCode.TLS_UNAVAILABLE_TEMP} TLS not available`);
return;
}
// Send ready for TLS response
this.sendResponse(socket, `${SmtpResponseCode.SERVICE_READY} Ready to start TLS`);
// Upgrade the connection to TLS
try {
this.startTLS(socket);
} catch (error) {
SmtpLogger.error(`STARTTLS negotiation failed: ${error instanceof Error ? error.message : String(error)}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress,
error: error instanceof Error ? error : new Error(String(error))
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'STARTTLS negotiation failed',
{ error: error instanceof Error ? error.message : String(error) },
session.remoteAddress
);
}
}
/**
* Upgrade a connection to TLS
* @param socket - Client socket
*/
public startTLS(socket: plugins.net.Socket): void {
// Get the session for this socket
const session = this.sessionManager.getSession(socket);
// Create TLS context
const context = {
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca,
isServer: true,
rejectUnauthorized: this.options.rejectUnauthorized || false
};
try {
// Upgrade the connection
const secureSocket = new plugins.tls.TLSSocket(socket, context);
// Store reference to the original socket to facilitate cleanup
(secureSocket as any).originalSocket = socket;
// Log the successful upgrade
if (session) {
SmtpLogger.info(`Upgraded connection to TLS for session ${session.id}`, {
sessionId: session.id,
remoteAddress: session.remoteAddress
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.INFO,
SecurityEventType.TLS_NEGOTIATION,
'STARTTLS negotiation successful',
{},
session.remoteAddress,
undefined,
true
);
// Update session properties
session.useTLS = true;
session.secure = true;
// Reset session state (per RFC 3207)
// After STARTTLS, client must issue a new EHLO
if (this.sessionManager.updateSessionState) {
this.sessionManager.updateSessionState(session, SmtpState.GREETING);
}
} else {
const socketDetails = getSocketDetails(socket);
SmtpLogger.info(`Upgraded connection to TLS without session from ${socketDetails.remoteAddress}:${socketDetails.remotePort}`);
}
// Securely handle TLS errors
secureSocket.on('error', (err) => {
SmtpLogger.error(`TLS error: ${err.message}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: err
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'TLS error after successful negotiation',
{ error: err.message },
socket.remoteAddress
);
socket.destroy();
});
// Log TLS connection details on secure
secureSocket.on('secure', () => {
const tlsDetails = getTlsDetails(secureSocket);
if (tlsDetails) {
SmtpLogger.info('TLS connection established', {
remoteAddress: secureSocket.remoteAddress,
remotePort: secureSocket.remotePort,
protocol: tlsDetails.protocol,
cipher: tlsDetails.cipher,
authorized: tlsDetails.authorized
});
// Log security event with TLS details
SmtpLogger.logSecurityEvent(
SecurityLogLevel.INFO,
SecurityEventType.TLS_NEGOTIATION,
'TLS connection details',
{
protocol: tlsDetails.protocol,
cipher: tlsDetails.cipher,
authorized: tlsDetails.authorized
},
secureSocket.remoteAddress,
undefined,
true
);
}
});
} catch (error) {
SmtpLogger.error(`Failed to upgrade connection to TLS: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: error instanceof Error ? error : new Error(String(error))
});
// Log security event
SmtpLogger.logSecurityEvent(
SecurityLogLevel.ERROR,
SecurityEventType.TLS_NEGOTIATION,
'Failed to upgrade connection to TLS',
{ error: error instanceof Error ? error.message : String(error) },
socket.remoteAddress,
undefined,
false
);
socket.destroy();
}
}
/**
* Create a secure server
* @returns TLS server instance or undefined if TLS is not enabled
*/
public createSecureServer(): plugins.tls.Server | undefined {
if (!this.isTlsEnabled()) {
return undefined;
}
try {
// Create TLS context
const context = {
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca,
rejectUnauthorized: this.options.rejectUnauthorized || false
};
// Create secure server
return new plugins.tls.Server(context);
} catch (error) {
SmtpLogger.error(`Failed to create secure server: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error))
});
return undefined;
}
}
/**
* Check if TLS is enabled
* @returns Whether TLS is enabled
*/
public isTlsEnabled(): boolean {
return !!(this.options.key && this.options.cert);
}
/**
* Send a response to the client
* @param socket - Client socket
* @param response - Response message
*/
private sendResponse(socket: plugins.net.Socket | plugins.tls.TLSSocket, response: string): void {
try {
socket.write(`${response}\r\n`);
SmtpLogger.logResponse(response, socket);
} catch (error) {
SmtpLogger.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`, {
response,
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
error: error instanceof Error ? error : new Error(String(error))
});
socket.destroy();
}
}
}
// Import SmtpState only for type reference, not available at runtime
import { SmtpState } from '../interfaces.js';

View File

@ -0,0 +1,201 @@
/**
* SMTP Helper Functions
* Provides utility functions for SMTP server implementation
*/
import * as plugins from '../../../../plugins.js';
import { SMTP_DEFAULTS } from '../constants.js';
import type { ISmtpSession, ISmtpServerOptions } from '../../interfaces.js';
/**
* Formats a multi-line SMTP response according to RFC 5321
* @param code - Response code
* @param lines - Response lines
* @returns Formatted SMTP response
*/
export function formatMultilineResponse(code: number, lines: string[]): string {
if (!lines || lines.length === 0) {
return `${code} `;
}
if (lines.length === 1) {
return `${code} ${lines[0]}`;
}
let response = '';
for (let i = 0; i < lines.length - 1; i++) {
response += `${code}-${lines[i]}${SMTP_DEFAULTS.CRLF}`;
}
response += `${code} ${lines[lines.length - 1]}`;
return response;
}
/**
* Generates a unique session ID
* @returns Unique session ID
*/
export function generateSessionId(): string {
return `${Date.now()}-${Math.floor(Math.random() * 10000)}`;
}
/**
* Safely parses an integer from string with a default value
* @param value - String value to parse
* @param defaultValue - Default value if parsing fails
* @returns Parsed integer or default value
*/
export function safeParseInt(value: string | undefined, defaultValue: number): number {
if (!value) {
return defaultValue;
}
const parsed = parseInt(value, 10);
return isNaN(parsed) ? defaultValue : parsed;
}
/**
* Safely gets the socket details
* @param socket - Socket to get details from
* @returns Socket details object
*/
export function getSocketDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): {
remoteAddress: string;
remotePort: number;
remoteFamily: string;
localAddress: string;
localPort: number;
encrypted: boolean;
} {
return {
remoteAddress: socket.remoteAddress || 'unknown',
remotePort: socket.remotePort || 0,
remoteFamily: socket.remoteFamily || 'unknown',
localAddress: socket.localAddress || 'unknown',
localPort: socket.localPort || 0,
encrypted: socket instanceof plugins.tls.TLSSocket
};
}
/**
* Gets TLS details if socket is TLS
* @param socket - Socket to get TLS details from
* @returns TLS details or undefined if not TLS
*/
export function getTlsDetails(socket: plugins.net.Socket | plugins.tls.TLSSocket): {
protocol?: string;
cipher?: string;
authorized?: boolean;
} | undefined {
if (!(socket instanceof plugins.tls.TLSSocket)) {
return undefined;
}
return {
protocol: socket.getProtocol(),
cipher: socket.getCipher()?.name,
authorized: socket.authorized
};
}
/**
* Merges default options with provided options
* @param options - User provided options
* @returns Merged options with defaults
*/
export function mergeWithDefaults(options: Partial<ISmtpServerOptions>): ISmtpServerOptions {
return {
port: options.port || SMTP_DEFAULTS.SMTP_PORT,
key: options.key || '',
cert: options.cert || '',
hostname: options.hostname || SMTP_DEFAULTS.HOSTNAME,
host: options.host,
securePort: options.securePort,
ca: options.ca,
maxSize: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE,
maxConnections: options.maxConnections || SMTP_DEFAULTS.MAX_CONNECTIONS,
socketTimeout: options.socketTimeout || SMTP_DEFAULTS.SOCKET_TIMEOUT,
connectionTimeout: options.connectionTimeout || SMTP_DEFAULTS.CONNECTION_TIMEOUT,
cleanupInterval: options.cleanupInterval || SMTP_DEFAULTS.CLEANUP_INTERVAL,
maxRecipients: options.maxRecipients || SMTP_DEFAULTS.MAX_RECIPIENTS,
size: options.size || SMTP_DEFAULTS.MAX_MESSAGE_SIZE,
dataTimeout: options.dataTimeout || SMTP_DEFAULTS.DATA_TIMEOUT,
auth: options.auth,
};
}
/**
* Creates a text response formatter for the SMTP server
* @param socket - Socket to send responses to
* @returns Function to send formatted response
*/
export function createResponseFormatter(socket: plugins.net.Socket | plugins.tls.TLSSocket): (response: string) => void {
return (response: string): void => {
try {
socket.write(`${response}${SMTP_DEFAULTS.CRLF}`);
console.log(`${response}`);
} catch (error) {
console.error(`Error sending response: ${error instanceof Error ? error.message : String(error)}`);
socket.destroy();
}
};
}
/**
* Extracts SMTP command name from a command line
* @param commandLine - Full command line
* @returns Command name in uppercase
*/
export function extractCommandName(commandLine: string): string {
const parts = commandLine.trim().split(' ');
return parts[0].toUpperCase();
}
/**
* Extracts SMTP command arguments from a command line
* @param commandLine - Full command line
* @returns Arguments string
*/
export function extractCommandArgs(commandLine: string): string {
const firstSpace = commandLine.indexOf(' ');
if (firstSpace === -1) {
return '';
}
return commandLine.substring(firstSpace + 1).trim();
}
/**
* Sanitizes data for logging (hides sensitive info)
* @param data - Data to sanitize
* @returns Sanitized data
*/
export function sanitizeForLogging(data: any): any {
if (!data) {
return data;
}
if (typeof data !== 'object') {
return data;
}
const result: any = Array.isArray(data) ? [] : {};
for (const key in data) {
if (Object.prototype.hasOwnProperty.call(data, key)) {
// Sanitize sensitive fields
if (key.toLowerCase().includes('password') ||
key.toLowerCase().includes('token') ||
key.toLowerCase().includes('secret') ||
key.toLowerCase().includes('credential')) {
result[key] = '********';
} else if (typeof data[key] === 'object' && data[key] !== null) {
result[key] = sanitizeForLogging(data[key]);
} else {
result[key] = data[key];
}
}
}
return result;
}

View File

@ -0,0 +1,246 @@
/**
* SMTP Logging Utilities
* Provides structured logging for SMTP server components
*/
import * as plugins from '../../../../plugins.js';
import { logger } from '../../../../logger.js';
import { SecurityLogLevel, SecurityEventType } from '../constants.js';
import type { ISmtpSession } from '../../interfaces.js';
/**
* SMTP connection metadata to include in logs
*/
export interface IConnectionMetadata {
remoteAddress?: string;
remotePort?: number;
socketId?: string;
secure?: boolean;
sessionId?: string;
}
/**
* Log levels for SMTP server
*/
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
/**
* Options for SMTP log
*/
export interface ISmtpLogOptions {
level?: LogLevel;
sessionId?: string;
sessionState?: string;
remoteAddress?: string;
remotePort?: number;
command?: string;
error?: Error;
[key: string]: any;
}
/**
* SMTP logger - provides structured logging for SMTP server
*/
export class SmtpLogger {
/**
* Log a message with context
* @param level - Log level
* @param message - Log message
* @param options - Additional log options
*/
public static log(level: LogLevel, message: string, options: ISmtpLogOptions = {}): void {
// Extract error information if provided
const errorInfo = options.error ? {
errorMessage: options.error.message,
errorStack: options.error.stack,
errorName: options.error.name
} : {};
// Structure log data
const logData = {
component: 'smtp-server',
...options,
...errorInfo
};
// Remove error from log data to avoid duplication
if (logData.error) {
delete logData.error;
}
// Log through the main logger
logger.log(level, message, logData);
// Also console log for immediate visibility during development
if (level === 'error' || level === 'warn') {
console[level](`[SMTP] ${message}`, logData);
}
}
/**
* Log debug level message
* @param message - Log message
* @param options - Additional log options
*/
public static debug(message: string, options: ISmtpLogOptions = {}): void {
this.log('debug', message, options);
}
/**
* Log info level message
* @param message - Log message
* @param options - Additional log options
*/
public static info(message: string, options: ISmtpLogOptions = {}): void {
this.log('info', message, options);
}
/**
* Log warning level message
* @param message - Log message
* @param options - Additional log options
*/
public static warn(message: string, options: ISmtpLogOptions = {}): void {
this.log('warn', message, options);
}
/**
* Log error level message
* @param message - Log message
* @param options - Additional log options
*/
public static error(message: string, options: ISmtpLogOptions = {}): void {
this.log('error', message, options);
}
/**
* Log command received from client
* @param command - The command string
* @param socket - The client socket
* @param session - The SMTP session
*/
public static logCommand(command: string, socket: plugins.net.Socket | plugins.tls.TLSSocket, session?: ISmtpSession): void {
const clientInfo = {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
secure: socket instanceof plugins.tls.TLSSocket,
sessionId: session?.id,
sessionState: session?.state
};
this.info(`Command received: ${command}`, {
...clientInfo,
command: command.split(' ')[0]?.toUpperCase()
});
// Also log to console for easy debugging
console.log(`${command}`);
}
/**
* Log response sent to client
* @param response - The response string
* @param socket - The client socket
*/
public static logResponse(response: string, socket: plugins.net.Socket | plugins.tls.TLSSocket): void {
const clientInfo = {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
secure: socket instanceof plugins.tls.TLSSocket
};
// Get the response code from the beginning of the response
const responseCode = response.substring(0, 3);
// Log different levels based on response code
if (responseCode.startsWith('2') || responseCode.startsWith('3')) {
this.debug(`Response sent: ${response}`, clientInfo);
} else if (responseCode.startsWith('4')) {
this.warn(`Temporary error response: ${response}`, clientInfo);
} else if (responseCode.startsWith('5')) {
this.error(`Permanent error response: ${response}`, clientInfo);
}
// Also log to console for easy debugging
console.log(`${response}`);
}
/**
* Log client connection event
* @param socket - The client socket
* @param eventType - Type of connection event (connect, close, error)
* @param session - The SMTP session
* @param error - Optional error object for error events
*/
public static logConnection(
socket: plugins.net.Socket | plugins.tls.TLSSocket,
eventType: 'connect' | 'close' | 'error',
session?: ISmtpSession,
error?: Error
): void {
const clientInfo = {
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
secure: socket instanceof plugins.tls.TLSSocket,
sessionId: session?.id,
sessionState: session?.state
};
switch (eventType) {
case 'connect':
this.info(`New ${clientInfo.secure ? 'secure ' : ''}connection from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo);
break;
case 'close':
this.info(`Connection closed from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, clientInfo);
break;
case 'error':
this.error(`Connection error from ${clientInfo.remoteAddress}:${clientInfo.remotePort}`, {
...clientInfo,
error
});
break;
}
}
/**
* Log security event
* @param level - Security log level
* @param type - Security event type
* @param message - Log message
* @param details - Event details
* @param ipAddress - Client IP address
* @param domain - Optional domain involved
* @param success - Whether the security check was successful
*/
public static logSecurityEvent(
level: SecurityLogLevel,
type: SecurityEventType,
message: string,
details: Record<string, any>,
ipAddress?: string,
domain?: string,
success?: boolean
): void {
// Map security log level to system log level
const logLevel: LogLevel = level === SecurityLogLevel.DEBUG ? 'debug' :
level === SecurityLogLevel.INFO ? 'info' :
level === SecurityLogLevel.WARN ? 'warn' : 'error';
// Log the security event
this.log(logLevel, message, {
component: 'smtp-security',
eventType: type,
success,
ipAddress,
domain,
...details
});
}
}
/**
* Default instance for backward compatibility
*/
export const smtpLogger = SmtpLogger;

View File

@ -0,0 +1,194 @@
/**
* SMTP Validation Utilities
* Provides validation functions for SMTP server
*/
import { SmtpState } from '../../interfaces.js';
import { SMTP_PATTERNS } from '../constants.js';
/**
* Validates an email address
* @param email - Email address to validate
* @returns Whether the email address is valid
*/
export function isValidEmail(email: string): boolean {
if (!email || typeof email !== 'string') {
return false;
}
return SMTP_PATTERNS.EMAIL.test(email);
}
/**
* Validates the MAIL FROM command syntax
* @param args - Arguments string from the MAIL FROM command
* @returns Object with validation result and extracted data
*/
export function validateMailFrom(args: string): {
isValid: boolean;
address?: string;
params?: Record<string, string>;
errorMessage?: string;
} {
if (!args) {
return { isValid: false, errorMessage: 'Missing arguments' };
}
const match = args.match(SMTP_PATTERNS.MAIL_FROM);
if (!match) {
return { isValid: false, errorMessage: 'Invalid syntax' };
}
const [, address, paramsString] = match;
if (!isValidEmail(address)) {
return { isValid: false, errorMessage: 'Invalid email address' };
}
// Parse parameters if they exist
const params: Record<string, string> = {};
if (paramsString) {
let paramMatch;
const paramRegex = SMTP_PATTERNS.PARAM;
paramRegex.lastIndex = 0; // Reset the regex
while ((paramMatch = paramRegex.exec(paramsString)) !== null) {
const [, name, value = ''] = paramMatch;
params[name.toUpperCase()] = value;
}
}
return { isValid: true, address, params };
}
/**
* Validates the RCPT TO command syntax
* @param args - Arguments string from the RCPT TO command
* @returns Object with validation result and extracted data
*/
export function validateRcptTo(args: string): {
isValid: boolean;
address?: string;
params?: Record<string, string>;
errorMessage?: string;
} {
if (!args) {
return { isValid: false, errorMessage: 'Missing arguments' };
}
const match = args.match(SMTP_PATTERNS.RCPT_TO);
if (!match) {
return { isValid: false, errorMessage: 'Invalid syntax' };
}
const [, address, paramsString] = match;
if (!isValidEmail(address)) {
return { isValid: false, errorMessage: 'Invalid email address' };
}
// Parse parameters if they exist
const params: Record<string, string> = {};
if (paramsString) {
let paramMatch;
const paramRegex = SMTP_PATTERNS.PARAM;
paramRegex.lastIndex = 0; // Reset the regex
while ((paramMatch = paramRegex.exec(paramsString)) !== null) {
const [, name, value = ''] = paramMatch;
params[name.toUpperCase()] = value;
}
}
return { isValid: true, address, params };
}
/**
* Validates the EHLO command syntax
* @param args - Arguments string from the EHLO command
* @returns Object with validation result and extracted data
*/
export function validateEhlo(args: string): {
isValid: boolean;
hostname?: string;
errorMessage?: string;
} {
if (!args) {
return { isValid: false, errorMessage: 'Missing domain name' };
}
const match = args.match(SMTP_PATTERNS.EHLO);
if (!match) {
return { isValid: false, errorMessage: 'Invalid syntax' };
}
const hostname = match[1];
// Check for invalid characters in hostname
if (hostname.includes('@') || hostname.includes('<')) {
return { isValid: false, errorMessage: 'Invalid domain name format' };
}
return { isValid: true, hostname };
}
/**
* Validates command in the current SMTP state
* @param command - SMTP command
* @param currentState - Current SMTP state
* @returns Whether the command is valid in the current state
*/
export function isValidCommandSequence(command: string, currentState: SmtpState): boolean {
const upperCommand = command.toUpperCase();
// Some commands are valid in any state
if (upperCommand === 'QUIT' || upperCommand === 'RSET' || upperCommand === 'NOOP' || upperCommand === 'HELP') {
return true;
}
// State-specific validation
switch (currentState) {
case SmtpState.GREETING:
return upperCommand === 'EHLO' || upperCommand === 'HELO';
case SmtpState.AFTER_EHLO:
return upperCommand === 'MAIL' || upperCommand === 'STARTTLS' || upperCommand === 'AUTH';
case SmtpState.MAIL_FROM:
case SmtpState.RCPT_TO:
if (upperCommand === 'RCPT') {
return true;
}
return currentState === SmtpState.RCPT_TO && upperCommand === 'DATA';
case SmtpState.DATA:
// In DATA state, only the data content is accepted, not commands
return false;
case SmtpState.DATA_RECEIVING:
// In DATA_RECEIVING state, only the data content is accepted, not commands
return false;
case SmtpState.FINISHED:
// After data is received, only new transactions or session end
return upperCommand === 'MAIL' || upperCommand === 'QUIT' || upperCommand === 'RSET';
default:
return false;
}
}
/**
* Validates if a hostname is valid according to RFC 5321
* @param hostname - Hostname to validate
* @returns Whether the hostname is valid
*/
export function isValidHostname(hostname: string): boolean {
if (!hostname || typeof hostname !== 'string') {
return false;
}
// Basic hostname validation
// This is a simplified check, full RFC compliance would be more complex
return /^[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])?)*$/.test(hostname);
}