feat(mta): Enhance MTA service and SMTP server with robust session management, advanced email handling, and integrated API routes
This commit is contained in:
@ -2,35 +2,218 @@ export interface IAttachment {
|
||||
filename: string;
|
||||
content: Buffer;
|
||||
contentType: string;
|
||||
contentId?: string; // Optional content ID for inline attachments
|
||||
encoding?: string; // Optional encoding specification
|
||||
}
|
||||
|
||||
export interface IEmailOptions {
|
||||
from: string;
|
||||
to: string;
|
||||
to: string | string[]; // Support multiple recipients
|
||||
cc?: string | string[]; // Optional CC recipients
|
||||
bcc?: string | string[]; // Optional BCC recipients
|
||||
subject: string;
|
||||
text: string;
|
||||
attachments: IAttachment[];
|
||||
html?: string; // Optional HTML version
|
||||
attachments?: IAttachment[];
|
||||
headers?: Record<string, string>; // Optional additional headers
|
||||
mightBeSpam?: boolean;
|
||||
priority?: 'high' | 'normal' | 'low'; // Optional email priority
|
||||
}
|
||||
|
||||
export class Email {
|
||||
from: string;
|
||||
to: string;
|
||||
to: string[];
|
||||
cc: string[];
|
||||
bcc: string[];
|
||||
subject: string;
|
||||
text: string;
|
||||
html?: string;
|
||||
attachments: IAttachment[];
|
||||
headers: Record<string, string>;
|
||||
mightBeSpam: boolean;
|
||||
priority: 'high' | 'normal' | 'low';
|
||||
|
||||
constructor(options: IEmailOptions) {
|
||||
// Validate and set the from address
|
||||
if (!this.isValidEmail(options.from)) {
|
||||
throw new Error(`Invalid sender email address: ${options.from}`);
|
||||
}
|
||||
this.from = options.from;
|
||||
this.to = options.to;
|
||||
this.subject = options.subject;
|
||||
this.text = options.text;
|
||||
this.attachments = options.attachments;
|
||||
|
||||
// Handle to addresses (single or multiple)
|
||||
this.to = this.parseRecipients(options.to);
|
||||
|
||||
// Handle optional cc and bcc
|
||||
this.cc = options.cc ? this.parseRecipients(options.cc) : [];
|
||||
this.bcc = options.bcc ? this.parseRecipients(options.bcc) : [];
|
||||
|
||||
// Validate that we have at least one recipient
|
||||
if (this.to.length === 0 && this.cc.length === 0 && this.bcc.length === 0) {
|
||||
throw new Error('Email must have at least one recipient');
|
||||
}
|
||||
|
||||
// Set subject with sanitization
|
||||
this.subject = this.sanitizeString(options.subject || '');
|
||||
|
||||
// Set text content with sanitization
|
||||
this.text = this.sanitizeString(options.text || '');
|
||||
|
||||
// Set optional HTML content
|
||||
this.html = options.html ? this.sanitizeString(options.html) : undefined;
|
||||
|
||||
// Set attachments
|
||||
this.attachments = Array.isArray(options.attachments) ? options.attachments : [];
|
||||
|
||||
// Set additional headers
|
||||
this.headers = options.headers || {};
|
||||
|
||||
// Set spam flag
|
||||
this.mightBeSpam = options.mightBeSpam || false;
|
||||
|
||||
// Set priority
|
||||
this.priority = options.priority || 'normal';
|
||||
}
|
||||
|
||||
public getFromDomain() {
|
||||
return this.from.split('@')[1]
|
||||
/**
|
||||
* Validates an email address using a regex pattern
|
||||
* @param email The email address to validate
|
||||
* @returns boolean indicating if the email is valid
|
||||
*/
|
||||
private isValidEmail(email: string): boolean {
|
||||
if (!email || typeof email !== 'string') return false;
|
||||
|
||||
// Basic but effective email regex
|
||||
const emailRegex = /^[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses and validates recipient email addresses
|
||||
* @param recipients A string or array of recipient emails
|
||||
* @returns Array of validated email addresses
|
||||
*/
|
||||
private parseRecipients(recipients: string | string[]): string[] {
|
||||
const result: string[] = [];
|
||||
|
||||
if (typeof recipients === 'string') {
|
||||
// Handle single recipient
|
||||
if (this.isValidEmail(recipients)) {
|
||||
result.push(recipients);
|
||||
} else {
|
||||
throw new Error(`Invalid recipient email address: ${recipients}`);
|
||||
}
|
||||
} else if (Array.isArray(recipients)) {
|
||||
// Handle multiple recipients
|
||||
for (const recipient of recipients) {
|
||||
if (this.isValidEmail(recipient)) {
|
||||
result.push(recipient);
|
||||
} else {
|
||||
throw new Error(`Invalid recipient email address: ${recipient}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic sanitization for strings to prevent header injection
|
||||
* @param input The string to sanitize
|
||||
* @returns Sanitized string
|
||||
*/
|
||||
private sanitizeString(input: string): string {
|
||||
if (!input) return '';
|
||||
|
||||
// Remove CR and LF characters to prevent header injection
|
||||
return input.replace(/\r|\n/g, ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the domain part of the from email address
|
||||
* @returns The domain part of the from email or null if invalid
|
||||
*/
|
||||
public getFromDomain(): string | null {
|
||||
try {
|
||||
const parts = this.from.split('@');
|
||||
if (parts.length !== 2 || !parts[1]) {
|
||||
return null;
|
||||
}
|
||||
return parts[1];
|
||||
} catch (error) {
|
||||
console.error('Error extracting domain from email:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all recipients (to, cc, bcc) as a unique array
|
||||
* @returns Array of all unique recipient email addresses
|
||||
*/
|
||||
public getAllRecipients(): string[] {
|
||||
// Combine all recipients and remove duplicates
|
||||
return [...new Set([...this.to, ...this.cc, ...this.bcc])];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets primary recipient (first in the to field)
|
||||
* @returns The primary recipient email or null if none exists
|
||||
*/
|
||||
public getPrimaryRecipient(): string | null {
|
||||
return this.to.length > 0 ? this.to[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the email has attachments
|
||||
* @returns Boolean indicating if the email has attachments
|
||||
*/
|
||||
public hasAttachments(): boolean {
|
||||
return this.attachments.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the total size of all attachments in bytes
|
||||
* @returns Total size of all attachments in bytes
|
||||
*/
|
||||
public getAttachmentsSize(): number {
|
||||
return this.attachments.reduce((total, attachment) => {
|
||||
return total + (attachment.content?.length || 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an RFC822 compliant email string
|
||||
* @returns The email formatted as an RFC822 compliant string
|
||||
*/
|
||||
public toRFC822String(): string {
|
||||
// This is a simplified version - a complete implementation would be more complex
|
||||
let result = '';
|
||||
|
||||
// Add headers
|
||||
result += `From: ${this.from}\r\n`;
|
||||
result += `To: ${this.to.join(', ')}\r\n`;
|
||||
|
||||
if (this.cc.length > 0) {
|
||||
result += `Cc: ${this.cc.join(', ')}\r\n`;
|
||||
}
|
||||
|
||||
result += `Subject: ${this.subject}\r\n`;
|
||||
result += `Date: ${new Date().toUTCString()}\r\n`;
|
||||
|
||||
// Add custom headers
|
||||
for (const [key, value] of Object.entries(this.headers)) {
|
||||
result += `${key}: ${value}\r\n`;
|
||||
}
|
||||
|
||||
// Add priority if not normal
|
||||
if (this.priority !== 'normal') {
|
||||
const priorityValue = this.priority === 'high' ? '1' : '5';
|
||||
result += `X-Priority: ${priorityValue}\r\n`;
|
||||
}
|
||||
|
||||
// Add content type and body
|
||||
result += `Content-Type: text/plain; charset=utf-8\r\n`;
|
||||
result += `\r\n${this.text}\r\n`;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user