diff --git a/.env.example b/.env.example
deleted file mode 100644
index aa23c26ca3..0000000000
--- a/.env.example
+++ /dev/null
@@ -1,10 +0,0 @@
-PORT=3000
-
-DB_CLIENT="pg"
-DB_HOST="localhost"
-DB_PORT=5432
-DB_NAME="directus"
-DB_USER="postgres"
-DB_PASSWORD="psql1234"
-
-EXTENSIONS_PATH="./extensions"
diff --git a/example.env b/example.env
new file mode 100644
index 0000000000..ad1345541c
--- /dev/null
+++ b/example.env
@@ -0,0 +1,29 @@
+# General
+PORT=3000
+
+# Database
+DB_CLIENT="pg"
+DB_HOST="localhost"
+DB_PORT=5432
+DB_NAME="directus"
+DB_USER="postgres"
+DB_PASSWORD="psql1234"
+
+# Extensions
+EXTENSIONS_PATH="./extensions"
+
+# Email
+EMAIL_TRANSPORT="sendmail"
+
+## Email (Sendmail Transport)
+EMAIL_SENDMAIL_NEW_LINE="unix"
+EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
+
+## Email (SMTP Transport)
+EMAIL_SMTP_POOL=true
+EMAIL_SMTP_HOST="localhost"
+EMAIL_SMTP_PORT=465
+EMAIL_SMTP_SECURE=false # Use TLS
+EMAIL_SMTP_USER="username"
+EMAIL_SMTP_PASSWORD="password"
+
diff --git a/package-lock.json b/package-lock.json
index df397d117e..7971fe2dd6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -119,6 +119,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.13.tgz",
"integrity": "sha512-rouEWBImiRaSJsVA+ITTFM6ZxibuAlTuNOCyxVbwreu6k6+ujs7DfnU9o+PShFhET78pMBl3eH+AGSI5eOTkPA=="
},
+ "@types/nodemailer": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.0.tgz",
+ "integrity": "sha512-KY7bFWB0MahRZvVW4CuW83qcCDny59pJJ0MQ5ifvfcjNwPlIT0vW4uARO4u1gtkYnWdhSvURegecY/tzcukJcA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@@ -722,6 +731,17 @@
"string-width": "^4.2.0"
}
},
+ "cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
@@ -826,6 +846,28 @@
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
},
+ "copyfiles": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.3.0.tgz",
+ "integrity": "sha512-73v7KFuDFJ/ofkQjZBMjMBFWGgkS76DzXvBMUh7djsMOE5EELWtAO/hRB6Wr5Vj5Zg+YozvoHemv0vnXpqxmOQ==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.0.5",
+ "minimatch": "^3.0.3",
+ "mkdirp": "^1.0.4",
+ "noms": "0.0.0",
+ "through2": "^2.0.1",
+ "yargs": "^15.3.1"
+ },
+ "dependencies": {
+ "mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "dev": true
+ }
+ }
+ },
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@@ -1677,6 +1719,12 @@
}
}
},
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true
+ },
"get-own-enumerable-property-symbols": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz",
@@ -2463,6 +2511,11 @@
}
}
},
+ "liquidjs": {
+ "version": "9.12.0",
+ "resolved": "https://registry.npmjs.org/liquidjs/-/liquidjs-9.12.0.tgz",
+ "integrity": "sha512-Z1fERRy5TG76kNQfDuCl9+YmK6RMBsGhi3fl/57rBb5BbuVAS0YA7R3kh0/V0TwezpeywUf5xasR+Np86nD2TA=="
+ },
"listr2": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-2.1.7.tgz",
@@ -2963,6 +3016,57 @@
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "nodemailer": {
+ "version": "6.4.10",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.10.tgz",
+ "integrity": "sha512-j+pS9CURhPgk6r0ENr7dji+As2xZiHSvZeVnzKniLOw1eRAyM/7flP0u65tCnsapV8JFu+t0l/5VeHsCZEeh9g=="
+ },
+ "noms": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz",
+ "integrity": "sha1-2o69nzr51nYJGbJ9nNyAkqczKFk=",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "readable-stream": "~1.0.31"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "1.0.34",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz",
+ "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "dev": true
+ }
}
},
"nopt": {
@@ -3769,6 +3873,18 @@
}
}
},
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
+ "dev": true
+ },
+ "require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
@@ -3813,9 +3929,10 @@
"integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg=="
},
"rimraf": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
- "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
"requires": {
"glob": "^7.1.3"
}
@@ -4559,6 +4676,14 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
}
}
},
@@ -4790,6 +4915,12 @@
"isexe": "^2.0.0"
}
},
+ "which-module": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
+ "dev": true
+ },
"which-pm-runs": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz",
@@ -4905,6 +5036,12 @@
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="
},
+ "y18n": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
+ "dev": true
+ },
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -4916,6 +5053,43 @@
"integrity": "sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg==",
"dev": true
},
+ "yargs": {
+ "version": "15.3.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
+ "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
+ "dev": true,
+ "requires": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.1"
+ }
+ },
+ "yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "requires": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "dependencies": {
+ "camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true
+ }
+ }
+ },
"yn": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
diff --git a/package.json b/package.json
index 54e215d2e5..3545820cd5 100644
--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
"main": "dist/server.js",
"scripts": {
"start": "NODE_ENV=production node dist/server.js",
- "build": "tsc",
+ "build": "rimraf dist && tsc && copyfiles \"src/**/*.*\" -e \"src/**/*.ts\" -u 1 dist",
"dev": "LOG_LEVEL=trace ts-node-dev src/server.ts --clear --watch \"src/**/*.ts\" --rs --transpile-only | pino-colada"
},
"repository": {
@@ -29,12 +29,15 @@
"homepage": "https://github.com/directus/api-node#readme",
"devDependencies": {
"@types/express": "^4.17.6",
+ "@types/nodemailer": "^6.4.0",
"@types/pino": "^6.3.0",
+ "copyfiles": "^2.3.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.2.5",
"lint-staged": "^10.2.10",
"pino-colada": "^1.6.1",
"prettier": "^2.0.5",
+ "rimraf": "^3.0.2",
"ts-node": "^8.10.2",
"tslint": "^6.1.2",
"typescript": "^3.9.5"
@@ -56,8 +59,10 @@
"express-async-handler": "^1.1.4",
"get-port": "^5.1.1",
"knex": "^0.21.1",
+ "liquidjs": "^9.12.0",
"mssql": "^6.2.0",
"mysql": "^2.18.1",
+ "nodemailer": "^6.4.10",
"oracledb": "^4.2.0",
"pg": "^8.2.1",
"pino": "^6.3.2",
diff --git a/src/app.ts b/src/app.ts
index 0773767047..30f6c5bbe2 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -23,6 +23,15 @@ import webhooksRouter from './routes/webhooks';
import notFoundHandler from './routes/not-found';
+// import sendMail from './mail';
+
+// sendMail({
+// to: 'rijkvanzanten@me.com',
+// subject: 'Test Email',
+// text: 'Hi there!',
+// html: '
Hi there!
',
+// });
+
const app = express()
.disable('x-powered-by')
.use(bodyParser.json())
diff --git a/src/mail/index.ts b/src/mail/index.ts
new file mode 100644
index 0000000000..067381aa16
--- /dev/null
+++ b/src/mail/index.ts
@@ -0,0 +1,70 @@
+import logger from '../logger';
+import nodemailer, { Transporter } from 'nodemailer';
+import { Liquid } from 'liquidjs';
+import path from 'path';
+import fs from 'fs';
+import { promisify } from 'util';
+
+const readFile = promisify(fs.readFile);
+
+const liquidEngine = new Liquid();
+
+logger.trace('[Email] Initializing email transport...');
+
+if (!process.env.EMAIL_TRANSPORT) {
+ logger.warn(`[Email] No email transport is configured. Using default: sendmail.`);
+}
+
+const emailTransport = process.env.EMAIL_TRANSPORT || 'sendmail';
+
+let transporter: Transporter;
+
+if (emailTransport === 'sendmail') {
+ transporter = nodemailer.createTransport({
+ sendmail: true,
+ newline: process.env.EMAIL_SENDMAIL_NEW_LINE || 'unix',
+ path: process.env.EMAIL_SENDMAIL_PATH || '/usr/sbin/sendmail',
+ });
+} else if (emailTransport.toLowerCase() === 'smtp') {
+ transporter = nodemailer.createTransport({
+ pool: process.env.EMAIL_SMTP_POOL === 'true',
+ host: process.env.EMAIL_SMTP_HOST,
+ port: Number(process.env.EMAIL_SMTP_PORT),
+ secure: process.env.EMAIL_SMTP_SECURE === 'true',
+ auth: {
+ user: process.env.EMAIL_SMTP_USER,
+ pass: process.env.EMAIL_SMTP_PASSWORD,
+ },
+ } as any);
+
+ logger.trace('[Email] Verifying SMTP connection.');
+
+ transporter
+ .verify()
+ .then(() => {
+ logger.info('[Email] SMTP connected. Ready to send emails.');
+ })
+ .catch((err) => {
+ logger.error(`[Email] Couldn't connect to SMTP server:`);
+ logger.error(err);
+ });
+}
+
+export type EmailOptions = {
+ to: string; // email address of the recipient
+ subject: string;
+ text: string;
+ html: string;
+};
+
+export default async function sendMail(options: EmailOptions) {
+ const templateString = await readFile(path.join(__dirname, 'templates/base.liquid'), 'utf8');
+ const html = await liquidEngine.parseAndRender(templateString, { html: options.html });
+
+ try {
+ await transporter.sendMail({ ...options, html: html });
+ } catch (error) {
+ logger.warn('[Email] Unexpected error while sending an email:');
+ logger.warn(error);
+ }
+}
diff --git a/src/mail/templates/base.liquid b/src/mail/templates/base.liquid
new file mode 100644
index 0000000000..536550b3f8
--- /dev/null
+++ b/src/mail/templates/base.liquid
@@ -0,0 +1,72 @@
+
+
+
+
+
+ Directus Email Service
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+ {% block content %}{{ html }}{% endblock %}
+ |
+
+
+ |
+
+
+
+
+
+ |
+ {% block footer %}{% endblock %}
+ |
+
+
+ |
+
+
+ |
+
+
+
+
+