@454creative/easy-email
Version:
A framework-agnostic email service library for Node.js with observability and monitoring
195 lines (164 loc) • 6.77 kB
text/typescript
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;
}
}