import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; import { Email } from './mta.classes.email.js'; import { EmailSendJob, DeliveryStatus } from './mta.classes.emailsendjob.js'; import { DKIMCreator } from './mta.classes.dkimcreator.js'; import { DKIMVerifier } from './mta.classes.dkimverifier.js'; import { SMTPServer, type ISmtpServerOptions } from './mta.classes.smtpserver.js'; import { DNSManager } from './mta.classes.dnsmanager.js'; import { ApiManager } from './mta.classes.apimanager.js'; import type { SzPlatformService } from '../classes.platformservice.js'; /** * Configuration options for the MTA service */ export interface IMtaConfig { /** SMTP server options */ smtp?: { /** Whether to enable the SMTP server */ enabled?: boolean; /** Port to listen on (default: 25) */ port?: number; /** SMTP server hostname */ hostname?: string; /** Maximum allowed email size in bytes */ maxSize?: number; }; /** SSL/TLS configuration */ tls?: { /** Domain for certificate */ domain?: string; /** Whether to auto-renew certificates */ autoRenew?: boolean; /** Custom key/cert paths (if not using auto-provision) */ keyPath?: string; certPath?: string; }; /** Outbound email sending configuration */ outbound?: { /** Maximum concurrent sending jobs */ concurrency?: number; /** Retry configuration */ retries?: { /** Maximum number of retries per message */ max?: number; /** Initial delay between retries (milliseconds) */ delay?: number; /** Whether to use exponential backoff for retries */ useBackoff?: boolean; }; /** Rate limiting configuration */ rateLimit?: { /** Maximum emails per period */ maxPerPeriod?: number; /** Time period in milliseconds */ periodMs?: number; /** Whether to apply per domain (vs globally) */ perDomain?: boolean; }; }; /** Security settings */ security?: { /** Whether to use DKIM signing */ useDkim?: boolean; /** Whether to verify inbound DKIM signatures */ verifyDkim?: boolean; /** Whether to verify SPF on inbound */ verifySpf?: boolean; /** Whether to use TLS for outbound when available */ useTls?: boolean; /** Whether to require valid certificates */ requireValidCerts?: boolean; }; /** Domains configuration */ domains?: { /** List of domains that this MTA will handle as local */ local?: string[]; /** Whether to auto-create DNS records */ autoCreateDnsRecords?: boolean; /** DKIM selector to use (default: "mta") */ dkimSelector?: string; }; } /** * Email queue entry */ interface QueueEntry { id: string; email: Email; addedAt: Date; processing: boolean; attempts: number; lastAttempt?: Date; nextAttempt?: Date; error?: Error; status: DeliveryStatus; } /** * Certificate information */ interface Certificate { privateKey: string; publicKey: string; expiresAt: Date; } /** * Stats for MTA monitoring */ interface MtaStats { startTime: Date; emailsReceived: number; emailsSent: number; emailsFailed: number; activeConnections: number; queueSize: number; certificateInfo?: { domain: string; expiresAt: Date; daysUntilExpiry: number; }; } /** * Main MTA Service class that coordinates all email functionality */ export class MtaService { /** Reference to the platform service */ public platformServiceRef: SzPlatformService; /** SMTP server instance */ public server: SMTPServer; /** DKIM creator for signing outgoing emails */ public dkimCreator: DKIMCreator; /** DKIM verifier for validating incoming emails */ public dkimVerifier: DKIMVerifier; /** DNS manager for handling DNS records */ public dnsManager: DNSManager; /** API manager for external integrations */ public apiManager: ApiManager; /** Email queue for outbound emails */ private emailQueue: Map = new Map(); /** Email queue processing state */ private queueProcessing = false; /** Rate limiters for outbound emails */ private rateLimiters: Map = new Map(); /** Certificate cache */ private certificate: Certificate = null; /** MTA configuration */ private config: IMtaConfig; /** Stats for monitoring */ private stats: MtaStats; /** Whether the service is currently running */ private running = false; /** * Initialize the MTA service * @param platformServiceRefArg Reference to the platform service * @param config Configuration options */ constructor(platformServiceRefArg: SzPlatformService, config: IMtaConfig = {}) { this.platformServiceRef = platformServiceRefArg; // Initialize with default configuration this.config = this.getDefaultConfig(); // Merge with provided configuration this.config = this.mergeConfig(this.config, config); // Initialize components this.dkimCreator = new DKIMCreator(this); this.dkimVerifier = new DKIMVerifier(this); this.dnsManager = new DNSManager(this); this.apiManager = new ApiManager(); // Initialize stats this.stats = { startTime: new Date(), emailsReceived: 0, emailsSent: 0, emailsFailed: 0, activeConnections: 0, queueSize: 0 }; // Ensure required directories exist this.ensureDirectories(); } /** * Get default configuration */ private getDefaultConfig(): IMtaConfig { return { smtp: { enabled: true, port: 25, hostname: 'mta.lossless.one', maxSize: 10 * 1024 * 1024 // 10MB }, tls: { domain: 'mta.lossless.one', autoRenew: true }, outbound: { concurrency: 5, retries: { max: 3, delay: 300000, // 5 minutes useBackoff: true }, rateLimit: { maxPerPeriod: 100, periodMs: 60000, // 1 minute perDomain: true } }, security: { useDkim: true, verifyDkim: true, verifySpf: true, useTls: true, requireValidCerts: false }, domains: { local: ['lossless.one'], autoCreateDnsRecords: true, dkimSelector: 'mta' } }; } /** * Merge configurations */ private mergeConfig(defaultConfig: IMtaConfig, customConfig: IMtaConfig): IMtaConfig { // Deep merge of configurations // (A more robust implementation would use a dedicated deep-merge library) const merged = { ...defaultConfig }; // Merge first level for (const [key, value] of Object.entries(customConfig)) { if (value === null || value === undefined) continue; if (typeof value === 'object' && !Array.isArray(value)) { merged[key] = { ...merged[key], ...value }; } else { merged[key] = value; } } return merged; } /** * Ensure required directories exist */ private ensureDirectories(): void { plugins.smartfile.fs.ensureDirSync(paths.keysDir); plugins.smartfile.fs.ensureDirSync(paths.sentEmailsDir); plugins.smartfile.fs.ensureDirSync(paths.receivedEmailsDir); plugins.smartfile.fs.ensureDirSync(paths.failedEmailsDir); plugins.smartfile.fs.ensureDirSync(paths.dnsRecordsDir); plugins.smartfile.fs.ensureDirSync(paths.logsDir); } /** * Start the MTA service */ public async start(): Promise { if (this.running) { console.warn('MTA service is already running'); return; } try { console.log('Starting MTA service...'); // Load or provision certificate await this.loadOrProvisionCertificate(); // Start SMTP server if enabled if (this.config.smtp.enabled) { const smtpOptions: ISmtpServerOptions = { port: this.config.smtp.port, key: this.certificate.privateKey, cert: this.certificate.publicKey, hostname: this.config.smtp.hostname }; this.server = new SMTPServer(this, smtpOptions); this.server.start(); console.log(`SMTP server started on port ${smtpOptions.port}`); } // Start queue processing this.startQueueProcessing(); // Update DNS records for local domains if configured if (this.config.domains.autoCreateDnsRecords) { await this.updateDnsRecordsForLocalDomains(); } this.running = true; console.log('MTA service started successfully'); } catch (error) { console.error('Failed to start MTA service:', error); throw error; } } /** * Stop the MTA service */ public async stop(): Promise { if (!this.running) { console.warn('MTA service is not running'); return; } try { console.log('Stopping MTA service...'); // Stop SMTP server if running if (this.server) { await this.server.stop(); this.server = null; console.log('SMTP server stopped'); } // Stop queue processing this.queueProcessing = false; console.log('Email queue processing stopped'); this.running = false; console.log('MTA service stopped successfully'); } catch (error) { console.error('Error stopping MTA service:', error); throw error; } } /** * Send an email (add to queue) */ public async send(email: Email): Promise { if (!this.running) { throw new Error('MTA service is not running'); } // Generate a unique ID for this email const id = plugins.uuid.v4(); // Validate email this.validateEmail(email); // Create DKIM keys if needed if (this.config.security.useDkim) { await this.dkimCreator.handleDKIMKeysForEmail(email); } // Add to queue this.emailQueue.set(id, { id, email, addedAt: new Date(), processing: false, attempts: 0, status: DeliveryStatus.PENDING }); // Update stats this.stats.queueSize = this.emailQueue.size; console.log(`Email added to queue: ${id}`); return id; } /** * Get status of an email in the queue */ public getEmailStatus(id: string): QueueEntry | null { return this.emailQueue.get(id) || null; } /** * Handle an incoming email */ public async processIncomingEmail(email: Email): Promise { if (!this.running) { throw new Error('MTA service is not running'); } try { console.log(`Processing incoming email from ${email.from} to ${email.to}`); // Update stats this.stats.emailsReceived++; // Check if the recipient domain is local const recipientDomain = email.to[0].split('@')[1]; const isLocalDomain = this.isLocalDomain(recipientDomain); if (isLocalDomain) { // Save to local mailbox await this.saveToLocalMailbox(email); return true; } else { // Forward to another server const forwardId = await this.send(email); console.log(`Forwarding email to ${email.to} with queue ID ${forwardId}`); return true; } } catch (error) { console.error('Error processing incoming email:', error); return false; } } /** * Check if a domain is local */ private isLocalDomain(domain: string): boolean { return this.config.domains.local.includes(domain); } /** * Save an email to a local mailbox */ private async saveToLocalMailbox(email: Email): Promise { // Simplified implementation - in a real system, this would store to a user's mailbox const mailboxPath = plugins.path.join(paths.receivedEmailsDir, 'local'); plugins.smartfile.fs.ensureDirSync(mailboxPath); const emailContent = email.toRFC822String(); const filename = `${Date.now()}_${email.to[0].replace('@', '_at_')}.eml`; plugins.smartfile.memory.toFsSync( emailContent, plugins.path.join(mailboxPath, filename) ); console.log(`Email saved to local mailbox: ${filename}`); } /** * Start processing the email queue */ private startQueueProcessing(): void { if (this.queueProcessing) return; this.queueProcessing = true; this.processQueue(); console.log('Email queue processing started'); } /** * Process emails in the queue */ private async processQueue(): Promise { if (!this.queueProcessing) return; try { // Get pending emails ordered by next attempt time const pendingEmails = Array.from(this.emailQueue.values()) .filter(entry => (entry.status === DeliveryStatus.PENDING || entry.status === DeliveryStatus.DEFERRED) && !entry.processing && (!entry.nextAttempt || entry.nextAttempt <= new Date()) ) .sort((a, b) => { // Sort by next attempt time, then by added time if (a.nextAttempt && b.nextAttempt) { return a.nextAttempt.getTime() - b.nextAttempt.getTime(); } else if (a.nextAttempt) { return 1; } else if (b.nextAttempt) { return -1; } else { return a.addedAt.getTime() - b.addedAt.getTime(); } }); // Determine how many emails we can process concurrently const availableSlots = Math.max(0, this.config.outbound.concurrency - Array.from(this.emailQueue.values()).filter(e => e.processing).length); // Process emails up to our concurrency limit for (let i = 0; i < Math.min(availableSlots, pendingEmails.length); i++) { const entry = pendingEmails[i]; // Check rate limits if (!this.checkRateLimit(entry.email)) { continue; } // Mark as processing entry.processing = true; // Process in background this.processQueueEntry(entry).catch(error => { console.error(`Error processing queue entry ${entry.id}:`, error); }); } } catch (error) { console.error('Error in queue processing:', error); } finally { // Schedule next processing cycle setTimeout(() => this.processQueue(), 1000); } } /** * Process a single queue entry */ private async processQueueEntry(entry: QueueEntry): Promise { try { console.log(`Processing queue entry ${entry.id}`); // Update attempt counters entry.attempts++; entry.lastAttempt = new Date(); // Create send job const sendJob = new EmailSendJob(this, entry.email, { maxRetries: 1, // We handle retries at the queue level tlsOptions: { rejectUnauthorized: this.config.security.requireValidCerts } }); // Send the email const status = await sendJob.send(); entry.status = status; if (status === DeliveryStatus.DELIVERED) { // Success - remove from queue this.emailQueue.delete(entry.id); this.stats.emailsSent++; console.log(`Email ${entry.id} delivered successfully`); } else if (status === DeliveryStatus.FAILED) { // Permanent failure entry.error = sendJob.deliveryInfo.error; this.stats.emailsFailed++; console.log(`Email ${entry.id} failed permanently: ${entry.error.message}`); // Remove from queue this.emailQueue.delete(entry.id); } else if (status === DeliveryStatus.DEFERRED) { // Temporary failure - schedule retry if attempts remain entry.error = sendJob.deliveryInfo.error; if (entry.attempts >= this.config.outbound.retries.max) { // Max retries reached - mark as failed entry.status = DeliveryStatus.FAILED; this.stats.emailsFailed++; console.log(`Email ${entry.id} failed after ${entry.attempts} attempts: ${entry.error.message}`); // Remove from queue this.emailQueue.delete(entry.id); } else { // Schedule retry const delay = this.calculateRetryDelay(entry.attempts); entry.nextAttempt = new Date(Date.now() + delay); console.log(`Email ${entry.id} deferred, next attempt at ${entry.nextAttempt}`); } } } catch (error) { console.error(`Unexpected error processing queue entry ${entry.id}:`, error); // Handle unexpected errors similarly to deferred entry.error = error; if (entry.attempts >= this.config.outbound.retries.max) { entry.status = DeliveryStatus.FAILED; this.stats.emailsFailed++; this.emailQueue.delete(entry.id); } else { entry.status = DeliveryStatus.DEFERRED; const delay = this.calculateRetryDelay(entry.attempts); entry.nextAttempt = new Date(Date.now() + delay); } } finally { // Mark as no longer processing entry.processing = false; // Update stats this.stats.queueSize = this.emailQueue.size; } } /** * Calculate delay before retry based on attempt number */ private calculateRetryDelay(attemptNumber: number): number { const baseDelay = this.config.outbound.retries.delay; if (this.config.outbound.retries.useBackoff) { // Exponential backoff: base_delay * (2^(attempt-1)) return baseDelay * Math.pow(2, attemptNumber - 1); } else { return baseDelay; } } /** * Check if an email can be sent under rate limits */ private checkRateLimit(email: Email): boolean { const config = this.config.outbound.rateLimit; if (!config || !config.maxPerPeriod) { return true; // No rate limit configured } // Determine which limiter to use const key = config.perDomain ? email.getFromDomain() : 'global'; // Initialize limiter if needed if (!this.rateLimiters.has(key)) { this.rateLimiters.set(key, { tokens: config.maxPerPeriod, lastRefill: Date.now() }); } const limiter = this.rateLimiters.get(key); // Refill tokens based on time elapsed const now = Date.now(); const elapsedMs = now - limiter.lastRefill; const tokensToAdd = Math.floor(elapsedMs / config.periodMs) * config.maxPerPeriod; if (tokensToAdd > 0) { limiter.tokens = Math.min(config.maxPerPeriod, limiter.tokens + tokensToAdd); limiter.lastRefill = now - (elapsedMs % config.periodMs); } // Check if we have tokens available if (limiter.tokens > 0) { limiter.tokens--; return true; } else { console.log(`Rate limit exceeded for ${key}`); return false; } } /** * Load or provision a TLS certificate */ private async loadOrProvisionCertificate(): Promise { try { // Check if we have manual cert paths specified if (this.config.tls.keyPath && this.config.tls.certPath) { console.log('Using manually specified certificate files'); const [privateKey, publicKey] = await Promise.all([ plugins.fs.promises.readFile(this.config.tls.keyPath, 'utf-8'), plugins.fs.promises.readFile(this.config.tls.certPath, 'utf-8') ]); this.certificate = { privateKey, publicKey, expiresAt: this.getCertificateExpiry(publicKey) }; console.log(`Certificate loaded, expires: ${this.certificate.expiresAt}`); return; } // Otherwise, use auto-provisioning console.log(`Provisioning certificate for ${this.config.tls.domain}`); this.certificate = await this.provisionCertificate(this.config.tls.domain); console.log(`Certificate provisioned, expires: ${this.certificate.expiresAt}`); // Set up auto-renewal if configured if (this.config.tls.autoRenew) { this.setupCertificateRenewal(); } } catch (error) { console.error('Error loading or provisioning certificate:', error); throw error; } } /** * Provision a certificate from the certificate service */ private async provisionCertificate(domain: string): Promise { try { // Setup proper authentication const authToken = await this.getAuthToken(); if (!authToken) { throw new Error('Failed to obtain authentication token for certificate provisioning'); } // Initialize client const typedrouter = new plugins.typedrequest.TypedRouter(); const typedsocketClient = await plugins.typedsocket.TypedSocket.createClient( typedrouter, 'https://cloudly.lossless.one:443' ); try { // Request certificate const typedCertificateRequest = typedsocketClient.createTypedRequest('getSslCertificate'); const typedResponse = await typedCertificateRequest.fire({ authToken, requiredCertName: domain, }); if (!typedResponse || !typedResponse.certificate) { throw new Error('Invalid response from certificate service'); } // Extract certificate information const cert = typedResponse.certificate; // Determine expiry date const expiresAt = this.getCertificateExpiry(cert.publicKey); return { privateKey: cert.privateKey, publicKey: cert.publicKey, expiresAt }; } finally { // Always close the client await typedsocketClient.stop(); } } catch (error) { console.error('Certificate provisioning failed:', error); throw error; } } /** * Get authentication token for certificate service */ private async getAuthToken(): Promise { // Implementation would depend on authentication mechanism // This is a simplified example assuming the platform service has an auth method try { // For now, return a placeholder token - in production this would // authenticate properly with the certificate service return 'mta-service-auth-token'; } catch (error) { console.error('Failed to obtain auth token:', error); return null; } } /** * Extract certificate expiry date from public key */ private getCertificateExpiry(publicKey: string): Date { try { // This is a simplified implementation // In a real system, you would parse the certificate properly // using a certificate parsing library // For now, set expiry to 90 days from now const expiresAt = new Date(); expiresAt.setDate(expiresAt.getDate() + 90); return expiresAt; } catch (error) { console.error('Failed to extract certificate expiry:', error); // Default to 30 days from now const defaultExpiry = new Date(); defaultExpiry.setDate(defaultExpiry.getDate() + 30); return defaultExpiry; } } /** * Set up certificate auto-renewal */ private setupCertificateRenewal(): void { if (!this.certificate || !this.certificate.expiresAt) { console.warn('Cannot setup certificate renewal: no valid certificate'); return; } // Calculate time until renewal (30 days before expiry) const now = new Date(); const renewalDate = new Date(this.certificate.expiresAt); renewalDate.setDate(renewalDate.getDate() - 30); const timeUntilRenewal = Math.max(0, renewalDate.getTime() - now.getTime()); console.log(`Certificate renewal scheduled for ${renewalDate}`); // Schedule renewal setTimeout(() => { this.renewCertificate().catch(error => { console.error('Certificate renewal failed:', error); }); }, timeUntilRenewal); } /** * Renew the certificate */ private async renewCertificate(): Promise { try { console.log('Renewing certificate...'); // Provision new certificate const newCertificate = await this.provisionCertificate(this.config.tls.domain); // Replace current certificate this.certificate = newCertificate; console.log(`Certificate renewed, new expiry: ${newCertificate.expiresAt}`); // Update SMTP server with new certificate if running if (this.server) { // Restart server with new certificate await this.server.stop(); const smtpOptions: ISmtpServerOptions = { port: this.config.smtp.port, key: this.certificate.privateKey, cert: this.certificate.publicKey, hostname: this.config.smtp.hostname }; this.server = new SMTPServer(this, smtpOptions); this.server.start(); console.log('SMTP server restarted with new certificate'); } // Schedule next renewal this.setupCertificateRenewal(); } catch (error) { console.error('Certificate renewal failed:', error); // Schedule retry after 24 hours setTimeout(() => { this.renewCertificate().catch(err => { console.error('Certificate renewal retry failed:', err); }); }, 24 * 60 * 60 * 1000); } } /** * Update DNS records for all local domains */ private async updateDnsRecordsForLocalDomains(): Promise { if (!this.config.domains.local || this.config.domains.local.length === 0) { return; } console.log('Updating DNS records for local domains...'); for (const domain of this.config.domains.local) { try { console.log(`Updating DNS records for ${domain}`); // Generate DKIM keys if needed await this.dkimCreator.handleDKIMKeysForDomain(domain); // Generate all recommended DNS records const records = await this.dnsManager.generateAllRecommendedRecords(domain); console.log(`Generated ${records.length} DNS records for ${domain}`); } catch (error) { console.error(`Error updating DNS records for ${domain}:`, error); } } } /** * Validate an email before sending */ private validateEmail(email: Email): void { // The Email class constructor already performs basic validation // Here we can add additional MTA-specific validation if (!email.from) { throw new Error('Email must have a sender address'); } if (!email.to || email.to.length === 0) { throw new Error('Email must have at least one recipient'); } // Check if the sender domain is allowed const senderDomain = email.getFromDomain(); if (!senderDomain) { throw new Error('Invalid sender domain'); } // If the sender domain is one of our local domains, ensure we have DKIM keys if (this.isLocalDomain(senderDomain) && this.config.security.useDkim) { // DKIM keys will be created if needed in the send method } } /** * Get MTA service statistics */ public getStats(): MtaStats { // Update queue size this.stats.queueSize = this.emailQueue.size; // Update certificate info if available if (this.certificate) { const now = new Date(); const daysUntilExpiry = Math.floor( (this.certificate.expiresAt.getTime() - now.getTime()) / (24 * 60 * 60 * 1000) ); this.stats.certificateInfo = { domain: this.config.tls.domain, expiresAt: this.certificate.expiresAt, daysUntilExpiry }; } return { ...this.stats }; } }