Merge pull request #20 from directus/mail

Add sendMail handler
This commit is contained in:
Rijk van Zanten
2020-06-23 12:07:46 -04:00
committed by GitHub
7 changed files with 363 additions and 14 deletions

View File

@@ -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"

29
example.env Normal file
View File

@@ -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"

180
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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: '<h1>Hi there!</h1>',
// });
const app = express()
.disable('x-powered-by')
.use(bodyParser.json())

70
src/mail/index.ts Normal file
View File

@@ -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);
}
}

View File

@@ -0,0 +1,72 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Directus Email Service</title>
<meta name="viewport" content="width=device-width">
<meta name="format-detection" content="telephone=no">
<style type="text/css">
a {
border: none;
text-decoration: none;
color: #2196f3;
outline: none !important;
color: #2196f3 !important;
}
a:hover {
color: #2196f3 !important;
}
p {
margin: 20px 0 20px 0;
}
</style>
</head>
<body
style="margin: 0; padding: 20px; background-color: #eceff1; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; -ms-text-size-adjust: none; font-weight: 400; font-size: 14px; color: #546e7a; font-family: 'Roboto', Helvetica, Helvetica, Arial, sans-serif; letter-spacing: 0;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="padding: 0;">
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600"
style="border: 0 solid #263238; border-collapse: collapse;">
<tr>
<td align="center" bgcolor="#263238"
style="padding: 30px 0 30px 0; border-radius: 4px 4px 0 0;">
<img src="https://directus.io/assets/directus-white.png" alt="Directus" width="130"
style="display: block;" />
</td>
</tr>
<tr>
<td bgcolor="#ffffff" style="padding: 60px 30px 60px 30px; border-radius: 0 0 4px 4px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td
style="color: #546e7a; font-family: Helvetica, Arial, sans-serif; padding: 0; line-height: 1.5; font-size: 16px;">
{% block content %}{{ html }}{% endblock %}
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#eceff1" style="padding: 20px 30px 0 32px;">
<table border="0" cellpadding="0" cellspacing="0" width="100%">
<tr>
<td style="color:#b0bec5; font-family:Helvetica, Arial, sans-serif; font-size:12px;"
width="75%">
{% block footer %}{% endblock %}
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>