UNPKG

@454creative/easy-email

Version:

A framework-agnostic email service library for Node.js with observability and monitoring

195 lines (164 loc) 6.77 kB
import { ITemplateService, TemplateInfo, TemplateValidationResult } from '../../interfaces/template.interface'; import { EmailProviderType } from '../../interfaces/email-options.interface'; import { SesConfig } from '../../interfaces/email-options.interface'; import { Logger } from '../../utils/logger'; import { TemplateError, InvalidTemplateError } from '../../errors'; import { SESClient } from '@aws-sdk/client-ses'; const { GetTemplateCommand, ListTemplatesCommand } = require('@aws-sdk/client-ses'); export class SesTemplateService implements ITemplateService { private logger: Logger; private config: SesConfig; private client: SESClient; constructor(config: SesConfig) { this.config = config; this.logger = Logger.getInstance(); this.client = this.createSesClient(); } async renderTemplate( templateName: string, data: Record<string, any>, provider: EmailProviderType ): Promise<string> { if (provider !== EmailProviderType.SES) { throw new TemplateError(`Provider ${provider} not supported by SesTemplateService`); } try { this.logger.debug(`Rendering SES template: ${templateName}`, { data }); const template = await this.getTemplate(templateName); const renderedContent = this.renderWithData(template, data); this.logger.debug(`SES template rendered successfully: ${templateName}`); return renderedContent; } catch (error) { this.logger.error(`Failed to render SES template: ${templateName}`, { error }); throw new TemplateError(`Failed to render SES template: ${templateName}`, { error }); } } async validateTemplate(templateName: string, provider: EmailProviderType): Promise<boolean> { if (provider !== EmailProviderType.SES) { return false; } try { await this.getTemplate(templateName); return true; } catch { return false; } } async getTemplateInfo(templateName: string, provider: EmailProviderType): Promise<TemplateInfo> { if (provider !== EmailProviderType.SES) { throw new TemplateError(`Provider ${provider} not supported by SesTemplateService`); } try { const template = await this.getTemplate(templateName); return { id: templateName, name: template.Template?.TemplateName || templateName, version: template.Template?.Version || '1.0.0', variables: this.extractVariables(template.Template?.HtmlPart || ''), lastModified: template.Template?.LastModifiedDate || new Date(), provider: EmailProviderType.SES }; } catch (error) { throw new TemplateError(`Failed to get template info: ${templateName}`, { error }); } } async validateTemplateWithDetails(templateName: string, data: Record<string, any>): Promise<TemplateValidationResult> { try { const template = await this.getTemplate(templateName); const templateVariables = this.extractVariables(template.Template?.HtmlPart || ''); const providedVariables = Object.keys(data); const missingVariables = templateVariables.filter(v => !providedVariables.includes(v)); const warnings = providedVariables.filter(v => !templateVariables.includes(v)); return { isValid: missingVariables.length === 0, errors: missingVariables.length > 0 ? [`Missing required variables: ${missingVariables.join(', ')}`] : [], warnings: warnings.length > 0 ? [`Unused variables provided: ${warnings.join(', ')}`] : [], variables: templateVariables, missingVariables }; } catch (error) { return { isValid: false, errors: [`Template not found or invalid: ${templateName}`], warnings: [], variables: [], missingVariables: [] }; } } async listTemplates(): Promise<string[]> { try { const command = new ListTemplatesCommand({}); const response = await this.client.send(command); return response.TemplatesMetadata?.map((t: any) => t.Name || '') || []; } catch (error) { this.logger.error('Failed to list SES templates', { error }); throw new TemplateError('Failed to list SES templates', { error }); } } private createSesClient(): SESClient { const clientConfig: any = { region: this.config.region, apiVersion: this.config.apiVersion || '2010-12-01', maxAttempts: this.config.maxRetries || 3, }; if (this.config.credentials) { clientConfig.credentials = this.config.credentials; } else if (this.config.accessKeyId && this.config.secretAccessKey) { clientConfig.credentials = { accessKeyId: this.config.accessKeyId, secretAccessKey: this.config.secretAccessKey, sessionToken: this.config.sessionToken, }; } if (this.config.endpoint) { clientConfig.endpoint = this.config.endpoint; } return new SESClient(clientConfig); } private async getTemplate(templateName: string): Promise<any> { try { const command = new GetTemplateCommand({ TemplateName: templateName }); const response = await this.client.send(command); if (!response.Template) { throw new InvalidTemplateError(`SES template not found: ${templateName}`); } return response; } catch (error: any) { if (error.name === 'TemplateDoesNotExist') { throw new InvalidTemplateError(`SES template does not exist: ${templateName}`); } throw error; } } private extractVariables(htmlContent: string): string[] { // Extract variables from SES template format // SES uses {{variable}} format const variableRegex = /\{\{([^}]+)\}\}/g; const variables: string[] = []; let match; while ((match = variableRegex.exec(htmlContent)) !== null) { const variable = match[1].trim(); if (!variables.includes(variable)) { variables.push(variable); } } return variables; } private renderWithData(template: any, data: Record<string, any>): string { let content = template.Template?.HtmlPart || template.Template?.TextPart || ''; // Replace variables with data Object.entries(data).forEach(([key, value]) => { const regex = new RegExp(`{{${key}}}`, 'g'); content = content.replace(regex, String(value)); }); // Replace any remaining variables with empty strings const remainingVariables = content.match(/\{\{([^}]+)\}\}/g) || []; remainingVariables.forEach((variable: string) => { const key = variable.slice(2, -2); // Remove {{ }} const regex = new RegExp(`{{${key}}}`, 'g'); content = content.replace(regex, ''); }); return content; } }