Files
smartmta/dist_ts/mail/delivery/smtpserver/smtp-server.js

698 lines
55 KiB
JavaScript
Raw Normal View History

2026-02-10 15:54:09 +00:00
/**
* SMTP Server
* Core implementation for the refactored SMTP server
*/
import * as plugins from '../../../plugins.js';
import { SmtpState } from './interfaces.js';
import { SessionManager } from './session-manager.js';
import { ConnectionManager } from './connection-manager.js';
import { CommandHandler } from './command-handler.js';
import { DataHandler } from './data-handler.js';
import { TlsHandler } from './tls-handler.js';
import { SecurityHandler } from './security-handler.js';
import { SMTP_DEFAULTS } from './constants.js';
import { mergeWithDefaults } from './utils/helpers.js';
import { SmtpLogger } from './utils/logging.js';
import { adaptiveLogger } from './utils/adaptive-logging.js';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.js';
/**
* SMTP Server implementation
* The main server class that coordinates all components
*/
export class SmtpServer {
/**
* Email server reference
*/
emailServer;
/**
* Session manager
*/
sessionManager;
/**
* Connection manager
*/
connectionManager;
/**
* Command handler
*/
commandHandler;
/**
* Data handler
*/
dataHandler;
/**
* TLS handler
*/
tlsHandler;
/**
* Security handler
*/
securityHandler;
/**
* SMTP server options
*/
options;
/**
* Net server instance
*/
server = null;
/**
* Secure server instance
*/
secureServer = null;
/**
* Whether the server is running
*/
running = false;
/**
* Server recovery state
*/
recoveryState = {
/**
* Whether recovery is in progress
*/
recovering: false,
/**
* Number of consecutive connection failures
*/
connectionFailures: 0,
/**
* Last recovery attempt timestamp
*/
lastRecoveryAttempt: 0,
/**
* Recovery cooldown in milliseconds
*/
recoveryCooldown: 5000,
/**
* Maximum recovery attempts before giving up
*/
maxRecoveryAttempts: 3,
/**
* Current recovery attempt
*/
currentRecoveryAttempt: 0
};
/**
* Creates a new SMTP server
* @param config - Server configuration
*/
constructor(config) {
this.emailServer = config.emailServer;
this.options = mergeWithDefaults(config.options);
// Create components - all components now receive the SMTP server instance
this.sessionManager = config.sessionManager || new SessionManager({
socketTimeout: this.options.socketTimeout,
connectionTimeout: this.options.connectionTimeout,
cleanupInterval: this.options.cleanupInterval
});
this.securityHandler = config.securityHandler || new SecurityHandler(this);
this.tlsHandler = config.tlsHandler || new TlsHandler(this);
this.dataHandler = config.dataHandler || new DataHandler(this);
this.commandHandler = config.commandHandler || new CommandHandler(this);
this.connectionManager = config.connectionManager || new ConnectionManager(this);
}
/**
* Start the SMTP server
* @returns Promise that resolves when server is started
*/
async listen() {
if (this.running) {
throw new Error('SMTP server is already running');
}
try {
// Create the server
this.server = plugins.net.createServer((socket) => {
// Check IP reputation before handling connection
this.securityHandler.checkIpReputation(socket)
.then(allowed => {
if (allowed) {
this.connectionManager.handleNewConnection(socket);
}
else {
// Close connection if IP is not allowed
socket.destroy();
}
})
.catch(error => {
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
error: error instanceof Error ? error : new Error(String(error))
});
// Allow connection on error (fail open)
this.connectionManager.handleNewConnection(socket);
});
});
// Set up error handling with recovery
this.server.on('error', (err) => {
SmtpLogger.error(`SMTP server error: ${err.message}`, { error: err });
// Try to recover from specific errors
if (this.shouldAttemptRecovery(err)) {
this.attemptServerRecovery('standard', err);
}
});
// Start listening
await new Promise((resolve, reject) => {
if (!this.server) {
reject(new Error('Server not initialized'));
return;
}
this.server.listen(this.options.port, this.options.host, () => {
SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`);
resolve();
});
this.server.on('error', reject);
});
// Start secure server if configured
if (this.options.securePort && this.tlsHandler.isTlsEnabled()) {
try {
// Import the secure server creation utility from our new module
// This gives us better certificate handling and error resilience
const { createSecureTlsServer } = await import('./secure-server.js');
// Create secure server with the certificates
// This uses a more robust approach to certificate loading and validation
this.secureServer = createSecureTlsServer({
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca
});
SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort}`);
if (this.secureServer) {
// Use explicit error handling for secure connections
this.secureServer.on('tlsClientError', (err, tlsSocket) => {
SmtpLogger.error(`TLS client error: ${err.message}`, {
error: err,
remoteAddress: tlsSocket.remoteAddress,
remotePort: tlsSocket.remotePort,
stack: err.stack
});
// No need to destroy, the error event will handle that
});
// Register the secure connection handler
this.secureServer.on('secureConnection', (socket) => {
SmtpLogger.info(`New secure connection from ${socket.remoteAddress}:${socket.remotePort}`, {
protocol: socket.getProtocol(),
cipher: socket.getCipher()?.name
});
// Check IP reputation before handling connection
this.securityHandler.checkIpReputation(socket)
.then(allowed => {
if (allowed) {
// Pass the connection to the connection manager
this.connectionManager.handleNewSecureConnection(socket);
}
else {
// Close connection if IP is not allowed
socket.destroy();
}
})
.catch(error => {
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
error: error instanceof Error ? error : new Error(String(error)),
stack: error instanceof Error ? error.stack : 'No stack trace available'
});
// Allow connection on error (fail open)
this.connectionManager.handleNewSecureConnection(socket);
});
});
// Global error handler for the secure server with recovery
this.secureServer.on('error', (err) => {
SmtpLogger.error(`SMTP secure server error: ${err.message}`, {
error: err,
stack: err.stack
});
// Try to recover from specific errors
if (this.shouldAttemptRecovery(err)) {
this.attemptServerRecovery('secure', err);
}
});
// Start listening on secure port
await new Promise((resolve, reject) => {
if (!this.secureServer) {
reject(new Error('Secure server not initialized'));
return;
}
this.secureServer.listen(this.options.securePort, this.options.host, () => {
SmtpLogger.info(`SMTP secure server listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`);
resolve();
});
// Only use error event for startup issues
this.secureServer.once('error', reject);
});
}
else {
SmtpLogger.warn('Failed to create secure server, TLS may not be properly configured');
}
}
catch (error) {
SmtpLogger.error(`Error setting up secure server: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error)),
stack: error instanceof Error ? error.stack : 'No stack trace available'
});
}
}
this.running = true;
}
catch (error) {
SmtpLogger.error(`Failed to start SMTP server: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error))
});
// Clean up on error
this.close();
throw error;
}
}
/**
* Stop the SMTP server
* @returns Promise that resolves when server is stopped
*/
async close() {
if (!this.running) {
return;
}
SmtpLogger.info('Stopping SMTP server');
try {
// Close all active connections
this.connectionManager.closeAllConnections();
// Clear all sessions
this.sessionManager.clearAllSessions();
// Clean up adaptive logger to prevent hanging timers
adaptiveLogger.destroy();
// Destroy all components to clean up their resources
await this.destroy();
// Close servers
const closePromises = [];
if (this.server) {
closePromises.push(new Promise((resolve, reject) => {
if (!this.server) {
resolve();
return;
}
this.server.close((err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
}));
}
if (this.secureServer) {
closePromises.push(new Promise((resolve, reject) => {
if (!this.secureServer) {
resolve();
return;
}
this.secureServer.close((err) => {
if (err) {
reject(err);
}
else {
resolve();
}
});
}));
}
// Add timeout to prevent hanging on close
await Promise.race([
Promise.all(closePromises),
new Promise((resolve) => {
setTimeout(() => {
SmtpLogger.warn('Server close timed out after 3 seconds, forcing shutdown');
resolve();
}, 3000);
})
]);
this.server = null;
this.secureServer = null;
this.running = false;
SmtpLogger.info('SMTP server stopped');
}
catch (error) {
SmtpLogger.error(`Error stopping SMTP server: ${error instanceof Error ? error.message : String(error)}`, {
error: error instanceof Error ? error : new Error(String(error))
});
throw error;
}
}
/**
* Get the session manager
* @returns Session manager instance
*/
getSessionManager() {
return this.sessionManager;
}
/**
* Get the connection manager
* @returns Connection manager instance
*/
getConnectionManager() {
return this.connectionManager;
}
/**
* Get the command handler
* @returns Command handler instance
*/
getCommandHandler() {
return this.commandHandler;
}
/**
* Get the data handler
* @returns Data handler instance
*/
getDataHandler() {
return this.dataHandler;
}
/**
* Get the TLS handler
* @returns TLS handler instance
*/
getTlsHandler() {
return this.tlsHandler;
}
/**
* Get the security handler
* @returns Security handler instance
*/
getSecurityHandler() {
return this.securityHandler;
}
/**
* Get the server options
* @returns SMTP server options
*/
getOptions() {
return this.options;
}
/**
* Get the email server reference
* @returns Email server instance
*/
getEmailServer() {
return this.emailServer;
}
/**
* Check if the server is running
* @returns Whether the server is running
*/
isRunning() {
return this.running;
}
/**
* Check if we should attempt to recover from an error
* @param error - The error that occurred
* @returns Whether recovery should be attempted
*/
shouldAttemptRecovery(error) {
// Skip recovery if we're already in recovery mode
if (this.recoveryState.recovering) {
return false;
}
// Check if we've reached the maximum number of recovery attempts
if (this.recoveryState.currentRecoveryAttempt >= this.recoveryState.maxRecoveryAttempts) {
SmtpLogger.warn('Maximum recovery attempts reached, not attempting further recovery');
return false;
}
// Check if enough time has passed since the last recovery attempt
const now = Date.now();
if (now - this.recoveryState.lastRecoveryAttempt < this.recoveryState.recoveryCooldown) {
SmtpLogger.warn('Recovery cooldown period not elapsed, skipping recovery attempt');
return false;
}
// Recoverable errors include:
// - EADDRINUSE: Address already in use (port conflict)
// - ECONNRESET: Connection reset by peer
// - EPIPE: Broken pipe
// - ETIMEDOUT: Connection timed out
const recoverableErrors = [
'EADDRINUSE',
'ECONNRESET',
'EPIPE',
'ETIMEDOUT',
'ECONNABORTED',
'EPROTO',
'EMFILE' // Too many open files
];
// Check if this is a recoverable error
const errorCode = error.code;
return recoverableErrors.includes(errorCode);
}
/**
* Attempt to recover the server after a critical error
* @param serverType - The type of server to recover ('standard' or 'secure')
* @param error - The error that triggered recovery
*/
async attemptServerRecovery(serverType, error) {
// Set recovery flag to prevent multiple simultaneous recovery attempts
if (this.recoveryState.recovering) {
SmtpLogger.warn('Recovery already in progress, skipping new recovery attempt');
return;
}
this.recoveryState.recovering = true;
this.recoveryState.lastRecoveryAttempt = Date.now();
this.recoveryState.currentRecoveryAttempt++;
SmtpLogger.info(`Attempting server recovery for ${serverType} server after error: ${error.message}`, {
attempt: this.recoveryState.currentRecoveryAttempt,
maxAttempts: this.recoveryState.maxRecoveryAttempts,
errorCode: error.code
});
try {
// Determine which server to restart
const isStandardServer = serverType === 'standard';
// Close the affected server
if (isStandardServer && this.server) {
await new Promise((resolve) => {
if (!this.server) {
resolve();
return;
}
// First try a clean shutdown
this.server.close((err) => {
if (err) {
SmtpLogger.warn(`Error during server close in recovery: ${err.message}`);
}
resolve();
});
// Set a timeout to force close
setTimeout(() => {
resolve();
}, 3000);
});
this.server = null;
}
else if (!isStandardServer && this.secureServer) {
await new Promise((resolve) => {
if (!this.secureServer) {
resolve();
return;
}
// First try a clean shutdown
this.secureServer.close((err) => {
if (err) {
SmtpLogger.warn(`Error during secure server close in recovery: ${err.message}`);
}
resolve();
});
// Set a timeout to force close
setTimeout(() => {
resolve();
}, 3000);
});
this.secureServer = null;
}
// Short delay before restarting
await new Promise((resolve) => setTimeout(resolve, 1000));
// Clean up any lingering connections
this.connectionManager.closeAllConnections();
this.sessionManager.clearAllSessions();
// Restart the affected server
if (isStandardServer) {
// Create and start the standard server
this.server = plugins.net.createServer((socket) => {
// Check IP reputation before handling connection
this.securityHandler.checkIpReputation(socket)
.then(allowed => {
if (allowed) {
this.connectionManager.handleNewConnection(socket);
}
else {
// Close connection if IP is not allowed
socket.destroy();
}
})
.catch(error => {
SmtpLogger.error(`IP reputation check error: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
error: error instanceof Error ? error : new Error(String(error))
});
// Allow connection on error (fail open)
this.connectionManager.handleNewConnection(socket);
});
});
// Set up error handling with recovery
this.server.on('error', (err) => {
SmtpLogger.error(`SMTP server error after recovery: ${err.message}`, { error: err });
// Try to recover again if needed
if (this.shouldAttemptRecovery(err)) {
this.attemptServerRecovery('standard', err);
}
});
// Start listening again
await new Promise((resolve, reject) => {
if (!this.server) {
reject(new Error('Server not initialized during recovery'));
return;
}
this.server.listen(this.options.port, this.options.host, () => {
SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`);
resolve();
});
// Only use error event for startup issues during recovery
this.server.once('error', (err) => {
SmtpLogger.error(`Failed to restart server during recovery: ${err.message}`);
reject(err);
});
});
}
else if (this.options.securePort && this.tlsHandler.isTlsEnabled()) {
// Try to recreate the secure server
try {
// Import the secure server creation utility
const { createSecureTlsServer } = await import('./secure-server.js');
// Create secure server with the certificates
this.secureServer = createSecureTlsServer({
key: this.options.key,
cert: this.options.cert,
ca: this.options.ca
});
if (this.secureServer) {
SmtpLogger.info(`Created secure TLS server for port ${this.options.securePort} during recovery`);
// Use explicit error handling for secure connections
this.secureServer.on('tlsClientError', (err, tlsSocket) => {
SmtpLogger.error(`TLS client error after recovery: ${err.message}`, {
error: err,
remoteAddress: tlsSocket.remoteAddress,
remotePort: tlsSocket.remotePort,
stack: err.stack
});
});
// Register the secure connection handler
this.secureServer.on('secureConnection', (socket) => {
// Check IP reputation before handling connection
this.securityHandler.checkIpReputation(socket)
.then(allowed => {
if (allowed) {
// Pass the connection to the connection manager
this.connectionManager.handleNewSecureConnection(socket);
}
else {
// Close connection if IP is not allowed
socket.destroy();
}
})
.catch(error => {
SmtpLogger.error(`IP reputation check error after recovery: ${error instanceof Error ? error.message : String(error)}`, {
remoteAddress: socket.remoteAddress,
error: error instanceof Error ? error : new Error(String(error))
});
// Allow connection on error (fail open)
this.connectionManager.handleNewSecureConnection(socket);
});
});
// Global error handler for the secure server with recovery
this.secureServer.on('error', (err) => {
SmtpLogger.error(`SMTP secure server error after recovery: ${err.message}`, {
error: err,
stack: err.stack
});
// Try to recover again if needed
if (this.shouldAttemptRecovery(err)) {
this.attemptServerRecovery('secure', err);
}
});
// Start listening on secure port again
await new Promise((resolve, reject) => {
if (!this.secureServer) {
reject(new Error('Secure server not initialized during recovery'));
return;
}
this.secureServer.listen(this.options.securePort, this.options.host, () => {
SmtpLogger.info(`SMTP secure server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.securePort}`);
resolve();
});
// Only use error event for startup issues during recovery
this.secureServer.once('error', (err) => {
SmtpLogger.error(`Failed to restart secure server during recovery: ${err.message}`);
reject(err);
});
});
}
else {
SmtpLogger.warn('Failed to create secure server during recovery');
}
}
catch (error) {
SmtpLogger.error(`Error setting up secure server during recovery: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Recovery successful
SmtpLogger.info('Server recovery completed successfully');
}
catch (recoveryError) {
SmtpLogger.error(`Server recovery failed: ${recoveryError instanceof Error ? recoveryError.message : String(recoveryError)}`, {
error: recoveryError instanceof Error ? recoveryError : new Error(String(recoveryError)),
attempt: this.recoveryState.currentRecoveryAttempt,
maxAttempts: this.recoveryState.maxRecoveryAttempts
});
}
finally {
// Reset recovery flag
this.recoveryState.recovering = false;
}
}
/**
* Clean up all component resources
*/
async destroy() {
SmtpLogger.info('Destroying SMTP server components');
// Destroy all components in parallel
const destroyPromises = [];
if (this.sessionManager && typeof this.sessionManager.destroy === 'function') {
destroyPromises.push(Promise.resolve(this.sessionManager.destroy()));
}
if (this.connectionManager && typeof this.connectionManager.destroy === 'function') {
destroyPromises.push(Promise.resolve(this.connectionManager.destroy()));
}
if (this.commandHandler && typeof this.commandHandler.destroy === 'function') {
destroyPromises.push(Promise.resolve(this.commandHandler.destroy()));
}
if (this.dataHandler && typeof this.dataHandler.destroy === 'function') {
destroyPromises.push(Promise.resolve(this.dataHandler.destroy()));
}
if (this.tlsHandler && typeof this.tlsHandler.destroy === 'function') {
destroyPromises.push(Promise.resolve(this.tlsHandler.destroy()));
}
if (this.securityHandler && typeof this.securityHandler.destroy === 'function') {
destroyPromises.push(Promise.resolve(this.securityHandler.destroy()));
}
await Promise.all(destroyPromises);
// Destroy the adaptive logger singleton to clean up its timer
const { adaptiveLogger } = await import('./utils/adaptive-logging.js');
if (adaptiveLogger && typeof adaptiveLogger.destroy === 'function') {
adaptiveLogger.destroy();
}
// Clear recovery state
this.recoveryState = {
recovering: false,
connectionFailures: 0,
lastRecoveryAttempt: 0,
recoveryCooldown: 5000,
maxRecoveryAttempts: 3,
currentRecoveryAttempt: 0
};
SmtpLogger.info('All SMTP server components destroyed');
}
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic210cC1zZXJ2ZXIuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi90cy9tYWlsL2RlbGl2ZXJ5L3NtdHBzZXJ2ZXIvc210cC1zZXJ2ZXIudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7OztHQUdHO0FBRUgsT0FBTyxLQUFLLE9BQU8sTUFBTSxxQkFBcUIsQ0FBQztBQUMvQyxPQUFPLEVBQUUsU0FBUyxFQUFFLE1BQU0saUJBQWlCLENBQUM7QUFHNUMsT0FBTyxFQUFFLGNBQWMsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQ3RELE9BQU8sRUFBRSxpQkFBaUIsRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQzVELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSxzQkFBc0IsQ0FBQztBQUN0RCxPQUFPLEVBQUUsV0FBVyxFQUFFLE1BQU0sbUJBQW1CLENBQUM7QUFDaEQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGtCQUFrQixDQUFDO0FBQzlDLE9BQU8sRUFBRSxlQUFlLEVBQUUsTUFBTSx1QkFBdUIsQ0FBQztBQUN4RCxPQUFPLEVBQUUsYUFBYSxFQUFFLE1BQU0sZ0JBQWdCLENBQUM7QUFDL0MsT0FBTyxFQUFFLGlCQUFpQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDdkQsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLG9CQUFvQixDQUFDO0FBQ2hELE9BQU8sRUFBRSxjQUFjLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUM3RCxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSwrQ0FBK0MsQ0FBQztBQUVuRjs7O0dBR0c7QUFDSCxNQUFNLE9BQU8sVUFBVTtJQUNyQjs7T0FFRztJQUNLLFdBQVcsQ0FBcUI7SUFFeEM7O09BRUc7SUFDSyxjQUFjLENBQWtCO0lBRXhDOztPQUVHO0lBQ0ssaUJBQWlCLENBQXFCO0lBRTlDOztPQUVHO0lBQ0ssY0FBYyxDQUFrQjtJQUV4Qzs7T0FFRztJQUNLLFdBQVcsQ0FBZTtJQUVsQzs7T0FFRztJQUNLLFVBQVUsQ0FBYztJQUVoQzs7T0FFRztJQUNLLGVBQWUsQ0FBbUI7SUFFMUM7O09BRUc7SUFDSyxPQUFPLENBQXFCO0lBRXBDOztPQUVHO0lBQ0ssTUFBTSxHQUE4QixJQUFJLENBQUM7SUFFakQ7O09BRUc7SUFDSyxZQUFZLEdBQThCLElBQUksQ0FBQztJQUV2RDs7T0FFRztJQUNLLE9BQU8sR0FBRyxLQUFLLENBQUM7SUFFeEI7O09BRUc7SUFDSyxhQUFhLEdBQUc7UUFDdEI7O1dBRUc7UUFDSCxVQUFVLEVBQUUsS0FBSztRQUVqQjs7V0FFRztRQUNILGtCQUFrQixFQUFFLENBQUM7UUFFckI7O1dBRUc7UUFDSCxtQkFBbUIsRUFBRSxDQUFDO1FBRXRCOztXQUVHO1FBQ0gsZ0JBQWdCLEVBQUUsSUFBSTtRQUV0Qjs7V0FFRztRQUNILG1CQUFtQixFQUFFLENBQUM7UUFFdEI7O1dBRUc7UUFDSCxzQkFBc0IsRUFBRSxDQUFDO0tBQzFCLENBQUM7SUFFRjs7O09BR0c7SUFDSCxZQUFZLE1BQXlCO1FBQ25DLElBQUksQ0FBQyxXQUFXLEdBQUcsTUFBTSxDQUFDLFdBQVcsQ0FBQztRQUN0QyxJQUFJLENBQUMsT0FBTyxHQUFHLGlCQUFpQixDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUVqRCwwRUFBMEU7UUFDMUUsSUFBSSxDQUFDLGNBQWMsR0FBRyxNQUFNLENBQUMsY0FBYyxJQUFJLElBQUksY0FBYyxDQUFDO1lBQ2hFLGFBQWEsRUFBRSxJQUFJLENBQUMsT0FBTyxDQUFDLGFBQWE7WUFDekMsaUJBQWlCLEVBQUUsSUFBSSxDQUFDLE9BQU8sQ0FBQyxpQkFBaUI7WUFDakQsZUFBZSxFQUFFLElBQUksQ0FBQyxPQUFPLENBQUMsZUFBZTtTQUM5QyxDQUFDLENBQUM7UUFFSCxJQUFJLENBQUMsZUFBZSxHQUFHLE1BQU0sQ0FBQyxlQUFlLElBQUksSUFBSSxlQUFlLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDM0UsSUFBSSxDQUFDLFVBQVUsR0FBRyxNQUFNLENBQUMsVUFBVSxJQUFJLElBQUksVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQzVELElBQUksQ0FBQyxXQUFXLEdBQUcsTUFBTSxDQUFDLFdBQVcsSUFBSSxJQUFJLFdBQVcsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUMvRCxJQUFJLENBQUMsY0FBYyxHQUFHLE1BQU0sQ0FBQyxjQUFjLElBQUksSUFBSSxjQUFjLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDeEUsSUFBSSxDQUFDLGlCQUFpQixHQUFHLE1BQU0sQ0FBQyxpQkFBaUIsSUFBSSxJQUFJLGlCQUFpQixDQUFDLElBQUksQ0FBQyxDQUFDO0lBQ25GLENBQUM7SUFFRDs7O09BR0c7SUFDSSxLQUFLLENBQUMsTUFBTTtRQUNqQixJQUFJLElBQUksQ0FBQyxPQUFPLEVBQUUsQ0FBQztZQUNqQixNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7UUFDcEQsQ0FBQztRQUVELElBQUksQ0FBQztZQUNILG9CQUFvQjtZQUNwQixJQUFJLENBQUMsTUFBTSxHQUFHLE9BQU8sQ0FBQyxHQUFHLENBQUMsWUFBWSxDQUFDLENBQUMsTUFBTSxFQUFFLEVBQUU7Z0JBQ2hELGlEQUFpRDtnQkFDakQsSUFBSSxDQUFDLGVBQWUsQ0FBQyxpQkFBaUIsQ0FBQyxNQUFNLENBQUM7cUJBQzNDLElBQUksQ0FBQyxPQUFPLENBQUMsRUFBRTtvQkFDZCxJQUFJLE9BQU8sRUFBRSxDQUFDO3dCQUNaLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsQ0FBQztvQkFDckQsQ0FBQzt5QkFBTSxDQUFDO3dCQUNOLHdDQUF3Qzt3QkFDeEMsTUFBTSxDQUFDLE9BQU8sRUFBRSxDQUFDO29CQUNuQixDQUFDO2dCQUNILENBQUMsQ0FBQztxQkFDRCxLQUFLLENBQUMsS0FBSyxDQUFDLEVBQUU7b0JBQ2IsVUFBVSxDQUFDLEtBQUssQ0FBQyw4QkFBOEIsS0FBSyxZQUFZLEtBQUssQ0FBQyxDQUFDLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLEVBQUU7d0JBQ3ZHLGFBQWEsRUFBRSxNQUFNLENBQUMsYUFBYTt3QkFDbkMsS0FBSyxFQUFFLEtBQUssWUFBWSxLQUFLLENBQUMsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxLQUFLLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO3FCQUNqRSxDQUFDLENBQUM7b0JBRUgsd0NBQXdDO29CQUN4QyxJQUFJLENBQUMsaUJBQWlCLENBQUMsbUJBQW1CLENBQUMsTUFBTSxDQUFDLENBQUM7Z0JBQ3JELENBQUMsQ0FBQyxDQUFDO1lBQ1AsQ0FBQyxDQUFDLENBQUM7WUFFSCxzQ0FBc0M7WUFDdEMsSUFBSSxDQUFDLE1BQU0sQ0FBQyxFQ