From 913c7dc337658b3ff5950d16d4dbeeabab73d2e4 Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Sun, 17 Mar 2019 17:49:25 +0200 Subject: [PATCH 1/7] Added .vscode to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) 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 From 4a521a8f0f6370197f7d4fd618a2b4010839344b Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Sun, 24 Mar 2019 14:27:56 +0200 Subject: [PATCH 2/7] Implemented basic transactional emails API --- server/app-builder.js | 2 +- server/lib/privilege-helpers.js | 4 ++ server/lib/template-sender.js | 75 +++++++++++++++++++++++++++++++++ server/routes/api.js | 27 ++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 server/lib/template-sender.js diff --git a/server/app-builder.js b/server/app-builder.js index a9243f4a..2733d8eb 100644 --- a/server/app-builder.js +++ b/server/app-builder.js @@ -348,7 +348,7 @@ function createApp(appType) { data: [] }; - return status(err.status || 500).json(resp); + return res.status(err.status || 500).json(resp); } else { if (err instanceof interoperableErrors.NotLoggedInError) { 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..40ccd0b8 --- /dev/null +++ b/server/lib/template-sender.js @@ -0,0 +1,75 @@ +'use strict'; + +const contextHelpers = require('./context-helpers'); +const mailers = require('./mailers'); +const templates = require('../models/templates'); + +class TemplateSender { + constructor({ templateId, maxMails = 100 } = {}) { + if (!templateId) { + throw new Error('Cannot create template sender without templateId'); + } + + this.templateId = templateId; + this.maxMails = maxMails; + } + + async send(options) { + this._validateMailOptions(options); + + const [mailer, template] = await Promise.all([ + mailers.getOrCreateMailer(), + templates.getById( + contextHelpers.getAdminContext(), + this.templateId, + false + ) + ]); + + const html = this._substituteVariables( + template.html, + options.variables + ); + return mailer.sendTransactionalMail( + { + to: options.email, + subject: options.subject + }, + { + html: { template: html }, + locale: options.locale + } + ); + } + + _validateMailOptions(options) { + let { email, locale } = options; + + if (!email || email.length === 0) { + throw new Error('Missing email'); + } + if (typeof email === 'string') { + email = email.split(','); + } + if (email.length > this.maxMails) { + throw new Error( + `Cannot send more than ${this.maxMails} emails at once` + ); + } + if (!locale) { + throw new Error('Missing locale'); + } + } + + _substituteVariables(html, variables) { + if (!variables) return html; + return Object.keys(variables).reduce((res, key) => { + return res.replace( + new RegExp(`\\[${key}\\]`, 'gmi'), + variables[key] + ); + }, html); + } +} + +module.exports = TemplateSender; diff --git a/server/routes/api.js b/server/routes/api.js index 2dc81884..9fcc2863 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,31 @@ 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({ + templateId: req.params.templateId + }); + const info = await templateSender.send({ + email: input.EMAIL, + subject: input.SUBJECT, + locale: req.locale, + variables: input.VARIABLES + }); + res.status(200).json({ data: info }); + } catch (e) { + throw new APIError(e.message, 400); + } +}); module.exports = router; From ed4a13fef79607c0d862aeec4257e5e8b9d685b2 Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Sun, 31 Mar 2019 13:07:29 +0300 Subject: [PATCH 3/7] Added transactional mail api docs --- client/src/account/API.js | 29 +++++++++++++++++++++++++++++ locales/en-US/common.json | 5 ++++- locales/es-ES/common.json | 5 ++++- 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/client/src/account/API.js b/client/src/account/API.js index a9d1ac9e..b57c04d4 100644 --- a/client/src/account/API.js +++ b/client/src/account/API.js @@ -368,6 +368,35 @@ 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')} +

+
    +
  • access_token – {t('yourPersonalAccessToken')}
  • +
+ +

+ POST {t('arguments')} +

+
    +
  • EMAIL – {t('emailAddress')} ({t('required')})
  • +
  • SUBJECT – {t('subject')}
  • +
  • VARIABLES – {t('templateVariables')}: {"{"} FOO: bar {"}"}
  • +
+ +

+ {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..a5a10b28 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -964,5 +964,8 @@ "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", + "templateVariables": "Map of template variables to replace", + "sendTransactionalEmail": "Send transactional email", + "sendSingleEmailByTemplateId": "Send single template by :templateId" } \ No newline at end of file diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 980002e1..4a83ea90 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -964,6 +964,9 @@ "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", + "templateVariables": "Map of template variables to replace", + "sendTransactionalEmail": "Send transactional email", + "sendSingleEmailByTemplateId": "Send single template by :templateId" } \ No newline at end of file From 80279346f346dea3a1490804c80f151b2b478967 Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Sun, 31 Mar 2019 15:50:40 +0300 Subject: [PATCH 4/7] Transactional mail: code review fixes --- client/src/account/API.js | 1 + locales/en-US/common.json | 3 ++- locales/es-ES/common.json | 3 ++- server/lib/template-sender.js | 10 ++++++---- server/routes/api.js | 4 +++- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/client/src/account/API.js b/client/src/account/API.js index b57c04d4..56af2106 100644 --- a/client/src/account/API.js +++ b/client/src/account/API.js @@ -387,6 +387,7 @@ export default class API extends Component {

  • EMAIL – {t('emailAddress')} ({t('required')})
  • +
  • SEND_CONFIGURATION_ID – {t('sendConfigurationId')}
  • SUBJECT – {t('subject')}
  • VARIABLES – {t('templateVariables')}: {"{"} FOO: bar {"}"}
diff --git a/locales/en-US/common.json b/locales/en-US/common.json index a5a10b28..20d5b6d9 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -967,5 +967,6 @@ "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character", "templateVariables": "Map of template variables to replace", "sendTransactionalEmail": "Send transactional email", - "sendSingleEmailByTemplateId": "Send single template by :templateId" + "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 4a83ea90..247b23b5 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -967,6 +967,7 @@ "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character", "templateVariables": "Map of template variables to replace", "sendTransactionalEmail": "Send transactional email", - "sendSingleEmailByTemplateId": "Send single template by :templateId" + "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/lib/template-sender.js b/server/lib/template-sender.js index 40ccd0b8..5ea4db67 100644 --- a/server/lib/template-sender.js +++ b/server/lib/template-sender.js @@ -1,6 +1,5 @@ 'use strict'; -const contextHelpers = require('./context-helpers'); const mailers = require('./mailers'); const templates = require('../models/templates'); @@ -18,9 +17,9 @@ class TemplateSender { this._validateMailOptions(options); const [mailer, template] = await Promise.all([ - mailers.getOrCreateMailer(), + mailers.getOrCreateMailer(options.sendConfigurationId), templates.getById( - contextHelpers.getAdminContext(), + options.context, this.templateId, false ) @@ -43,8 +42,11 @@ class TemplateSender { } _validateMailOptions(options) { - let { email, locale } = options; + let { context, email, locale } = options; + if (!context) { + throw new Error('Missing context'); + } if (!email || email.length === 0) { throw new Error('Missing email'); } diff --git a/server/routes/api.js b/server/routes/api.js index 9fcc2863..60fa8a62 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -302,9 +302,11 @@ router.postAsync('/templates/:templateId/send', async (req, res) => { templateId: req.params.templateId }); const info = await templateSender.send({ + context: req.context, email: input.EMAIL, - subject: input.SUBJECT, locale: req.locale, + sendConfigurationId: input.SEND_CONFIGURATION_ID, + subject: input.SUBJECT, variables: input.VARIABLES }); res.status(200).json({ data: info }); From 76b4f8b8c2e862ac411430de623e2e76d82267d1 Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Tue, 2 Apr 2019 14:38:12 +0300 Subject: [PATCH 5/7] Transactional mail: added data rendering --- client/src/account/API.js | 3 ++- locales/en-US/common.json | 3 ++- locales/es-ES/common.json | 3 ++- server/.eslintrc | 3 ++- server/lib/template-sender.js | 28 +++++++++++++++------------- server/routes/api.js | 1 + 6 files changed, 24 insertions(+), 17 deletions(-) diff --git a/client/src/account/API.js b/client/src/account/API.js index 56af2106..724433ec 100644 --- a/client/src/account/API.js +++ b/client/src/account/API.js @@ -389,7 +389,8 @@ export default class API extends Component {
  • EMAIL – {t('emailAddress')} ({t('required')})
  • SEND_CONFIGURATION_ID – {t('sendConfigurationId')}
  • SUBJECT – {t('subject')}
  • -
  • VARIABLES – {t('templateVariables')}: {"{"} FOO: bar {"}"}
  • +
  • DATA – {t('templateData')}: {'{'} "any": ["type", {'{'}"of": "data"{'}'}] {'}'}
  • +
  • VARIABLES – {t('templateVariables')}: {'{'} "FOO": "bar" {'}'}
  • diff --git a/locales/en-US/common.json b/locales/en-US/common.json index 20d5b6d9..7dcce754 100644 --- a/locales/en-US/common.json +++ b/locales/en-US/common.json @@ -965,7 +965,8 @@ "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", - "templateVariables": "Map of template variables to replace", + "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" diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json index 247b23b5..e26f9507 100644 --- a/locales/es-ES/common.json +++ b/locales/es-ES/common.json @@ -965,7 +965,8 @@ "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", - "templateVariables": "Map of template variables to replace", + "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" diff --git a/server/.eslintrc b/server/.eslintrc index 91926fcd..360c05cb 100644 --- a/server/.eslintrc +++ b/server/.eslintrc @@ -1,3 +1,4 @@ { - "extends": "nodemailer" + "extends": "nodemailer", + "parserOptions": { "ecmaVersion": 2017 } } diff --git a/server/lib/template-sender.js b/server/lib/template-sender.js index 5ea4db67..d077f1b7 100644 --- a/server/lib/template-sender.js +++ b/server/lib/template-sender.js @@ -18,24 +18,25 @@ class TemplateSender { const [mailer, template] = await Promise.all([ mailers.getOrCreateMailer(options.sendConfigurationId), - templates.getById( - options.context, - this.templateId, - false - ) + templates.getById(options.context, this.templateId, false) ]); const html = this._substituteVariables( template.html, options.variables ); + const subject = this._substituteVariables( + options.subject || template.description || template.name, + options.variables + ); return mailer.sendTransactionalMail( { to: options.email, - subject: options.subject + subject }, { html: { template: html }, + data: options.data, locale: options.locale } ); @@ -64,13 +65,14 @@ class TemplateSender { } _substituteVariables(html, variables) { - if (!variables) return html; - return Object.keys(variables).reduce((res, key) => { - return res.replace( - new RegExp(`\\[${key}\\]`, 'gmi'), - variables[key] - ); - }, html); + if (!variables) { + return html; + } + return Object.keys(variables).reduce( + (res, key) => + res.replace(new RegExp(`\\[${key}\\]`, 'gmi'), variables[key]), + html + ); } } diff --git a/server/routes/api.js b/server/routes/api.js index 60fa8a62..69a6b10b 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -303,6 +303,7 @@ router.postAsync('/templates/:templateId/send', async (req, res) => { }); const info = await templateSender.send({ context: req.context, + data: input.DATA, email: input.EMAIL, locale: req.locale, sendConfigurationId: input.SEND_CONFIGURATION_ID, From e588e218b6ec90b12be34ec9f8e15298f6ec134e Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Tue, 2 Apr 2019 16:15:35 +0300 Subject: [PATCH 6/7] Transactional mail: use tools to format message --- server/lib/template-sender.js | 20 ++++++-------------- server/lib/tools.js | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/server/lib/template-sender.js b/server/lib/template-sender.js index d077f1b7..0413dfe5 100644 --- a/server/lib/template-sender.js +++ b/server/lib/template-sender.js @@ -1,6 +1,7 @@ 'use strict'; const mailers = require('./mailers'); +const tools = require('./tools'); const templates = require('../models/templates'); class TemplateSender { @@ -21,11 +22,13 @@ class TemplateSender { templates.getById(options.context, this.templateId, false) ]); - const html = this._substituteVariables( + const html = tools.formatTemplate( template.html, - options.variables + null, + options.variables, + true ); - const subject = this._substituteVariables( + const subject = tools.formatTemplate( options.subject || template.description || template.name, options.variables ); @@ -63,17 +66,6 @@ class TemplateSender { throw new Error('Missing locale'); } } - - _substituteVariables(html, variables) { - if (!variables) { - return html; - } - return Object.keys(variables).reduce( - (res, key) => - res.replace(new RegExp(`\\[${key}\\]`, 'gmi'), variables[key]), - html - ); - } } 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 }; From 8b39a101cd312d1869c038e432e24048267b1db1 Mon Sep 17 00:00:00 2001 From: Alexey Zinkevych Date: Tue, 2 Apr 2019 16:35:57 +0300 Subject: [PATCH 7/7] Transactional mail: minor template-sender refactoring --- server/.eslintrc | 2 +- server/lib/template-sender.js | 34 +++++++++++++++++++++------------- server/routes/api.js | 4 ++-- 3 files changed, 24 insertions(+), 16 deletions(-) diff --git a/server/.eslintrc b/server/.eslintrc index 360c05cb..3cf464f1 100644 --- a/server/.eslintrc +++ b/server/.eslintrc @@ -1,4 +1,4 @@ { "extends": "nodemailer", - "parserOptions": { "ecmaVersion": 2017 } + "parserOptions": { "ecmaVersion": 2018 } } diff --git a/server/lib/template-sender.js b/server/lib/template-sender.js index 0413dfe5..bcbd4db5 100644 --- a/server/lib/template-sender.js +++ b/server/lib/template-sender.js @@ -5,21 +5,26 @@ const tools = require('./tools'); const templates = require('../models/templates'); class TemplateSender { - constructor({ templateId, maxMails = 100 } = {}) { - if (!templateId) { - throw new Error('Cannot create template sender without templateId'); - } - - this.templateId = templateId; - this.maxMails = maxMails; + constructor(options) { + this.defaultOptions = { + maxMails: 100, + ...options + }; } - async send(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, this.templateId, false) + mailers.getOrCreateMailer( + options.sendConfigurationId + ), + templates.getById( + options.context, + options.templateId, + false + ) ]); const html = tools.formatTemplate( @@ -46,8 +51,11 @@ class TemplateSender { } _validateMailOptions(options) { - let { context, email, locale } = options; + let { context, email, locale, templateId } = options; + if (!templateId) { + throw new Error('Missing templateId'); + } if (!context) { throw new Error('Missing context'); } @@ -57,9 +65,9 @@ class TemplateSender { if (typeof email === 'string') { email = email.split(','); } - if (email.length > this.maxMails) { + if (email.length > options.maxMails) { throw new Error( - `Cannot send more than ${this.maxMails} emails at once` + `Cannot send more than ${options.maxMails} emails at once` ); } if (!locale) { diff --git a/server/routes/api.js b/server/routes/api.js index 69a6b10b..ebb822e4 100644 --- a/server/routes/api.js +++ b/server/routes/api.js @@ -299,13 +299,13 @@ router.postAsync('/templates/:templateId/send', async (req, res) => { try { const templateSender = new TemplateSender({ + context: req.context, + locale: req.locale, templateId: req.params.templateId }); const info = await templateSender.send({ - context: req.context, data: input.DATA, email: input.EMAIL, - locale: req.locale, sendConfigurationId: input.SEND_CONFIGURATION_ID, subject: input.SUBJECT, variables: input.VARIABLES