2025-05-08 01:13:54 +00:00
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
import { EmailService } from '../services/classes.emailservice.js';
|
|
|
|
import { logger } from '../../logger.js';
|
2025-03-15 16:04:03 +00:00
|
|
|
|
|
|
|
// Import MTA classes
|
2025-05-08 01:13:54 +00:00
|
|
|
import { MtaService } from './classes.mta.js';
|
|
|
|
import { Email as MtaEmail } from '../core/classes.email.js';
|
2025-05-08 10:39:43 +00:00
|
|
|
import { DeliveryStatus } from './classes.emailsendjob.js';
|
|
|
|
|
|
|
|
// Re-export for use in index.ts
|
|
|
|
export { DeliveryStatus };
|
2025-05-08 01:13:54 +00:00
|
|
|
|
|
|
|
// Import Email types
|
|
|
|
export interface IEmailOptions {
|
|
|
|
from: string;
|
|
|
|
to: string[];
|
|
|
|
cc?: string[];
|
|
|
|
bcc?: string[];
|
|
|
|
subject: string;
|
|
|
|
text?: string;
|
|
|
|
html?: string;
|
|
|
|
attachments?: IAttachment[];
|
|
|
|
headers?: { [key: string]: string };
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Reuse the IAttachment interface
|
|
|
|
export interface IAttachment {
|
|
|
|
filename: string;
|
|
|
|
content: Buffer;
|
|
|
|
contentType: string;
|
|
|
|
contentId?: string;
|
|
|
|
encoding?: string;
|
|
|
|
}
|
2025-03-15 16:04:03 +00:00
|
|
|
|
2025-05-08 10:39:43 +00:00
|
|
|
/**
|
|
|
|
* Email status details
|
|
|
|
*/
|
|
|
|
export interface IEmailStatusDetails {
|
|
|
|
/** Number of delivery attempts */
|
|
|
|
attempts?: number;
|
|
|
|
/** Timestamp of last delivery attempt */
|
|
|
|
lastAttempt?: Date;
|
|
|
|
/** Timestamp of next scheduled attempt */
|
|
|
|
nextAttempt?: Date;
|
|
|
|
/** Error message if delivery failed */
|
|
|
|
error?: string;
|
|
|
|
/** Message explaining the status */
|
|
|
|
message?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Email status response
|
|
|
|
*/
|
|
|
|
export interface IEmailStatusResponse {
|
|
|
|
/** Current status of the email */
|
|
|
|
status: DeliveryStatus | 'unknown' | 'error';
|
|
|
|
/** Additional status details */
|
|
|
|
details?: IEmailStatusDetails;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Options for sending an email via MTA
|
|
|
|
*/
|
|
|
|
export interface ISendEmailOptions {
|
|
|
|
/** Whether to use MIME format conversion */
|
|
|
|
useMimeFormat?: boolean;
|
|
|
|
/** Whether to track clicks */
|
|
|
|
trackClicks?: boolean;
|
|
|
|
/** Whether to track opens */
|
|
|
|
trackOpens?: boolean;
|
|
|
|
/** Message priority (1-5, where 1 is highest) */
|
|
|
|
priority?: number;
|
|
|
|
/** Message scheduling options */
|
|
|
|
schedule?: {
|
|
|
|
/** Time to send the email */
|
|
|
|
sendAt?: Date | string;
|
|
|
|
/** Time the message expires */
|
|
|
|
expireAt?: Date | string;
|
|
|
|
};
|
|
|
|
/** DKIM signing options */
|
|
|
|
dkim?: {
|
|
|
|
/** Whether to sign the message */
|
|
|
|
sign?: boolean;
|
|
|
|
/** Domain to use for signing */
|
|
|
|
domain?: string;
|
|
|
|
/** Key selector to use */
|
|
|
|
selector?: string;
|
|
|
|
};
|
|
|
|
/** Additional headers */
|
|
|
|
headers?: Record<string, string>;
|
|
|
|
/** Message tags for categorization */
|
|
|
|
tags?: string[];
|
|
|
|
}
|
|
|
|
|
2025-03-15 16:04:03 +00:00
|
|
|
export class MtaConnector {
|
|
|
|
public emailRef: EmailService;
|
|
|
|
private mtaService: MtaService;
|
|
|
|
|
|
|
|
constructor(emailRefArg: EmailService, mtaService?: MtaService) {
|
|
|
|
this.emailRef = emailRefArg;
|
|
|
|
this.mtaService = mtaService || this.emailRef.mtaService;
|
|
|
|
}
|
|
|
|
|
2025-05-08 10:39:43 +00:00
|
|
|
/**
|
|
|
|
* Send an email using the MTA service
|
|
|
|
* @param smartmail The email to send
|
|
|
|
* @param toAddresses Recipients (comma-separated or array)
|
|
|
|
* @param options Additional options
|
|
|
|
*/
|
|
|
|
|
2025-03-15 16:04:03 +00:00
|
|
|
/**
|
|
|
|
* 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(
|
2025-05-07 17:41:04 +00:00
|
|
|
smartmail: plugins.smartmail.Smartmail<any>,
|
2025-03-15 16:04:03 +00:00
|
|
|
toAddresses: string | string[],
|
2025-05-08 10:39:43 +00:00
|
|
|
options: ISendEmailOptions = {}
|
2025-03-15 16:04:03 +00:00
|
|
|
): Promise<string> {
|
2025-05-07 20:20:17 +00:00
|
|
|
// Check if recipients are on the suppression list
|
|
|
|
const recipients = Array.isArray(toAddresses)
|
|
|
|
? toAddresses
|
|
|
|
: toAddresses.split(',').map(addr => addr.trim());
|
|
|
|
|
|
|
|
// Filter out suppressed recipients
|
|
|
|
const validRecipients = [];
|
|
|
|
const suppressedRecipients = [];
|
|
|
|
|
|
|
|
for (const recipient of recipients) {
|
|
|
|
if (this.emailRef.bounceManager.isEmailSuppressed(recipient)) {
|
|
|
|
suppressedRecipients.push(recipient);
|
|
|
|
} else {
|
|
|
|
validRecipients.push(recipient);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Log suppressed recipients
|
|
|
|
if (suppressedRecipients.length > 0) {
|
|
|
|
logger.log('warn', `Skipping ${suppressedRecipients.length} suppressed recipients`, {
|
|
|
|
suppressedRecipients
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// If all recipients are suppressed, throw error
|
|
|
|
if (validRecipients.length === 0) {
|
|
|
|
throw new Error('All recipients are on the suppression list');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Continue with valid recipients
|
2025-03-15 16:04:03 +00:00
|
|
|
try {
|
2025-05-07 20:20:17 +00:00
|
|
|
// Use filtered recipients - already an array, no need for toArray
|
2025-03-15 16:04:03 +00:00
|
|
|
|
2025-05-07 17:41:04 +00:00
|
|
|
// Add recipients to smartmail if they're not already added
|
|
|
|
if (!smartmail.options.to || smartmail.options.to.length === 0) {
|
2025-05-07 20:20:17 +00:00
|
|
|
for (const recipient of validRecipients) {
|
2025-05-07 17:41:04 +00:00
|
|
|
smartmail.addRecipient(recipient);
|
|
|
|
}
|
|
|
|
}
|
2025-03-15 16:04:03 +00:00
|
|
|
|
2025-05-07 17:41:04 +00:00
|
|
|
// Handle options
|
|
|
|
const emailOptions: Record<string, any> = { ...options };
|
|
|
|
|
|
|
|
// Check if we should use MIME format
|
2025-05-08 10:39:43 +00:00
|
|
|
const useMimeFormat = options.useMimeFormat !== false; // Default to true
|
2025-05-07 17:41:04 +00:00
|
|
|
|
|
|
|
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
|
2025-05-07 20:20:17 +00:00
|
|
|
return this.sendMimeEmail(mimeEmail, validRecipients);
|
2025-05-07 17:41:04 +00:00
|
|
|
} catch (mimeError) {
|
|
|
|
logger.log('warn', `Failed to use MIME format, falling back to direct conversion: ${mimeError.message}`);
|
|
|
|
// Fall back to direct conversion
|
2025-05-07 20:20:17 +00:00
|
|
|
return this.sendDirectEmail(smartmail, validRecipients);
|
2025-05-07 17:41:04 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Use direct conversion
|
2025-05-07 20:20:17 +00:00
|
|
|
return this.sendDirectEmail(smartmail, validRecipients);
|
2025-05-07 17:41:04 +00:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.log('error', `Failed to send email via MTA: ${error.message}`, {
|
|
|
|
eventType: 'emailError',
|
|
|
|
provider: 'mta',
|
|
|
|
error: error.message
|
|
|
|
});
|
2025-05-07 20:20:17 +00:00
|
|
|
|
|
|
|
// Check if this is a bounce-related error
|
|
|
|
if (error.message.includes('550') || // Rejected
|
|
|
|
error.message.includes('551') || // User not local
|
|
|
|
error.message.includes('552') || // Mailbox full
|
|
|
|
error.message.includes('553') || // Bad mailbox name
|
|
|
|
error.message.includes('554') || // Transaction failed
|
|
|
|
error.message.includes('does not exist') ||
|
|
|
|
error.message.includes('unknown user') ||
|
|
|
|
error.message.includes('invalid recipient')) {
|
|
|
|
|
|
|
|
// Process as a bounce
|
|
|
|
for (const recipient of validRecipients) {
|
|
|
|
await this.emailRef.bounceManager.processSmtpFailure(
|
|
|
|
recipient,
|
|
|
|
error.message,
|
|
|
|
{
|
|
|
|
sender: smartmail.options.from,
|
|
|
|
statusCode: error.message.match(/\b([45]\d{2})\b/) ? error.message.match(/\b([45]\d{2})\b/)[1] : undefined
|
|
|
|
}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-05-07 17:41:04 +00:00
|
|
|
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
|
2025-03-15 16:04:03 +00:00
|
|
|
const mtaEmail = new MtaEmail({
|
2025-05-07 17:41:04 +00:00
|
|
|
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)]))
|
2025-03-15 16:04:03 +00:00
|
|
|
});
|
2025-05-07 17:41:04 +00:00
|
|
|
|
2025-03-15 16:04:03 +00:00
|
|
|
// Send using MTA
|
|
|
|
const emailId = await this.mtaService.send(mtaEmail);
|
|
|
|
|
2025-05-07 17:41:04 +00:00
|
|
|
logger.log('info', `MIME email sent via MTA to ${recipients.join(', ')}`, {
|
2025-03-15 16:04:03 +00:00
|
|
|
eventType: 'sentEmail',
|
|
|
|
provider: 'mta',
|
|
|
|
emailId,
|
2025-05-07 17:41:04 +00:00
|
|
|
to: recipients
|
2025-03-15 16:04:03 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
return emailId;
|
|
|
|
} catch (error) {
|
2025-05-07 17:41:04 +00:00
|
|
|
logger.log('error', `Failed to send MIME email: ${error.message}`);
|
2025-03-15 16:04:03 +00:00
|
|
|
throw error;
|
|
|
|
}
|
|
|
|
}
|
2025-05-07 17:41:04 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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()];
|
|
|
|
}
|
2025-03-15 16:04:03 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2025-05-07 17:41:04 +00:00
|
|
|
* @param options Additional processing options
|
2025-03-15 16:04:03 +00:00
|
|
|
*/
|
2025-05-07 17:41:04 +00:00
|
|
|
public async receiveEmail(
|
|
|
|
emailData: string,
|
|
|
|
options: {
|
|
|
|
preserveHeaders?: boolean;
|
|
|
|
includeRawData?: boolean;
|
|
|
|
validateSender?: boolean;
|
|
|
|
} = {}
|
|
|
|
): Promise<plugins.smartmail.Smartmail<any>> {
|
2025-03-15 16:04:03 +00:00
|
|
|
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);
|
|
|
|
|
2025-05-07 17:41:04 +00:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2025-03-15 16:04:03 +00:00
|
|
|
// Create a Smartmail from the parsed email
|
|
|
|
const smartmail = new plugins.smartmail.Smartmail({
|
2025-05-07 17:41:04 +00:00
|
|
|
from: senderEmail,
|
2025-03-15 16:04:03 +00:00
|
|
|
subject: parsedEmail.subject || '',
|
|
|
|
body: parsedEmail.html || parsedEmail.text || '',
|
2025-05-07 17:41:04 +00:00
|
|
|
creationObjectRef
|
2025-03-15 16:04:03 +00:00
|
|
|
});
|
2025-05-07 17:41:04 +00:00
|
|
|
|
|
|
|
// 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');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-03-15 16:04:03 +00:00
|
|
|
|
|
|
|
// Add attachments if present
|
|
|
|
if (parsedEmail.attachments && parsedEmail.attachments.length > 0) {
|
|
|
|
for (const attachment of parsedEmail.attachments) {
|
2025-05-07 17:41:04 +00:00
|
|
|
// 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}`);
|
2025-03-15 16:04:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2025-05-08 10:39:43 +00:00
|
|
|
* @returns Current status and details
|
2025-03-15 16:04:03 +00:00
|
|
|
*/
|
2025-05-08 10:39:43 +00:00
|
|
|
public async checkEmailStatus(emailId: string): Promise<IEmailStatusResponse> {
|
2025-03-15 16:04:03 +00:00
|
|
|
try {
|
|
|
|
const status = this.mtaService.getEmailStatus(emailId);
|
|
|
|
|
|
|
|
if (!status) {
|
|
|
|
return {
|
2025-05-08 10:39:43 +00:00
|
|
|
status: 'unknown' as const,
|
2025-03-15 16:04:03 +00:00
|
|
|
details: { message: 'Email not found' }
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2025-05-08 10:39:43 +00:00
|
|
|
// Use type assertion to ensure this passes type check
|
|
|
|
status: status.status as DeliveryStatus,
|
2025-03-15 16:04:03 +00:00
|
|
|
details: {
|
|
|
|
attempts: status.attempts,
|
|
|
|
lastAttempt: status.lastAttempt,
|
|
|
|
nextAttempt: status.nextAttempt,
|
2025-05-08 10:39:43 +00:00
|
|
|
error: status.error?.message,
|
|
|
|
message: `Status: ${status.status}${status.error ? `, Error: ${status.error.message}` : ''}`
|
2025-03-15 16:04:03 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
} catch (error) {
|
|
|
|
logger.log('error', `Failed to check email status: ${error.message}`, {
|
|
|
|
eventType: 'emailError',
|
|
|
|
provider: 'mta',
|
|
|
|
emailId,
|
|
|
|
error: error.message
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
2025-05-08 10:39:43 +00:00
|
|
|
status: 'error' as const,
|
2025-03-15 16:04:03 +00:00
|
|
|
details: { message: error.message }
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|