import * as plugins from './smartmail.plugins.js'; import { EmailAddressValidator } from './smartmail.classes.emailaddressvalidator.js'; import type { IMailSendResponse } from './smartmail.wire.js'; import type { WireTarget } from './smartmail.classes.wiretarget.js'; export type EmailAddress = string; export type EmailAddressList = EmailAddress[]; export interface ISmartmailOptions { from: EmailAddress; to?: EmailAddressList; cc?: EmailAddressList; bcc?: EmailAddressList; replyTo?: EmailAddress; subject: string; body: string; htmlBody?: string; creationObjectRef?: T; headers?: Record; priority?: 'high' | 'normal' | 'low'; validateEmails?: boolean; } /** * JSON representation of an attachment for wire transmission */ export interface IAttachmentJson { filename: string; contentBase64: string; contentType: string; } /** * JSON representation of a Smartmail for wire transmission */ export interface ISmartmailJson { from: string; to?: string[]; cc?: string[]; bcc?: string[]; replyTo?: string; subject: string; body: string; htmlBody?: string; headers?: Record; priority?: 'high' | 'normal' | 'low'; creationObjectRef?: T; attachments: IAttachmentJson[]; } export interface IMimeAttachment { filename: string; content: Buffer; contentType: string; } /** * A standard representation for emails with advanced features */ export class Smartmail { public options: ISmartmailOptions; public attachments: plugins.smartfile.SmartFile[] = []; private emailValidator: EmailAddressValidator; constructor(optionsArg: ISmartmailOptions) { // Set default options this.options = { validateEmails: false, to: [], cc: [], bcc: [], headers: {}, priority: 'normal', ...optionsArg }; this.emailValidator = new EmailAddressValidator(); } /** * Adds an attachment to the email * @param smartfileArg The file to attach * @returns this for chaining */ public addAttachment(smartfileArg: plugins.smartfile.SmartFile): this { this.attachments.push(smartfileArg); return this; } /** * Gets the creation object reference * @returns The creation object reference */ public getCreationObject(): T { return this.options.creationObjectRef; } /** * Gets the processed subject with template variables applied * @param dataArg Data to apply to the template * @returns Processed subject */ public getSubject(dataArg: any = {}): string { const smartmustache = new plugins.smartmustache.SmartMustache(this.options.subject); return smartmustache.applyData(dataArg); } /** * Applies variables to all template strings in the email * @param variables Variables to apply to templates * @returns this for chaining */ public applyVariables(variables: Record): this { if (!variables || typeof variables !== 'object') { return this; } // Process the subject, body, and HTML body with the provided variables if (this.options.subject) { const subjectMustache = new plugins.smartmustache.SmartMustache(this.options.subject); this.options.subject = subjectMustache.applyData(variables); } if (this.options.body) { const bodyMustache = new plugins.smartmustache.SmartMustache(this.options.body); this.options.body = bodyMustache.applyData(variables); } if (this.options.htmlBody) { const htmlBodyMustache = new plugins.smartmustache.SmartMustache(this.options.htmlBody); this.options.htmlBody = htmlBodyMustache.applyData(variables); } return this; } /** * Gets the processed plain text body with template variables applied * @param dataArg Data to apply to the template * @returns Processed body */ public getBody(dataArg: any = {}): string { const smartmustache = new plugins.smartmustache.SmartMustache(this.options.body); return smartmustache.applyData(dataArg); } /** * Gets the processed HTML body with template variables applied * @param dataArg Data to apply to the template * @returns Processed HTML body or null if not set */ public getHtmlBody(dataArg: any = {}): string | null { if (!this.options.htmlBody) { return null; } const smartmustache = new plugins.smartmustache.SmartMustache(this.options.htmlBody); return smartmustache.applyData(dataArg); } /** * Adds a recipient to the email * @param email Email address to add * @param type Type of recipient (to, cc, bcc) * @returns this for chaining */ public addRecipient(email: EmailAddress, type: 'to' | 'cc' | 'bcc' = 'to'): this { if (!this.options[type]) { this.options[type] = []; } this.options[type]!.push(email); return this; } /** * Adds multiple recipients to the email * @param emails Email addresses to add * @param type Type of recipients (to, cc, bcc) * @returns this for chaining */ public addRecipients(emails: EmailAddressList, type: 'to' | 'cc' | 'bcc' = 'to'): this { if (!this.options[type]) { this.options[type] = []; } this.options[type] = [...this.options[type]!, ...emails]; return this; } /** * Sets the reply-to address * @param email Email address for reply-to * @returns this for chaining */ public setReplyTo(email: EmailAddress): this { this.options.replyTo = email; return this; } /** * Sets the priority of the email * @param priority Priority level * @returns this for chaining */ public setPriority(priority: 'high' | 'normal' | 'low'): this { this.options.priority = priority; return this; } /** * Adds a custom header to the email * @param name Header name * @param value Header value * @returns this for chaining */ public addHeader(name: string, value: string): this { if (!this.options.headers) { this.options.headers = {}; } this.options.headers[name] = value; return this; } /** * Validates all email addresses in the email * @returns Promise resolving to validation results */ public async validateAllEmails(): Promise> { const results: Record = {}; const emails: EmailAddress[] = []; // Collect all emails if (this.options.from) emails.push(this.options.from); if (this.options.replyTo) emails.push(this.options.replyTo); if (this.options.to) emails.push(...this.options.to); if (this.options.cc) emails.push(...this.options.cc); if (this.options.bcc) emails.push(...this.options.bcc); // Validate each email for (const email of emails) { const validationResult = await this.emailValidator.validate(email); results[email] = validationResult.valid; } return results; } // ========================================== // Wire Format Serialization Methods // ========================================== /** * Converts the email to a JSON-serializable object for wire transmission * @returns JSON-serializable object */ public toObject(): ISmartmailJson { const attachmentsJson: IAttachmentJson[] = this.attachments.map((file) => ({ filename: file.path.split('/').pop() || 'attachment', contentBase64: file.contentBuffer.toString('base64'), contentType: 'application/octet-stream', })); return { from: this.options.from, to: this.options.to, cc: this.options.cc, bcc: this.options.bcc, replyTo: this.options.replyTo, subject: this.options.subject, body: this.options.body, htmlBody: this.options.htmlBody, headers: this.options.headers, priority: this.options.priority, creationObjectRef: this.options.creationObjectRef, attachments: attachmentsJson, }; } /** * Serializes the email to a JSON string for wire transmission * @returns JSON string */ public toJson(): string { return JSON.stringify(this.toObject()); } /** * Creates a Smartmail instance from a JSON-serializable object * @param obj JSON object representing the email * @returns Smartmail instance */ public static fromObject(obj: ISmartmailJson): Smartmail { const email = new Smartmail({ from: obj.from, to: obj.to, cc: obj.cc, bcc: obj.bcc, replyTo: obj.replyTo, subject: obj.subject, body: obj.body, htmlBody: obj.htmlBody, headers: obj.headers, priority: obj.priority, creationObjectRef: obj.creationObjectRef, }); // Reconstruct attachments from base64 for (const att of obj.attachments || []) { const buffer = Buffer.from(att.contentBase64, 'base64'); const smartfile = new plugins.smartfile.SmartFile({ path: att.filename, contentBuffer: buffer, base: './', }); email.attachments.push(smartfile); } return email; } /** * Deserializes a Smartmail instance from a JSON string * @param json JSON string representing the email * @returns Smartmail instance */ public static fromJson(json: string): Smartmail { const obj = JSON.parse(json) as ISmartmailJson; return Smartmail.fromObject(obj); } /** * Send this email to a WireTarget * @param target The WireTarget to send the email to * @returns Promise resolving to the send response */ public async sendTo(target: WireTarget): Promise { return target.sendEmail(this); } // ========================================== // MIME Format Methods // ========================================== /** * Converts the email to a MIME format object for sending * @param dataArg Data to apply to templates * @returns MIME format object */ public async toMimeFormat(dataArg: any = {}): Promise { // Validate emails if option is enabled if (this.options.validateEmails) { const validationResults = await this.validateAllEmails(); const invalidEmails = Object.entries(validationResults) .filter(([_, valid]) => !valid) .map(([email]) => email); if (invalidEmails.length > 0) { throw new Error(`Invalid email addresses: ${invalidEmails.join(', ')}`); } } // Build MIME parts const subject = this.getSubject(dataArg); const textBody = this.getBody(dataArg); const htmlBody = this.getHtmlBody(dataArg); // Convert attachments to MIME format const mimeAttachments: IMimeAttachment[] = await Promise.all( this.attachments.map(async (file) => { return { filename: file.path.split('/').pop()!, content: file.contentBuffer, contentType: 'application/octet-stream' }; }) ); // Build email format object const mimeObj: any = { from: this.options.from, subject, text: textBody, attachments: mimeAttachments, headers: { ...this.options.headers } }; // Add optional fields if (this.options.to && this.options.to.length > 0) { mimeObj.to = this.options.to; } if (this.options.cc && this.options.cc.length > 0) { mimeObj.cc = this.options.cc; } if (this.options.bcc && this.options.bcc.length > 0) { mimeObj.bcc = this.options.bcc; } if (this.options.replyTo) { mimeObj.replyTo = this.options.replyTo; } if (htmlBody) { mimeObj.html = htmlBody; } // Add priority headers if specified if (this.options.priority === 'high') { mimeObj.headers['X-Priority'] = '1'; mimeObj.headers['X-MSMail-Priority'] = 'High'; mimeObj.headers['Importance'] = 'High'; } else if (this.options.priority === 'low') { mimeObj.headers['X-Priority'] = '5'; mimeObj.headers['X-MSMail-Priority'] = 'Low'; mimeObj.headers['Importance'] = 'Low'; } return mimeObj; } }