1
import { useEnv } from '@directus/env';
2
import { InvalidPayloadError } from '@directus/errors';
3
import type { Accountability, SchemaOverview } from '@directus/types';
4
import fse from 'fs-extra';
5
import type { Knex } from 'knex';
6
import { Liquid } from 'liquidjs';
7
import type { SendMailOptions, Transporter } from 'nodemailer';
8
import path from 'path';
9
import { fileURLToPath } from 'url';
10
import getDatabase from '../../database/index.js';
11
import { useLogger } from '../../logger.js';
12
import getMailer from '../../mailer.js';
13
import type { AbstractServiceOptions } from '../../types/index.js';
14
import { Url } from '../../utils/url.js';
17
const logger = useLogger();
19
const __dirname = path.dirname(fileURLToPath(import.meta.url));
21
const liquidEngine = new Liquid({
22
root: [path.resolve(env['EMAIL_TEMPLATES_PATH'] as string), path.resolve(__dirname, 'templates')],
26
export type EmailOptions = SendMailOptions & {
29
data: Record<string, any>;
33
export class MailService {
34
schema: SchemaOverview;
35
accountability: Accountability | null;
39
constructor(opts: AbstractServiceOptions) {
40
this.schema = opts.schema;
41
this.accountability = opts.accountability || null;
42
this.knex = opts?.knex || getDatabase();
43
this.mailer = getMailer();
45
if (env['EMAIL_VERIFY_SETUP']) {
46
this.mailer.verify((error) => {
48
logger.warn(`Email connection failed:`);
55
async send<T>(options: EmailOptions): Promise<T> {
56
const { template, ...emailOptions } = options;
57
let { html } = options;
59
const defaultTemplateData = await this.getDefaultTemplateData();
61
const from = `${defaultTemplateData.projectName} <${options.from || (env['EMAIL_FROM'] as string)}>`;
64
let templateData = template.data;
67
...defaultTemplateData,
71
html = await this.renderTemplate(template.name, templateData);
74
if (typeof html === 'string') {
75
// Some email clients start acting funky when line length exceeds 75 characters. See #6074
78
.map((line) => line.trim())
82
const info = await this.mailer.sendMail({ ...emailOptions, from, html });
86
private async renderTemplate(template: string, variables: Record<string, any>) {
87
const customTemplatePath = path.resolve(env['EMAIL_TEMPLATES_PATH'] as string, template + '.liquid');
88
const systemTemplatePath = path.join(__dirname, 'templates', template + '.liquid');
90
const templatePath = (await fse.pathExists(customTemplatePath)) ? customTemplatePath : systemTemplatePath;
92
if ((await fse.pathExists(templatePath)) === false) {
93
throw new InvalidPayloadError({ reason: `Template "${template}" doesn't exist` });
96
const templateString = await fse.readFile(templatePath, 'utf8');
97
const html = await liquidEngine.parseAndRender(templateString, variables);
102
private async getDefaultTemplateData() {
103
const projectInfo = await this.knex
104
.select(['project_name', 'project_logo', 'project_color', 'project_url'])
105
.from('directus_settings')
109
projectName: projectInfo?.project_name || 'Directus',
110
projectColor: projectInfo?.project_color || '#171717',
111
projectLogo: getProjectLogoURL(projectInfo?.project_logo),
112
projectUrl: projectInfo?.project_url || '',
115
function getProjectLogoURL(logoID?: string) {
116
const projectLogoUrl = new Url(env['PUBLIC_URL'] as string);
119
projectLogoUrl.addPath('assets', logoID);
121
projectLogoUrl.addPath('admin', 'img', 'directus-white.png');
124
return projectLogoUrl.toString();