diff --git a/.gitignore b/.gitignore index e8fec96e..96ac666e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.idea +/.vscode /last-failed-e2e-test.* node_modules diff --git a/client/src/account/API.js b/client/src/account/API.js index a9d1ac9e..724433ec 100644 --- a/client/src/account/API.js +++ b/client/src/account/API.js @@ -368,6 +368,37 @@ export default class API extends Component {

curl -XGET '{getUrl(`api/rss/fetch/5OOnZKrp0?access_token=${accessToken}`)}'
+ +

POST /api/templates/:templateId/send – {t('sendTransactionalEmail')}

+ +

+ {t('sendSingleEmailByTemplateId')} +

+ +

+ GET {t('arguments')} +

+ + +

+ POST {t('arguments')} +

+ + +

+ {t('example')} +

+ +
curl -XPOST '{getUrl(`api/templates/1/send?access_token={accessToken}`)}' \
+--data 'EMAIL=test@example.com&SUBJECT=Test&VARIABLES[FOO]=bar&VARIABLES[TEST]=example'
); } diff --git a/locales/en-US/common.json b/locales/en-US/common.json index ee60a4de..7dcce754 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -964,5 +964,10 @@ "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "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" } \ No newline at end of file diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 980002e1..e26f9507 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -964,6 +964,11 @@ "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "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" } \ No newline at end of file diff --git a/server/.eslintrc b/server/.eslintrc index 91926fcd..3cf464f1 100644 --- a/server/.eslintrc +++ b/server/.eslintrc @@ -1,3 +1,4 @@ { - "extends": "nodemailer" + "extends": "nodemailer", + "parserOptions": { "ecmaVersion": 2018 } } diff --git a/server/lib/privilege-helpers.js b/server/lib/privilege-helpers.js index 14a2f00d..d1f4671c 100644 --- a/server/lib/privilege-helpers.js +++ b/server/lib/privilege-helpers.js @@ -8,6 +8,10 @@ const fs = require('fs-extra-promise'); const tryRequire = require('try-require'); 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) { let uid = defaultUid; let gid = defaultGid; diff --git a/server/lib/template-sender.js b/server/lib/template-sender.js new file mode 100644 index 00000000..bcbd4db5 --- /dev/null +++ b/server/lib/template-sender.js @@ -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; diff --git a/server/lib/tools.js b/server/lib/tools.js index c7d35f28..0677e054 100644 --- a/server/lib/tools.js +++ b/server/lib/tools.js @@ -14,6 +14,7 @@ const mjml2html = require('mjml'); const hbs = require('hbs'); const juice = require('juice'); const he = require('he'); +const htmlToText = require('html-to-text'); const fs = require('fs-extra'); @@ -148,14 +149,21 @@ function validateEmailGetMessage(result, address, language) { function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) { 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 keys = (fullKey || '').split('.'); - if (links.hasOwnProperty(keys[0])) { + if (links && links.hasOwnProperty(keys[0])) { return links[keys[0]]; } + if (!mergeTags) { return false; } + let value = mergeTags; while (keys.length > 0) { let key = keys.shift(); @@ -173,7 +181,7 @@ function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) }) : (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); if (value === false) { return match; @@ -229,6 +237,7 @@ module.exports = { getTemplate, prepareHtml, getMessageLinks, - formatMessage + formatMessage, + formatTemplate }; diff --git a/server/routes/api.js b/server/routes/api.js index 2dc81884..ebb822e4 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -16,6 +16,7 @@ const contextHelpers = require('../lib/context-helpers'); const shares = require('../models/shares'); const slugify = require('slugify'); const passport = require('../lib/passport'); +const TemplateSender = require('../lib/template-sender'); const campaigns = require('../models/campaigns'); class APIError extends Error { @@ -285,5 +286,34 @@ router.getAsync('/rss/fetch/:campaignCid', passport.loggedIn, async (req, res) = 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;