feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
Some checks failed
CI / Type Check & Lint (push) Failing after 3s
CI / Build Test (Current Platform) (push) Failing after 3s
CI / Build All Platforms (push) Failing after 3s

feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution

feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
This commit is contained in:
2025-10-28 19:46:17 +00:00
parent 6523c55516
commit 17f5661636
271 changed files with 61736 additions and 6222 deletions

View File

@@ -18,7 +18,6 @@ import { mergeWithDefaults } from './utils/helpers.ts';
import { SmtpLogger } from './utils/logging.ts';
import { adaptiveLogger } from './utils/adaptive-logging.ts';
import { UnifiedEmailServer } from '../../routing/classes.unified.email.server.ts';
import { ConnectionWrapper } from './utils/connection-wrapper.ts';
/**
* SMTP Server implementation
@@ -66,20 +65,15 @@ export class SmtpServer implements ISmtpServer {
private options: ISmtpServerOptions;
/**
* Deno listener instance (replaces Node.js net.Server)
* Net server instance
*/
private listener: Deno.Listener | null = null;
private server: plugins.net.Server | null = null;
/**
* Accept loop promise for clean shutdown
*/
private acceptLoop: Promise<void> | null = null;
/**
* Secure server instance (TLS/SSL)
* Secure server instance
*/
private secureServer: plugins.tls.Server | null = null;
/**
* Whether the server is running
*/
@@ -152,26 +146,60 @@ export class SmtpServer implements ISmtpServer {
}
try {
// Create Deno listener (native networking, replaces Node.js net.createServer)
this.listener = Deno.listen({
hostname: this.options.host || '0.0.0.0',
port: this.options.port,
transport: 'tcp',
// 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);
});
});
SmtpLogger.info(`SMTP server listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`, {
component: 'smtp-server',
// 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<void>((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 accepting connections in the background
this.acceptLoop = this.acceptConnections();
// 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.ts');
const { createSecureTlsServer } = await import('./secure-server.js');
// Create secure server with the certificates
// This uses a more robust approach to certificate loading and validation
@@ -277,67 +305,6 @@ export class SmtpServer implements ISmtpServer {
}
}
/**
* Accept connections in a loop (Deno-native networking)
*/
private async acceptConnections(): Promise<void> {
if (!this.listener) {
return;
}
try {
for await (const conn of this.listener) {
if (!this.running) {
conn.close();
break;
}
// Wrap Deno.Conn in ConnectionWrapper for Socket compatibility
const wrapper = new ConnectionWrapper(conn);
// Handle connection in the background
this.handleConnection(wrapper as any).catch(error => {
SmtpLogger.error(`Error handling connection: ${error instanceof Error ? error.message : String(error)}`, {
component: 'smtp-server',
error: error instanceof Error ? error : new Error(String(error)),
});
});
}
} catch (error) {
if (this.running) {
SmtpLogger.error(`Error in accept loop: ${error instanceof Error ? error.message : String(error)}`, {
component: 'smtp-server',
error: error instanceof Error ? error : new Error(String(error)),
});
}
}
}
/**
* Handle a single connection
*/
private async handleConnection(socket: plugins.net.Socket): Promise<void> {
try {
// Check IP reputation before handling connection
const allowed = await this.securityHandler.checkIpReputation(socket);
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);
}
}
/**
* Stop the SMTP server
* @returns Promise that resolves when server is stopped
@@ -364,27 +331,24 @@ export class SmtpServer implements ISmtpServer {
// Close servers
const closePromises: Promise<void>[] = [];
// Close Deno listener
if (this.listener) {
try {
this.listener.close();
} catch (error) {
SmtpLogger.error(`Error closing listener: ${error instanceof Error ? error.message : String(error)}`, {
component: 'smtp-server',
});
}
this.listener = null;
}
// Wait for accept loop to finish
if (this.acceptLoop) {
if (this.server) {
closePromises.push(
this.acceptLoop.catch(() => {
// Accept loop may throw when listener is closed, ignore
new Promise<void>((resolve, reject) => {
if (!this.server) {
resolve();
return;
}
this.server.close((err) => {
if (err) {
reject(err);
} else {
resolve();
}
});
})
);
this.acceptLoop = null;
}
if (this.secureServer) {
@@ -417,6 +381,7 @@ export class SmtpServer implements ISmtpServer {
})
]);
this.server = null;
this.secureServer = null;
this.running = false;
@@ -571,25 +536,30 @@ export class SmtpServer implements ISmtpServer {
try {
// Determine which server to restart
const isStandardServer = serverType === 'standard';
// Close the affected server
if (isStandardServer && this.listener) {
try {
this.listener.close();
} catch (error) {
SmtpLogger.warn(`Error during listener close in recovery: ${error instanceof Error ? error.message : String(error)}`);
}
this.listener = null;
// Wait for accept loop to finish
if (this.acceptLoop) {
try {
await this.acceptLoop;
} catch {
// Ignore errors from accept loop
if (isStandardServer && this.server) {
await new Promise<void>((resolve) => {
if (!this.server) {
resolve();
return;
}
this.acceptLoop = null;
}
// 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<void>((resolve) => {
if (!this.secureServer) {
@@ -623,27 +593,62 @@ export class SmtpServer implements ISmtpServer {
// Restart the affected server
if (isStandardServer) {
try {
// Create Deno listener for recovery
this.listener = Deno.listen({
hostname: this.options.host || '0.0.0.0',
port: this.options.port,
transport: 'tcp',
// 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<void>((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();
});
SmtpLogger.info(`SMTP server recovered and listening on ${this.options.host || '0.0.0.0'}:${this.options.port}`);
// Start accepting connections again
this.acceptLoop = this.acceptConnections();
} catch (listenError) {
SmtpLogger.error(`Failed to restart server during recovery: ${listenError instanceof Error ? listenError.message : String(listenError)}`);
throw listenError;
}
// 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.ts');
const { createSecureTlsServer } = await import('./secure-server.js');
// Create secure server with the certificates
this.secureServer = createSecureTlsServer({
@@ -779,7 +784,7 @@ export class SmtpServer implements ISmtpServer {
await Promise.all(destroyPromises);
// Destroy the adaptive logger singleton to clean up its timer
const { adaptiveLogger } = await import('./utils/adaptive-logging.ts');
const { adaptiveLogger } = await import('./utils/adaptive-logging.js');
if (adaptiveLogger && typeof adaptiveLogger.destroy === 'function') {
adaptiveLogger.destroy();
}