UNPKG

@directus/api

Version:

Directus is a real-time API and App dashboard for managing SQL database content

110 lines (109 loc) 4.6 kB
import { useEnv } from '@directus/env'; import { InvalidPayloadError } from '@directus/errors'; import { isObject } from '@directus/utils'; import fse from 'fs-extra'; import { Liquid } from 'liquidjs'; import path from 'path'; import { fileURLToPath } from 'url'; import getDatabase from '../../database/index.js'; import emitter from '../../emitter.js'; import { useLogger } from '../../logger/index.js'; import getMailer from '../../mailer.js'; import { Url } from '../../utils/url.js'; import { useEmailRateLimiterQueue } from './rate-limiter.js'; const env = useEnv(); const logger = useLogger(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const liquidEngine = new Liquid({ root: [path.resolve(env['EMAIL_TEMPLATES_PATH']), path.resolve(__dirname, 'templates')], extname: '.liquid', }); export class MailService { schema; accountability; knex; mailer; constructor(opts) { this.schema = opts.schema; this.accountability = opts.accountability || null; this.knex = opts?.knex || getDatabase(); this.mailer = getMailer(); if (env['EMAIL_VERIFY_SETUP']) { this.mailer.verify((error) => { if (error) { logger.warn(`Email connection failed:`); logger.warn(error); } }); } } async send(data, options) { await useEmailRateLimiterQueue(); const payload = await emitter.emitFilter(`email.send`, data, {}); if (!payload) return null; const { template, ...emailOptions } = payload; let { html } = data; // option for providing tempalate data was added to prevent transaction race conditions with preceding promises const defaultTemplateData = options?.defaultTemplateData ?? (await this.getDefaultTemplateData()); if (isObject(emailOptions.from) && (!emailOptions.from.name || !emailOptions.from.address)) { throw new InvalidPayloadError({ reason: 'A name and address property are required in the "from" object' }); } const from = isObject(emailOptions.from) ? emailOptions.from : { name: defaultTemplateData.projectName, address: emailOptions.from || env['EMAIL_FROM'], }; if (template) { let templateData = template.data; templateData = { ...defaultTemplateData, ...templateData, }; html = await this.renderTemplate(template.name, templateData); } if (typeof html === 'string') { // Some email clients start acting funky when line length exceeds 75 characters. See #6074 html = html .split('\n') .map((line) => line.trim()) .join('\n'); } const info = await this.mailer.sendMail({ ...emailOptions, from, html }); return info; } async renderTemplate(template, variables) { const customTemplatePath = path.resolve(env['EMAIL_TEMPLATES_PATH'], template + '.liquid'); const systemTemplatePath = path.join(__dirname, 'templates', template + '.liquid'); const templatePath = (await fse.pathExists(customTemplatePath)) ? customTemplatePath : systemTemplatePath; if ((await fse.pathExists(templatePath)) === false) { throw new InvalidPayloadError({ reason: `Template "${template}" doesn't exist` }); } const templateString = await fse.readFile(templatePath, 'utf8'); const html = await liquidEngine.parseAndRender(templateString, variables); return html; } async getDefaultTemplateData() { const projectInfo = await this.knex .select(['project_name', 'project_logo', 'project_color', 'project_url']) .from('directus_settings') .first(); return { projectName: projectInfo?.project_name || 'Directus', projectColor: projectInfo?.project_color || '#171717', projectLogo: getProjectLogoURL(projectInfo?.project_logo), projectUrl: projectInfo?.project_url || '', }; function getProjectLogoURL(logoID) { const projectLogoUrl = new Url(env['PUBLIC_URL']); if (logoID) { projectLogoUrl.addPath('assets', logoID); } else { projectLogoUrl.addPath('admin', 'img', 'directus-white.png'); } return projectLogoUrl.toString(); } } }