platformservice/ts/mta/mta.classes.email.ts

219 lines
6.4 KiB
TypeScript
Raw Normal View History

2024-02-16 13:28:40 +01:00
export interface IAttachment {
filename: string;
content: Buffer;
contentType: string;
contentId?: string; // Optional content ID for inline attachments
encoding?: string; // Optional encoding specification
2024-02-16 13:28:40 +01:00
}
export interface IEmailOptions {
from: string;
to: string | string[]; // Support multiple recipients
cc?: string | string[]; // Optional CC recipients
bcc?: string | string[]; // Optional BCC recipients
2024-02-16 13:28:40 +01:00
subject: string;
text: string;
html?: string; // Optional HTML version
attachments?: IAttachment[];
headers?: Record<string, string>; // Optional additional headers
2024-02-16 13:28:40 +01:00
mightBeSpam?: boolean;
priority?: 'high' | 'normal' | 'low'; // Optional email priority
2024-02-16 13:28:40 +01:00
}
export class Email {
from: string;
to: string[];
cc: string[];
bcc: string[];
2024-02-16 13:28:40 +01:00
subject: string;
text: string;
html?: string;
2024-02-16 13:28:40 +01:00
attachments: IAttachment[];
headers: Record<string, string>;
2024-02-16 13:28:40 +01:00
mightBeSpam: boolean;
priority: 'high' | 'normal' | 'low';
2024-02-16 13:28:40 +01:00
constructor(options: IEmailOptions) {
// Validate and set the from address
if (!this.isValidEmail(options.from)) {
throw new Error(`Invalid sender email address: ${options.from}`);
}
2024-02-16 13:28:40 +01:00
this.from = options.from;
// 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
2024-02-16 13:28:40 +01:00
this.mightBeSpam = options.mightBeSpam || false;
// Set priority
this.priority = options.priority || 'normal';
2024-02-16 13:28:40 +01:00
}
/**
* 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);
2024-02-16 13:28:40 +01:00
}
/**
* 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;
}
}