import { EmailConfig, EmailProvider, SendEmailOptions, EmailResult, EmailError, EmailTemplate, BatchEmailOptions, BatchEmailResult, } from '../types'; export class Email { private config: EmailConfig; private templates: Map = new Map(); private provider: any; // Provider implementation constructor(config: EmailConfig) { this.config = config; this.initializeProvider(); } // Register an email template registerTemplate>(template: EmailTemplate): void { this.templates.set(template.id, template); } // Send a single email async send>(options: SendEmailOptions): Promise { try { // Get template if using template ID let template: EmailTemplate | undefined; if (typeof options.template === 'string') { template = this.templates.get(options.template) as EmailTemplate; if (!template) { throw new Error(`Template '${options.template}' not found`); } } else { template = options.template; } // Compile template const compiled = this.compileTemplate(template, options.data); // Prepare email data const emailData = { to: Array.isArray(options.to) ? options.to : [options.to], from: options.from || this.config.from, replyTo: options.replyTo || this.config.replyTo, subject: compiled.subject, html: compiled.html, text: compiled.text, cc: options.cc, bcc: options.bcc, attachments: options.attachments, tags: options.tags, metadata: { ...options.metadata, templateId: template?.id, }, }; // Handle scheduling if (options.schedule) { return this.scheduleEmail(emailData, options.schedule); } // Send email via provider const result = await this.sendViaProvider(emailData); // Call onSent callback if configured if (this.config.onSent) { await this.config.onSent(result); } return result; } catch (error: any) { const emailError: EmailError = { code: error.code || 'email/send-failed', message: error.message || 'Failed to send email', provider: this.config.provider, originalError: error, }; if (this.config.onError) { this.config.onError(emailError); } throw emailError; } } // Send batch emails async sendBatch>( options: BatchEmailOptions ): Promise { const batchId = this.generateBatchId(); const results: EmailResult[] = []; let successful = 0; let failed = 0; // Process batch for (const recipient of options.recipients) { try { const result = await this.send({ ...options, to: recipient.to, data: { ...options.recipients[0].data, ...recipient.data }, metadata: { ...recipient.metadata, batchId, }, }); results.push(result); successful++; } catch (error) { failed++; results.push({ id: '', provider: this.config.provider, to: [recipient.to], sentAt: new Date(), status: 'failed', error: error.message, }); } } return { batchId, total: options.recipients.length, successful, failed, results, }; } // Get a template by ID getTemplate(id: string): EmailTemplate | undefined { return this.templates.get(id); } // List all templates listTemplates(): EmailTemplate[] { return Array.from(this.templates.values()); } // Private methods private initializeProvider(): void { // Initialize provider based on config switch (this.config.provider) { case 'resend': // this.provider = new ResendProvider(this.config); break; case 'sendgrid': // this.provider = new SendGridProvider(this.config); break; default: console.warn(`Provider ${this.config.provider} not implemented yet`); } } private compileTemplate( template: EmailTemplate | undefined, data?: T ): { subject: string; html: string; text?: string } { if (!template) { return { subject: 'No Subject', html: '

No content

', text: 'No content', }; } // Compile subject const subject = typeof template.subject === 'function' ? template.subject(data || {} as T) : template.subject; // Compile HTML body let html = typeof template.body.html === 'function' ? template.body.html(data || {} as T) : template.body.html; // Compile text body let text: string | undefined; if (template.body.text) { text = typeof template.body.text === 'function' ? template.body.text(data || {} as T) : template.body.text; } // Add footer if not disabled if (template.includeFooter !== false && this.config.footer) { const footerHtml = this.generateFooterHtml(); const footerText = this.generateFooterText(); if (footerHtml) { html = this.injectFooterHtml(html, footerHtml); } if (footerText && text) { text = `${text}\n\n${footerText}`; } } return { subject, html, text }; } private generateFooterHtml(): string { const footer = this.config.footer; if (!footer) return ''; let footerHtml = '
'; // Custom footer HTML if (footer.html) { const customHtml = typeof footer.html === 'function' ? footer.html() : footer.html; footerHtml += customHtml; } else { // Build default footer const parts: string[] = []; // Disclaimer if (footer.disclaimer?.enabled && footer.disclaimer.text) { parts.push(`

${footer.disclaimer.text}

`); } // Unsubscribe if (footer.unsubscribe?.enabled) { const unsubUrl = footer.unsubscribe.url || '#unsubscribe'; const unsubText = footer.unsubscribe.text || 'Unsubscribe'; parts.push(`

${unsubText}

`); } // Privacy if (footer.privacy?.enabled) { const privacyUrl = footer.privacy.url || '#privacy'; const privacyText = footer.privacy.text || 'Privacy Policy'; parts.push(`

${privacyText}

`); } // Company info if (footer.company) { let companyHtml = `

© ${new Date().getFullYear()} ${footer.company.name}`; if (footer.company.address) { companyHtml += ` | ${footer.company.address}`; } if (footer.company.website) { companyHtml += ` | ${footer.company.website}`; } companyHtml += '

'; parts.push(companyHtml); } footerHtml += parts.join(''); } footerHtml += '
'; return footerHtml; } private generateFooterText(): string { const footer = this.config.footer; if (!footer) return ''; if (footer.text) { return typeof footer.text === 'function' ? footer.text() : footer.text; } // Build default text footer const parts: string[] = []; if (footer.disclaimer?.enabled && footer.disclaimer.text) { parts.push(footer.disclaimer.text); } if (footer.unsubscribe?.enabled) { const unsubText = footer.unsubscribe.text || 'Unsubscribe'; const unsubUrl = footer.unsubscribe.url || '#unsubscribe'; parts.push(`${unsubText}: ${unsubUrl}`); } if (footer.privacy?.enabled) { const privacyText = footer.privacy.text || 'Privacy Policy'; const privacyUrl = footer.privacy.url || '#privacy'; parts.push(`${privacyText}: ${privacyUrl}`); } if (footer.company) { let companyText = `© ${new Date().getFullYear()} ${footer.company.name}`; if (footer.company.address) { companyText += ` | ${footer.company.address}`; } if (footer.company.website) { companyText += ` | ${footer.company.website}`; } parts.push(companyText); } return parts.join('\n'); } private injectFooterHtml(html: string, footerHtml: string): string { // Try to inject before closing body tag if (html.includes('')) { return html.replace('', `${footerHtml}`); } // Otherwise append to end return html + footerHtml; } private async sendViaProvider(data: any): Promise { // Simulate sending (replace with actual provider implementation) const result: EmailResult = { id: this.generateEmailId(), provider: this.config.provider, to: data.to, template: data.metadata?.templateId, sentAt: new Date(), status: 'sent', }; // Simulate async send await new Promise(resolve => setTimeout(resolve, 100)); return result; } private async scheduleEmail(data: any, scheduleDate: Date): Promise { // Simulate scheduling (replace with actual implementation) const result: EmailResult = { id: this.generateEmailId(), provider: this.config.provider, to: data.to, template: data.metadata?.templateId, sentAt: scheduleDate, status: 'scheduled', }; return result; } private generateEmailId(): string { return `email_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } private generateBatchId(): string { return `batch_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } }