dcrouter/ts/mail/services/classes.emailservice.ts
2025-05-21 00:12:49 +00:00

397 lines
12 KiB
TypeScript

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 platform interfaces
import type { default as platformInterfaces } from '../../types/platform.interfaces.js';
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<string, string>;
/** 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 {
public platformServiceRef: any; // Reference to platform service
// 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(platformServiceRefArg: any, options: IEmailConfig = {}) {
this.platformServiceRef = platformServiceRefArg;
this.platformServiceRef.typedrouter.addTypedRouter(this.typedrouter);
// 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.)
this.unifiedEmailServer = new UnifiedEmailServer({
ports: emailPorts,
hostname: this.config.hostname || 'localhost',
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)
* @param options Additional options
*/
public async sendEmail(
email: plugins.smartmail.Smartmail<any>,
to: string | string[],
options: ISendEmailOptions = {}
): Promise<string> {
if (this.config.useEmail && this.unifiedEmailServer) {
// Convert Smartmail to Email format
const recipients = Array.isArray(to) ? to : [to];
// Access Smartmail properties using any type to bypass TypeScript checking
const emailAny = email as any;
const emailObj = new Email({
from: emailAny.from,
to: recipients,
subject: emailAny.subject,
text: emailAny.body || emailAny.text,
html: emailAny.htmlBody || emailAny.html,
attachments: emailAny.attachments ? emailAny.attachments.map((att: any) => ({
filename: att.filename,
content: att.contents || att.content,
contentType: att.contentType
})) : []
});
// Determine the domain for routing
let matchedRule;
const recipientDomain = recipients[0].split('@')[1];
if (recipientDomain && this.domainRouter) {
matchedRule = this.domainRouter.matchRule(recipients[0]);
}
// Send through UnifiedEmailServer
return this.unifiedEmailServer.sendEmail(
emailObj,
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<string> {
try {
// Get email from template
const smartmail = await this.templateManager.prepareEmail(templateId, context);
// Send the email through UnifiedEmailServer
return this.sendEmail(smartmail, 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<IValidationResult> {
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;
}
}