485 lines
16 KiB
TypeScript
485 lines
16 KiB
TypeScript
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<any>,
|
|
toAddresses: string | string[],
|
|
options: any = {}
|
|
): Promise<string> {
|
|
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<string, any> = { ...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<string> {
|
|
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<any>,
|
|
recipients: string[]
|
|
): Promise<string> {
|
|
// 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<string, string> = {
|
|
'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<plugins.smartmail.Smartmail<any>> {
|
|
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 <email>" 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<string, any> = {
|
|
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 }
|
|
};
|
|
}
|
|
}
|
|
} |