172 lines
4.6 KiB
TypeScript
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);
|
|
}
|
|
}
|