import * as plugins from '../../plugins.js'; import * as paths from '../../paths.js'; import { RuleManager } from '../core/classes.rulemanager.js'; import { ApiManager } from './classes.apimanager.js'; import { TemplateManager } from '../core/classes.templatemanager.js'; import { EmailValidator } from '../core/classes.emailvalidator.js'; import { BounceManager } from '../core/classes.bouncemanager.js'; import { logger } from '../../logger.js'; // Import types from router interfaces import { UnifiedEmailServer } from '../routing/classes.unified.email.server.js'; import { DomainRouter } from '../routing/classes.domain.router.js'; import { Email } from '../core/classes.email.js'; // Import configuration interfaces import type { IEmailConfig } from '../../config/email.config.js'; import { ConfigValidator, emailConfigSchema } from '../../config/index.js'; /** * Options for sending an email */ export interface ISendEmailOptions { /** Email sender override */ from?: string; /** Optional reply-to address */ replyTo?: string; /** CC recipients */ cc?: string | string[]; /** BCC recipients */ bcc?: string | string[]; /** Priority level */ priority?: 'high' | 'normal' | 'low'; /** Custom email headers */ headers?: Record; /** Whether to track opens */ trackOpens?: boolean; /** Whether to track clicks */ trackClicks?: boolean; /** Whether to skip suppression list check */ skipSuppressionCheck?: boolean; /** Specific IP to use for sending */ ipAddress?: string; /** Whether this is a transactional email */ isTransactional?: boolean; } /** * Template context data for email templates * @see ITemplateContext in TemplateManager */ export type ITemplateContext = import('../core/classes.templatemanager.js').ITemplateContext; /** * Validation options for email addresses * Compatible with EmailValidator.validate options */ export interface IValidateEmailOptions { /** Check MX records for the domain */ checkMx?: boolean; /** Check if the domain is disposable (temporary email) */ checkDisposable?: boolean; /** Check if the email is a role account (e.g., info@, support@) */ checkRole?: boolean; /** Only check syntax without DNS lookups */ checkSyntaxOnly?: boolean; } /** * Result of email validation * @see IEmailValidationResult from EmailValidator */ export type IValidationResult = import('../core/classes.emailvalidator.js').IEmailValidationResult; /** * Email service statistics */ export interface IEmailServiceStats { /** Active email providers */ activeProviders: string[]; /** MTA statistics, if MTA is active */ mta?: { /** Service start time */ startTime: Date; /** Total emails received */ emailsReceived: number; /** Total emails sent */ emailsSent: number; /** Total emails that failed to send */ emailsFailed: number; /** Active SMTP connections */ activeConnections: number; /** Current email queue size */ queueSize: number; /** Certificate information */ certificateInfo?: { /** Domain for the certificate */ domain: string; /** Certificate expiration date */ expiresAt: Date; /** Days until certificate expires */ daysUntilExpiry: number; }; /** IP warmup information */ warmupInfo?: { /** Whether IP warmup is enabled */ enabled: boolean; /** Number of active IPs */ activeIPs: number; /** Number of IPs in warmup phase */ inWarmupPhase: number; /** Number of IPs that completed warmup */ completedWarmup: number; }; /** Reputation monitoring information */ reputationInfo?: { /** Whether reputation monitoring is enabled */ enabled: boolean; /** Number of domains being monitored */ monitoredDomains: number; /** Average reputation score across domains */ averageScore: number; /** Number of domains with reputation issues */ domainsWithIssues: number; }; /** Rate limiting information */ rateLimiting?: { /** Global rate limit statistics */ global: { /** Current available tokens */ availableTokens: number; /** Maximum tokens per period */ maxTokens: number; /** Current consumption rate */ consumptionRate: number; /** Number of rate limiting events */ rateLimitEvents: number; }; }; }; } /** * Email service with MTA support */ export class EmailService { // typedrouter public typedrouter = new plugins.typedrequest.TypedRouter(); // environment public qenv = new plugins.qenv.Qenv('./', '.nogit/'); // unified email server public unifiedEmailServer: UnifiedEmailServer; public domainRouter: DomainRouter; // services public apiManager: ApiManager; public ruleManager: RuleManager; public templateManager: TemplateManager; public emailValidator: EmailValidator; public bounceManager: BounceManager; // configuration private config: IEmailConfig; constructor(options: IEmailConfig = {}) { // Validate and apply defaults to configuration const validationResult = ConfigValidator.validate(options, emailConfigSchema); if (!validationResult.valid) { logger.warn(`Email service configuration has validation errors: ${validationResult.errors.join(', ')}`); } // Set configuration with defaults this.config = validationResult.config; // Initialize validator this.emailValidator = new EmailValidator(); // Initialize bounce manager this.bounceManager = new BounceManager(); // Initialize template manager this.templateManager = new TemplateManager(this.config.templateConfig); if (this.config.useEmail) { // Initialize domain router for pattern matching this.domainRouter = new DomainRouter({ domainRules: this.config.domainRules || [], defaultMode: this.config.defaultMode || 'mta', defaultServer: this.config.defaultServer, defaultPort: this.config.defaultPort, defaultTls: this.config.defaultTls }); // Initialize UnifiedEmailServer const useInternalPorts = this.config.behindSmartProxy || false; const emailPorts = useInternalPorts ? this.config.ports.map(p => p + 10000) : // Use internal ports (10025, etc.) this.config.ports; // Use standard ports (25, etc.) // Pass null as dcRouter since this is a standalone service this.unifiedEmailServer = new UnifiedEmailServer(null as any, { ports: emailPorts, hostname: this.config.hostname || 'localhost', domains: [this.config.hostname || 'localhost'], // Default to hostname auth: this.config.auth, tls: this.config.tls, maxMessageSize: this.config.maxMessageSize, domainRules: this.config.domainRules || [], defaultMode: this.config.defaultMode || 'mta', defaultServer: this.config.defaultServer, defaultPort: this.config.defaultPort, defaultTls: this.config.defaultTls }); // Handle processed emails this.unifiedEmailServer.on('emailProcessed', (email, mode, rule) => { // Process email as needed (e.g., save to database, trigger notifications) logger.log('info', `Email processed: ${email.subject}`); }); } // Initialize API manager and rule manager this.apiManager = new ApiManager(this); this.ruleManager = new RuleManager(this); } /** * Start the email service */ public async start() { // Initialize rule manager await this.ruleManager.init(); // Load email templates if configured if (this.config.loadTemplatesFromDir) { try { await this.templateManager.loadTemplatesFromDirectory(paths.emailTemplatesDir); } catch (error) { logger.log('error', `Failed to load email templates: ${error.message}`); } } // Start UnifiedEmailServer if enabled if (this.config.useEmail && this.unifiedEmailServer) { await this.unifiedEmailServer.start(); logger.log('success', 'Started UnifiedEmailServer'); } logger.log('success', `Started email service`); } /** * Stop the email service */ public async stop() { // Stop UnifiedEmailServer if it's running if (this.config.useEmail && this.unifiedEmailServer) { await this.unifiedEmailServer.stop(); logger.log('info', 'Stopped UnifiedEmailServer'); } logger.log('info', 'Stopped email service'); } /** * Send an email using the UnifiedEmailServer * @param email The email to send * @param to Recipient(s) - if provided, overrides the email's 'to' field * @param options Additional options */ public async sendEmail( email: Email, to?: string | string[], options: ISendEmailOptions = {} ): Promise { if (this.config.useEmail && this.unifiedEmailServer) { // If 'to' is provided, update the email's recipients if (to) { const recipients = Array.isArray(to) ? to : [to]; email.to = recipients; } // Determine the domain for routing let matchedRule; const recipientDomain = email.to[0].split('@')[1]; if (recipientDomain && this.domainRouter) { matchedRule = this.domainRouter.matchRule(email.to[0]); } // Send through UnifiedEmailServer return this.unifiedEmailServer.sendEmail( email, matchedRule?.mode || 'mta', matchedRule ); } else { throw new Error('Email server not configured'); } } /** * Send an email using a template * @param templateId The template ID * @param to Recipient email(s) * @param context The template context data * @param options Additional options */ public async sendTemplateEmail( templateId: string, to: string | string[], context: ITemplateContext = {}, options: ISendEmailOptions = {} ): Promise { try { // Get email from template const email = await this.templateManager.prepareEmail(templateId, context); // Send the email through UnifiedEmailServer return this.sendEmail(email, to, options); } catch (error) { logger.log('error', `Failed to send template email: ${error.message}`, { templateId, to, error: error.message }); throw error; } } /** * Validate an email address * @param email The email address to validate * @param options Validation options * @returns Validation result */ public async validateEmail( email: string, options: IValidateEmailOptions = {} ): Promise { return this.emailValidator.validate(email, options); } /** * Get email service statistics * @returns Service statistics in the format expected by the API */ public getStats(): any { // First generate detailed internal stats const detailedStats: IEmailServiceStats = { activeProviders: [] }; if (this.config.useEmail && this.unifiedEmailServer) { detailedStats.activeProviders.push('unifiedEmail'); const serverStats = this.unifiedEmailServer.getStats(); detailedStats.mta = { startTime: serverStats.startTime, emailsReceived: serverStats.messages.processed, emailsSent: serverStats.messages.delivered, emailsFailed: serverStats.messages.failed, activeConnections: serverStats.connections.current, queueSize: 0 // Would need to be updated from deliveryQueue }; } // Convert detailed stats to the format expected by the API const apiStats: any = { totalEmailsSent: detailedStats.mta?.emailsSent || 0, totalEmailsDelivered: detailedStats.mta?.emailsSent || 0, // Default to emails sent if we don't track delivery separately totalEmailsBounced: detailedStats.mta?.emailsFailed || 0, averageDeliveryTimeMs: 0, // We don't track this yet lastUpdated: new Date().toISOString() }; return apiStats; } }