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 + + + + + + + + + + +
+ + + + + + + + + + +
+ Directus +
+ + + + +
+ {% block content %}{{ html }}{% endblock %} +
+
+ + + + +
+ {% block footer %}{% endblock %} +
+
+
+ + +