import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2' import { defaultProvider } from '@aws-sdk/credential-provider-node' import Email from 'email-templates' import nodemailer from 'nodemailer' import type SESTransport from 'nodemailer/lib/ses-transport' export default class Mailer { /** * The starting point of a range. * @type {string} */ private readonly from: string /** * The default file template for the application. * @type {string} */ private readonly templateDefaultFile: string = 'html' /** * The transporter object used for sending emails using the AWS SES service. * @type {ReturnType>} */ private readonly transporter: ReturnType< typeof nodemailer.createTransport > /** * Constructs a new instance of the EmailSender class. * @param {string} defaultFrom - The default "from" email address. * @param {string} region - The AWS region to use for sending emails. * @returns None */ constructor(defaultFrom: string, region: string) { this.from = defaultFrom const sesClient = new SESv2Client({ credentials: defaultProvider(), region, }) this.transporter = nodemailer.createTransport({ SES: { sesClient, SendEmailCommand }, }) } /** * Sends a raw email with the specified parameters. * @param {string | Array} to - The recipient(s) of the email. * @param {string} htmlMessage - The HTML content of the email. * @param {string} subject - The subject of the email. * @param {string | Array} [optionalCC] - The optional CC recipient(s) of the email. * @param {string} [optionalFrom] - The optional sender of the email. If not provided, the default sender will be used. * @param {string} [optionalReplyTo] - The optional reply-to address for the email. * @param {any[]} [optionalAttachments] - The optional attachments to include */ public async sendRawEmail( to: string | Array, htmlMessage: string, subject: string, optionalCC?: string | Array, optionalFrom?: string, optionalReplyTo?: string, // TODO: improve attachment type -> Attachment optionalAttachments?: any[], optionalTransport?: Email.NodeMailerTransportOptions ) { //Generate emails const email = new Email({ message: { from: optionalFrom || this.from, to: to, html: htmlMessage, subject, ...(optionalAttachments ? { attachments: optionalAttachments } : {}), ...(optionalCC ? { cc: optionalCC } : {}), ...(optionalReplyTo ? { replyTo: optionalReplyTo } : {}), }, transport: optionalTransport || this.transporter, send: true, preview: false, }) // let resp = null try { resp = await email.send() console.debug('Mailer resp:', resp) } catch (e) { console.error('Mailer error:', e) throw e } return resp } /** * Sends a templated email to the specified recipients. * @param {string | Array} to - The email address(es) of the recipient(s). * @param {string | Array} templates - The template(s) to use for the email. * @param {object} data - The data to be used in the email template. * @param {string | Array} [optionalCC] - The email address(es) to CC. * @param {string} [optionalFrom] - The email address to send the email from. * @param {string} [optionalReplyTo] - The email address to set as the reply-to address. * @param {any[]} [optionalAttachments] - An array */ public async sendTemplatedEmail( to: string | Array, templates: string | Array, data: object, optionalCC?: string | Array, optionalFrom?: string, optionalReplyTo?: string, // TODO: improve attachment type -> Attachment optionalAttachments?: any[], optionalTransport?: Email.NodeMailerTransportOptions ) { //Generate emails const email = new Email({ message: { from: optionalFrom || this.from, to: to, ...(optionalAttachments ? { attachments: optionalAttachments } : {}), ...(optionalCC ? { cc: optionalCC } : {}), ...(optionalReplyTo ? { replyTo: optionalReplyTo } : {}), }, transport: optionalTransport || this.transporter, send: true, }) // let resp = null try { const chosenTemplate = await this.chooseTemplate(templates, data) resp = await email.send({ template: chosenTemplate, locals: data }) console.debug('Mailer resp:', resp) } catch (e) { console.error('Mailer error:', e) throw e } return resp } /** * Creates a new SMTP transporter for sending emails using NodeMailer. * @param {string} host - The SMTP server host. * @param {number} portNumber - The port number to connect to the SMTP server. * @param {string} user - The username for authentication with the SMTP server. * @param {string} password - The password for authentication with the SMTP server. * @returns {Email.NodeMailerTransportOptions} - The SMTP transporter object. */ public newSMTPTransporter( host: string, portNumber: number, user: string, password: string ): Email.NodeMailerTransportOptions { const smtpTransporter = nodemailer.createTransport({ host: host, port: portNumber, connectionTimeout: 2000, auth: { user: user, pass: password, }, }) return smtpTransporter } /** * Chooses a template from the given array of templates or a single template string based on whether it can be rendered with the provided data. * @param {string | Array} templates - The template(s) to choose from. * @param {object} data - The data to be used for rendering the template. * @returns {Promise} - The chosen template. * @throws {Error} - If no template can be rendered with the provided data. */ private async chooseTemplate(templates: string | Array, data: object): Promise { if (Array.isArray(templates)) { //For each template check if can render it for (const template of templates) { if (await this.canRenderTemplate(template, data)) return template } } else if (templates) { if (await this.canRenderTemplate(templates, data)) return templates } throw new Error( `Could not render email with template ${templates} and following data. Please, check logs above! ` + JSON.stringify(data, null, 2) ) } /** * Checks if a given email template can be rendered with the provided data. * @param {string} template - The name of the email template. * @param {object} data - The data to be used for rendering the template. * @returns {Promise} - A promise that resolves to true if the template can be rendered, false otherwise. */ private async canRenderTemplate(template: string, data: object): Promise { const validRenderResp = await Mailer.renderTemplate( `${template}/${this.templateDefaultFile}`, data ) return validRenderResp != null } /** * Renders the given template with the provided data using the Email class. * @param {string} template - The name or path of the template to render. * @param {object} data - The data object to pass to the template. * @returns {Promise} - A promise that resolves to true if the template was rendered successfully, false otherwise. */ public static async renderTemplate(template: string, data: object): Promise { let validRenderResp: string | null = null try { const email = new Email() validRenderResp = await email.render(`${template}`, data) } catch (e) { console.log(`Error while checking renderability of email template ${template}`, e) } return validRenderResp } }