Merge pull request #573 from trucknet-io/development

feat(api): Transactional mail rest api
This commit is contained in:
Tomas Bures 2019-04-02 16:23:35 +02:00 committed by GitHub
commit 3a45443b64
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 171 additions and 6 deletions

1
.gitignore vendored
View file

@ -1,4 +1,5 @@
/.idea /.idea
/.vscode
/last-failed-e2e-test.* /last-failed-e2e-test.*
node_modules node_modules

View file

@ -368,6 +368,37 @@ export default class API extends Component {
</p> </p>
<pre>curl -XGET '{getUrl(`api/rss/fetch/5OOnZKrp0?access_token=${accessToken}`)}'</pre> <pre>curl -XGET '{getUrl(`api/rss/fetch/5OOnZKrp0?access_token=${accessToken}`)}'</pre>
<h4>POST /api/templates/:templateId/send {t('sendTransactionalEmail')}</h4>
<p>
{t('sendSingleEmailByTemplateId')}
</p>
<p>
<strong>GET</strong> {t('arguments')}
</p>
<ul>
<li><strong>access_token</strong> {t('yourPersonalAccessToken')}</li>
</ul>
<p>
<strong>POST</strong> {t('arguments')}
</p>
<ul>
<li><strong>EMAIL</strong> {t('emailAddress')} (<em>{t('required')}</em>)</li>
<li><strong>SEND_CONFIGURATION_ID</strong> {t('sendConfigurationId')}</li>
<li><strong>SUBJECT</strong> {t('subject')}</li>
<li><strong>DATA</strong> {t('templateData')}: <em>{'{'} "any": ["type", {'{'}"of": "data"{'}'}] {'}'}</em></li>
<li><strong>VARIABLES</strong> {t('templateVariables')}: <em>{'{'} "FOO": "bar" {'}'}</em></li>
</ul>
<p>
<strong>{t('example')}</strong>
</p>
<pre>curl -XPOST '{getUrl(`api/templates/1/send?access_token={accessToken}`)}' \
--data 'EMAIL=test@example.com&amp;SUBJECT=Test&amp;VARIABLES[FOO]=bar&amp;VARIABLES[TEST]=example'</pre>
</div> </div>
); );
} }

View file

@ -964,5 +964,10 @@
"thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter",
"thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter",
"thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number", "thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number",
"thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character" "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character",
"templateData": "Data passed to template when compiling with Handlebars",
"templateVariables": "Map of template/subject variables to replace",
"sendTransactionalEmail": "Send transactional email",
"sendSingleEmailByTemplateId": "Send single template by :templateId",
"sendConfigurationId": "ID of configuration used to create mailer instance"
} }

View file

@ -964,6 +964,11 @@
"thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter",
"thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter",
"thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number", "thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number",
"thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character" "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character",
"templateData": "Data passed to template when compiling with Handlebars",
"templateVariables": "Map of template/subject variables to replace",
"sendTransactionalEmail": "Send transactional email",
"sendSingleEmailByTemplateId": "Send single template by :templateId",
"sendConfigurationId": "ID of configuration used to create mailer instance"
} }

View file

@ -1,3 +1,4 @@
{ {
"extends": "nodemailer" "extends": "nodemailer",
"parserOptions": { "ecmaVersion": 2018 }
} }

View file

@ -8,6 +8,10 @@ const fs = require('fs-extra-promise');
const tryRequire = require('try-require'); const tryRequire = require('try-require');
const posix = tryRequire('posix'); const posix = tryRequire('posix');
// process.getuid and process.getgid are not supported on Windows
process.getuid = process.getuid || (() => 100);
process.getgid = process.getuid || (() => 100);
function _getConfigUidGid(userKey, groupKey, defaultUid, defaultGid) { function _getConfigUidGid(userKey, groupKey, defaultUid, defaultGid) {
let uid = defaultUid; let uid = defaultUid;
let gid = defaultGid; let gid = defaultGid;

View file

@ -0,0 +1,79 @@
'use strict';
const mailers = require('./mailers');
const tools = require('./tools');
const templates = require('../models/templates');
class TemplateSender {
constructor(options) {
this.defaultOptions = {
maxMails: 100,
...options
};
}
async send(params) {
const options = { ...this.defaultOptions, ...params };
this._validateMailOptions(options);
const [mailer, template] = await Promise.all([
mailers.getOrCreateMailer(
options.sendConfigurationId
),
templates.getById(
options.context,
options.templateId,
false
)
]);
const html = tools.formatTemplate(
template.html,
null,
options.variables,
true
);
const subject = tools.formatTemplate(
options.subject || template.description || template.name,
options.variables
);
return mailer.sendTransactionalMail(
{
to: options.email,
subject
},
{
html: { template: html },
data: options.data,
locale: options.locale
}
);
}
_validateMailOptions(options) {
let { context, email, locale, templateId } = options;
if (!templateId) {
throw new Error('Missing templateId');
}
if (!context) {
throw new Error('Missing context');
}
if (!email || email.length === 0) {
throw new Error('Missing email');
}
if (typeof email === 'string') {
email = email.split(',');
}
if (email.length > options.maxMails) {
throw new Error(
`Cannot send more than ${options.maxMails} emails at once`
);
}
if (!locale) {
throw new Error('Missing locale');
}
}
}
module.exports = TemplateSender;

View file

@ -14,6 +14,7 @@ const mjml2html = require('mjml');
const hbs = require('hbs'); const hbs = require('hbs');
const juice = require('juice'); const juice = require('juice');
const he = require('he'); const he = require('he');
const htmlToText = require('html-to-text');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -148,14 +149,21 @@ function validateEmailGetMessage(result, address, language) {
function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) { function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) {
const links = getMessageLinks(campaign, list, subscription); const links = getMessageLinks(campaign, list, subscription);
return formatTemplate(message, links, mergeTags, isHTML);
}
function formatTemplate(template, links, mergeTags, isHTML) {
if (!links && !mergeTags) { return template; }
const getValue = fullKey => { const getValue = fullKey => {
const keys = (fullKey || '').split('.'); const keys = (fullKey || '').split('.');
if (links.hasOwnProperty(keys[0])) { if (links && links.hasOwnProperty(keys[0])) {
return links[keys[0]]; return links[keys[0]];
} }
if (!mergeTags) { return false; }
let value = mergeTags; let value = mergeTags;
while (keys.length > 0) { while (keys.length > 0) {
let key = keys.shift(); let key = keys.shift();
@ -173,7 +181,7 @@ function formatMessage(campaign, list, subscription, mergeTags, message, isHTML)
}) : (containsHTML ? htmlToText.fromString(value) : value); }) : (containsHTML ? htmlToText.fromString(value) : value);
}; };
return message.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => { return template.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
let value = getValue(identifier); let value = getValue(identifier);
if (value === false) { if (value === false) {
return match; return match;
@ -229,6 +237,7 @@ module.exports = {
getTemplate, getTemplate,
prepareHtml, prepareHtml,
getMessageLinks, getMessageLinks,
formatMessage formatMessage,
formatTemplate
}; };

View file

@ -16,6 +16,7 @@ const contextHelpers = require('../lib/context-helpers');
const shares = require('../models/shares'); const shares = require('../models/shares');
const slugify = require('slugify'); const slugify = require('slugify');
const passport = require('../lib/passport'); const passport = require('../lib/passport');
const TemplateSender = require('../lib/template-sender');
const campaigns = require('../models/campaigns'); const campaigns = require('../models/campaigns');
class APIError extends Error { class APIError extends Error {
@ -285,5 +286,34 @@ router.getAsync('/rss/fetch/:campaignCid', passport.loggedIn, async (req, res) =
return res.json(); return res.json();
}); });
router.postAsync('/templates/:templateId/send', async (req, res) => {
const input = {};
Object.keys(req.body).forEach(key => {
input[
(key || '')
.toString()
.trim()
.toUpperCase()
] = req.body[key] || '';
});
try {
const templateSender = new TemplateSender({
context: req.context,
locale: req.locale,
templateId: req.params.templateId
});
const info = await templateSender.send({
data: input.DATA,
email: input.EMAIL,
sendConfigurationId: input.SEND_CONFIGURATION_ID,
subject: input.SUBJECT,
variables: input.VARIABLES
});
res.status(200).json({ data: info });
} catch (e) {
throw new APIError(e.message, 400);
}
});
module.exports = router; module.exports = router;