import * as plugins from '../plugins.js'; import { EmailService } from './classes.emailservice.js'; import { logger } from '../logger.js'; // Import MTA classes import { MtaService, Email as MtaEmail, type IEmailOptions, DeliveryStatus, type IAttachment } from '../mta/index.js'; export class MtaConnector { public emailRef: EmailService; private mtaService: MtaService; constructor(emailRefArg: EmailService, mtaService?: MtaService) { this.emailRef = emailRefArg; this.mtaService = mtaService || this.emailRef.mtaService; } /** * Send an email using the MTA service * @param smartmail The email to send * @param toAddresses Recipients (comma-separated or array) * @param options Additional options */ public async sendEmail( smartmail: plugins.smartmail.Smartmail, toAddresses: string | string[], options: any = {} ): Promise { try { // Process recipients const toArray = Array.isArray(toAddresses) ? toAddresses : toAddresses.split(',').map(addr => addr.trim()); // Add recipients to smartmail if they're not already added if (!smartmail.options.to || smartmail.options.to.length === 0) { for (const recipient of toArray) { smartmail.addRecipient(recipient); } } // Handle options const emailOptions: Record = { ...options }; // Check if we should use MIME format const useMimeFormat = options.useMimeFormat ?? true; if (useMimeFormat) { // Use smartmail's MIME conversion for improved handling try { // Convert to MIME format const mimeEmail = await smartmail.toMimeFormat(smartmail.options.creationObjectRef); // Parse the MIME email to create an MTA Email return this.sendMimeEmail(mimeEmail, toArray); } catch (mimeError) { logger.log('warn', `Failed to use MIME format, falling back to direct conversion: ${mimeError.message}`); // Fall back to direct conversion return this.sendDirectEmail(smartmail, toArray); } } else { // Use direct conversion return this.sendDirectEmail(smartmail, toArray); } } catch (error) { logger.log('error', `Failed to send email via MTA: ${error.message}`, { eventType: 'emailError', provider: 'mta', error: error.message }); throw error; } } /** * Send a MIME-formatted email * @param mimeEmail The MIME-formatted email content * @param recipients The email recipients */ private async sendMimeEmail(mimeEmail: string, recipients: string[]): Promise { try { // Parse the MIME email const parsedEmail = await plugins.mailparser.simpleParser(mimeEmail); // Extract necessary information for MTA Email const mtaEmail = new MtaEmail({ from: parsedEmail.from?.text || '', to: recipients, subject: parsedEmail.subject || '', text: parsedEmail.text || '', html: parsedEmail.html || undefined, attachments: parsedEmail.attachments?.map(attachment => ({ filename: attachment.filename || 'attachment', content: attachment.content, contentType: attachment.contentType || 'application/octet-stream', contentId: attachment.contentId })) || [], headers: Object.fromEntries([...parsedEmail.headers].map(([key, value]) => [key, String(value)])) }); // Send using MTA const emailId = await this.mtaService.send(mtaEmail); logger.log('info', `MIME email sent via MTA to ${recipients.join(', ')}`, { eventType: 'sentEmail', provider: 'mta', emailId, to: recipients }); return emailId; } catch (error) { logger.log('error', `Failed to send MIME email: ${error.message}`); throw error; } } /** * Send an email using direct conversion (fallback method) * @param smartmail The Smartmail instance * @param recipients The email recipients */ private async sendDirectEmail( smartmail: plugins.smartmail.Smartmail, recipients: string[] ): Promise { // Map SmartMail attachments to MTA attachments with improved content type handling const attachments: IAttachment[] = smartmail.attachments.map(attachment => { // Try to determine content type from file extension if not explicitly set let contentType = (attachment as any)?.contentType; if (!contentType) { const extension = attachment.parsedPath.ext.toLowerCase(); contentType = this.getContentTypeFromExtension(extension); } return { filename: attachment.parsedPath.base, content: Buffer.from(attachment.contentBuffer), contentType: contentType || 'application/octet-stream', // Add content ID for inline images if available contentId: (attachment as any)?.contentId }; }); // Create MTA Email const mtaEmail = new MtaEmail({ from: smartmail.options.from, to: recipients, subject: smartmail.getSubject(), text: smartmail.getBody(false), // Plain text version html: smartmail.getBody(true), // HTML version attachments }); // Prepare arrays for CC and BCC recipients let ccRecipients: string[] = []; let bccRecipients: string[] = []; // Add CC recipients if present if (smartmail.options.cc?.length > 0) { // Handle CC recipients - smartmail options may contain email objects ccRecipients = smartmail.options.cc.map(r => { if (typeof r === 'string') return r; return typeof (r as any).address === 'string' ? (r as any).address : typeof (r as any).email === 'string' ? (r as any).email : ''; }); mtaEmail.cc = ccRecipients; } // Add BCC recipients if present if (smartmail.options.bcc?.length > 0) { // Handle BCC recipients - smartmail options may contain email objects bccRecipients = smartmail.options.bcc.map(r => { if (typeof r === 'string') return r; return typeof (r as any).address === 'string' ? (r as any).address : typeof (r as any).email === 'string' ? (r as any).email : ''; }); mtaEmail.bcc = bccRecipients; } // Send using MTA const emailId = await this.mtaService.send(mtaEmail); logger.log('info', `Email sent via MTA to ${recipients.join(', ')}`, { eventType: 'sentEmail', provider: 'mta', emailId, to: recipients }); return emailId; } /** * Get content type from file extension * @param extension The file extension (with or without dot) * @returns The content type or undefined if unknown */ private getContentTypeFromExtension(extension: string): string | undefined { // Remove dot if present const ext = extension.startsWith('.') ? extension.substring(1) : extension; // Common content types const contentTypes: Record = { 'pdf': 'application/pdf', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'svg': 'image/svg+xml', 'webp': 'image/webp', 'txt': 'text/plain', 'html': 'text/html', 'csv': 'text/csv', 'json': 'application/json', 'xml': 'application/xml', 'zip': 'application/zip', 'doc': 'application/msword', 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls': 'application/vnd.ms-excel', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt': 'application/vnd.ms-powerpoint', 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation' }; return contentTypes[ext.toLowerCase()]; } /** * Retrieve and process an incoming email * For MTA, this would handle an email already received by the SMTP server * @param emailData The raw email data or identifier * @param options Additional processing options */ public async receiveEmail( emailData: string, options: { preserveHeaders?: boolean; includeRawData?: boolean; validateSender?: boolean; } = {} ): Promise> { try { // In a real implementation, this would retrieve an email from the MTA storage // For now, we can use a simplified approach: // Parse the email (assuming emailData is a raw email or a file path) const parsedEmail = await plugins.mailparser.simpleParser(emailData); // Extract sender information const sender = parsedEmail.from?.text || ''; let senderName = ''; let senderEmail = sender; // Try to extract name and email from "Name " format const senderMatch = sender.match(/(.*?)\s*<([^>]+)>/); if (senderMatch) { senderName = senderMatch[1].trim(); senderEmail = senderMatch[2].trim(); } // Extract recipients const recipients = []; if (parsedEmail.to) { // Extract recipients safely try { // Handle AddressObject or AddressObject[] if (parsedEmail.to && typeof parsedEmail.to === 'object' && 'value' in parsedEmail.to) { const addressList = Array.isArray(parsedEmail.to.value) ? parsedEmail.to.value : [parsedEmail.to.value]; for (const addr of addressList) { if (addr && typeof addr === 'object' && 'address' in addr) { recipients.push({ name: addr.name || '', email: addr.address || '' }); } } } } catch (error) { // If parsing fails, try to extract as string let toStr = ''; if (parsedEmail.to && typeof parsedEmail.to === 'object' && 'text' in parsedEmail.to) { toStr = String(parsedEmail.to.text || ''); } if (toStr) { recipients.push({ name: '', email: toStr }); } } } // Create a more comprehensive creation object reference const creationObjectRef: Record = { sender: { name: senderName, email: senderEmail }, recipients: recipients, subject: parsedEmail.subject || '', date: parsedEmail.date || new Date(), messageId: parsedEmail.messageId || '', inReplyTo: parsedEmail.inReplyTo || null, references: parsedEmail.references || [] }; // Include headers if requested if (options.preserveHeaders) { creationObjectRef.headers = parsedEmail.headers; } // Include raw data if requested if (options.includeRawData) { creationObjectRef.rawData = emailData; } // Create a Smartmail from the parsed email const smartmail = new plugins.smartmail.Smartmail({ from: senderEmail, subject: parsedEmail.subject || '', body: parsedEmail.html || parsedEmail.text || '', creationObjectRef }); // Add recipients if (recipients.length > 0) { for (const recipient of recipients) { smartmail.addRecipient(recipient.email); } } // Add CC recipients if present if (parsedEmail.cc) { try { // Extract CC recipients safely if (parsedEmail.cc && typeof parsedEmail.cc === 'object' && 'value' in parsedEmail.cc) { const ccList = Array.isArray(parsedEmail.cc.value) ? parsedEmail.cc.value : [parsedEmail.cc.value]; for (const addr of ccList) { if (addr && typeof addr === 'object' && 'address' in addr) { smartmail.addRecipient(addr.address, 'cc'); } } } } catch (error) { // If parsing fails, try to extract as string let ccStr = ''; if (parsedEmail.cc && typeof parsedEmail.cc === 'object' && 'text' in parsedEmail.cc) { ccStr = String(parsedEmail.cc.text || ''); } if (ccStr) { smartmail.addRecipient(ccStr, 'cc'); } } } // Add BCC recipients if present (usually not in received emails, but just in case) if (parsedEmail.bcc) { try { // Extract BCC recipients safely if (parsedEmail.bcc && typeof parsedEmail.bcc === 'object' && 'value' in parsedEmail.bcc) { const bccList = Array.isArray(parsedEmail.bcc.value) ? parsedEmail.bcc.value : [parsedEmail.bcc.value]; for (const addr of bccList) { if (addr && typeof addr === 'object' && 'address' in addr) { smartmail.addRecipient(addr.address, 'bcc'); } } } } catch (error) { // If parsing fails, try to extract as string let bccStr = ''; if (parsedEmail.bcc && typeof parsedEmail.bcc === 'object' && 'text' in parsedEmail.bcc) { bccStr = String(parsedEmail.bcc.text || ''); } if (bccStr) { smartmail.addRecipient(bccStr, 'bcc'); } } } // Add attachments if present if (parsedEmail.attachments && parsedEmail.attachments.length > 0) { for (const attachment of parsedEmail.attachments) { // Create smartfile with proper constructor options const file = new plugins.smartfile.SmartFile({ path: attachment.filename || 'attachment', contentBuffer: attachment.content, base: '' }); // Set content type and content ID for proper MIME handling if (attachment.contentType) { (file as any).contentType = attachment.contentType; } if (attachment.contentId) { (file as any).contentId = attachment.contentId; } smartmail.addAttachment(file); } } // Validate sender if requested if (options.validateSender && this.emailRef.emailValidator) { try { const validationResult = await this.emailRef.emailValidator.validate(senderEmail, { checkSyntaxOnly: true // Use syntax-only for performance }); // Add validation info to the creation object creationObjectRef.senderValidation = validationResult; } catch (validationError) { logger.log('warn', `Sender validation error: ${validationError.message}`); } } return smartmail; } catch (error) { logger.log('error', `Failed to receive email via MTA: ${error.message}`, { eventType: 'emailError', provider: 'mta', error: error.message }); throw error; } } /** * Check the status of a sent email * @param emailId The email ID to check */ public async checkEmailStatus(emailId: string): Promise<{ status: string; details?: any; }> { try { const status = this.mtaService.getEmailStatus(emailId); if (!status) { return { status: 'unknown', details: { message: 'Email not found' } }; } return { status: status.status, details: { attempts: status.attempts, lastAttempt: status.lastAttempt, nextAttempt: status.nextAttempt, error: status.error?.message } }; } catch (error) { logger.log('error', `Failed to check email status: ${error.message}`, { eventType: 'emailError', provider: 'mta', emailId, error: error.message }); return { status: 'error', details: { message: error.message } }; } } }