import { Resend } from 'resend'; import { ResendConfig } from '../config/notification.config'; import { EmailNotification, NotificationStatus, NotificationPriority, NotificationRecipient, NotificationChannel, } from '../types/notification.types'; export class EmailService { private resend: Resend; private config: ResendConfig; constructor(config: ResendConfig) { this.config = config; this.resend = new Resend(config.apiKey); } /** * Send a transactional email */ async sendEmail( recipient: NotificationRecipient, subject: string, htmlBody: string, textBody?: string, templateId?: string, attachments?: Array<{ filename: string; content: Buffer | string; mimeType?: string; }> ): Promise { const notification: EmailNotification = { id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, userId: recipient.userId, channel: NotificationChannel.EMAIL, templateId: templateId || 'custom', priority: NotificationPriority.NORMAL, status: NotificationStatus.PENDING, to: recipient.email!, subject, htmlBody, textBody, attachments, createdAt: new Date(), }; try { const { data, error } = await this.resend.emails.send({ from: `${this.config.fromName} <${this.config.fromEmail}>`, to: [recipient.email!], subject, html: htmlBody, text: textBody, attachments: attachments?.map((att) => ({ filename: att.filename, content: typeof att.content === 'string' ? Buffer.from(att.content) : att.content, mimeType: att.mimeType, })), metadata: { userId: recipient.userId, notificationId: notification.id, }, }); if (error) { notification.status = NotificationStatus.FAILED; notification.failedAt = new Date(); notification.errorMessage = error.message; } else { notification.status = NotificationStatus.SENT; notification.sentAt = new Date(); } return notification; } catch (error) { notification.status = NotificationStatus.FAILED; notification.failedAt = new Date(); notification.errorMessage = error instanceof Error ? error.message : 'Unknown error'; return notification; } } /** * Send email with retry logic */ async sendEmailWithRetry( recipient: NotificationRecipient, subject: string, htmlBody: string, textBody?: string, maxRetries: number = 3 ): Promise { let lastError: Error | undefined; for (let attempt = 0; attempt < maxRetries; attempt++) { try { const result = await this.sendEmail(recipient, subject, htmlBody, textBody); if (result.status === NotificationStatus.SENT) { return result; } lastError = new Error(result.errorMessage); } catch (error) { lastError = error as Error; } // Wait before retry (exponential backoff) await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, attempt))); } return { id: `email_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, userId: recipient.userId, channel: NotificationChannel.EMAIL, templateId: 'custom', priority: NotificationPriority.NORMAL, status: NotificationStatus.FAILED, to: recipient.email!, subject, htmlBody, textBody, createdAt: new Date(), failedAt: new Date(), errorMessage: lastError?.message, }; } /** * Check email delivery status */ async getDeliveryStatus(notificationId: string): Promise { try { const emailId = notificationId.replace('email_', ''); const { data } = await this.resend.emails.get(emailId); if (data.status === 'sent') { return NotificationStatus.SENT; } else if (data.status === 'delivered') { return NotificationStatus.DELIVERED; } else if (data.status === 'failed') { return NotificationStatus.FAILED; } return NotificationStatus.PENDING; } catch { return NotificationStatus.PENDING; } } /** * Batch send emails */ async batchSendEmails( recipients: Array<{ recipient: NotificationRecipient; subject: string; htmlBody: string; textBody?: string; }> ): Promise { const promises = recipients.map(async ({ recipient, subject, htmlBody, textBody }) => { return this.sendEmail(recipient, subject, htmlBody, textBody); }); return Promise.all(promises); } }