Files
FrenoCorp/packages/shared-notifications/src/services/email.service.ts

172 lines
4.6 KiB
TypeScript

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<EmailNotification> {
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<EmailNotification> {
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<NotificationStatus> {
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<EmailNotification[]> {
const promises = recipients.map(async ({ recipient, subject, htmlBody, textBody }) => {
return this.sendEmail(recipient, subject, htmlBody, textBody);
});
return Promise.all(promises);
}
}