diff --git a/client/src/campaigns/Content.js b/client/src/campaigns/Content.js index d7f91576..4d218cd8 100644 --- a/client/src/campaigns/Content.js +++ b/client/src/campaigns/Content.js @@ -229,7 +229,8 @@ export default class CustomContent extends Component { return { html: exportedData.data_sourceCustom_html, - text: this.getFormValue('data_sourceCustom_text') + text: this.getFormValue('data_sourceCustom_text'), + tagLanguage: this.getFormValue('data_sourceCustom_tag_language') }; } diff --git a/client/src/campaigns/Status.js b/client/src/campaigns/Status.js index c0965af3..a348daa0 100644 --- a/client/src/campaigns/Status.js +++ b/client/src/campaigns/Status.js @@ -148,7 +148,7 @@ class SendControls extends Component { const entity = this.props.entity; if (entity.scheduled) { - const date = moment(entity.scheduled); + const date = moment.utc(entity.scheduled); this.populateFormValues({ sendLater: true, date: date.format('YYYY-MM-DD'), diff --git a/client/src/campaigns/TestSendModalDialog.js b/client/src/campaigns/TestSendModalDialog.js index 894c612e..274b209c 100644 --- a/client/src/campaigns/TestSendModalDialog.js +++ b/client/src/campaigns/TestSendModalDialog.js @@ -97,6 +97,7 @@ export class TestSendModalDialog extends Component { const contentData = await this.props.getDataAsync(); data.html = contentData.html; data.text = contentData.text; + data.tagLanguage = contentData.tagLanguage; } if (mode === TestSendModalDialogMode.TEMPLATE) { @@ -202,7 +203,7 @@ export class TestSendModalDialog extends Component { const target = this.getFormValue('target'); const mode = this.props.mode; - if (mode === TestSendModalDialogMode.CAMPAIGN_STATUS) { + if (mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) { const targetOpts = [ {key: Target.CAMPAIGN_ONE, label: t('Single test user of the campaign')}, {key: Target.CAMPAIGN_ALL, label: t('All test users of the campaign')}, diff --git a/client/src/templates/CUD.js b/client/src/templates/CUD.js index 16d65b42..6042e48b 100644 --- a/client/src/templates/CUD.js +++ b/client/src/templates/CUD.js @@ -262,7 +262,8 @@ export default class CUD extends Component { return { html: exportedData.html, - text: this.getFormValue('text') + text: this.getFormValue('text'), + tagLanguage: this.getFormValue('tag_language') }; } diff --git a/client/src/templates/helpers.js b/client/src/templates/helpers.js index a99f8bdb..52d7a319 100644 --- a/client/src/templates/helpers.js +++ b/client/src/templates/helpers.js @@ -19,7 +19,7 @@ import {getSandboxUrl} from "../lib/urls"; import mailtrainConfig from 'mailtrainConfig'; import {ActionLink, Button} from "../lib/bootstrap-components"; import {Trans} from "react-i18next"; -import {TagLanguages} from "../../../shared/templates"; +import {TagLanguages, renderTag} from "../../../shared/templates"; import styles from "../lib/styles.scss"; @@ -507,6 +507,39 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM export function getEditForm(owner, typeKey, prefix = '') { const t = owner.props.t; + const tagLanguage = owner.getFormValue(prefix + 'tag_language'); + + const tg = tag => renderTag(tagLanguage, tag); + + let instructions = null; + if (tagLanguage === TagLanguages.SIMPLE) { + instructions = ( + <> + +

Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: {tg('TAG_NAME')} or [TAG_NAME/fallback] where fallback is an optional text value used when TAG_NAME is empty.

+
+ +

You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.

+
+ + ); + } else if (tagLanguage === TagLanguages.HBS) { + instructions = ( + <> + +

Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: {tg('TAG_NAME')}.

+
+ +

You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.

+
+ +

The whole message is interpreted as Handlebars template (see http://handlebarsjs.com/). You can use any Handlebars blocks and expressions + in the template. The merge tags form the root context of the Handlebars template.

+
+ + ); + } + return (
@@ -516,12 +549,7 @@ export function getEditForm(owner, typeKey, prefix = '') { label={t('mergeTagReference')}/> {owner.state.showMergeTagReference &&
- -

Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: [TAG_NAME] or [TAG_NAME/fallback] where fallback is an optional text value used when TAG_NAME is empty.

-
- -

You can use any of the standard merge tags below. In addition to that every custom field has its own merge tag. Check the fields of the list you are going to send to.

-
+ {instructions} @@ -536,7 +564,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [LINK_UNSUBSCRIBE] + {tg('LINK_UNSUBSCRIBE')} URL that points to the unsubscribe page @@ -544,7 +572,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [LINK_PREFERENCES] + {tg('LINK_PREFERENCES')} URL that points to the preferences page of the subscriber @@ -552,7 +580,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [LINK_BROWSER] + {tg('LINK_BROWSER')} URL to preview the message in a browser @@ -560,7 +588,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [EMAIL] + {tg('EMAIL')} Email address @@ -568,7 +596,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [SUBSCRIPTION_ID] + {tg('SUBSCRIPTION_ID')} Unique ID that identifies the recipient @@ -576,7 +604,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [LIST_ID] + {tg('LIST_ID')} Unique ID that identifies the list used for this campaign @@ -584,7 +612,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [CAMPAIGN_ID] + {tg('CAMPAIGN_ID')} Unique ID that identifies current campaign @@ -609,7 +637,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [RSS_ENTRY_TITLE] + {tg('RSS_ENTRY_TITLE')} RSS entry title @@ -617,7 +645,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [RSS_ENTRY_DATE] + {tg('RSS_ENTRY_DATE')} RSS entry date @@ -625,7 +653,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [RSS_ENTRY_LINK] + {tg('RSS_ENTRY_LINK')} RSS entry link @@ -633,7 +661,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [RSS_ENTRY_CONTENT] + {tg('RSS_ENTRY_CONTENT')} Content of an RSS entry @@ -641,7 +669,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [RSS_ENTRY_SUMMARY] + {tg('RSS_ENTRY_SUMMARY')} RSS entry summary @@ -649,7 +677,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
- [RSS_ENTRY_IMAGE_URL] + {tg('RSS_ENTRY_IMAGE_URL')} RSS entry image URL diff --git a/mvis/ivis-core b/mvis/ivis-core index ec89af43..fca588ec 160000 --- a/mvis/ivis-core +++ b/mvis/ivis-core @@ -1 +1 @@ -Subproject commit ec89af43120f95dcf7f14dc92a2b3811bf50d7e8 +Subproject commit fca588ecbe7df717d2219bac82acdc3da1bd6f40 diff --git a/server/lib/message-sender.js b/server/lib/message-sender.js index c19448a2..c01000f8 100644 --- a/server/lib/message-sender.js +++ b/server/lib/message-sender.js @@ -13,7 +13,7 @@ const fields = require('../models/fields'); const sendConfigurations = require('../models/send-configurations'); const links = require('../models/links'); const {CampaignSource, CampaignType} = require('../../shared/campaigns'); -const {SubscriptionStatus} = require('../../shared/lists'); +const {SubscriptionStatus, toNameTagLangauge} = require('../../shared/lists'); const tools = require('./tools'); const htmlToText = require('html-to-text'); const request = require('request-promise'); @@ -39,7 +39,7 @@ class MessageSender { settings is one of: - campaignCid / campaignId or - - sendConfiguration, listId, attachments, html, text, subject + - sendConfiguration, listId, attachments, html, text, subject, tagLanguage */ async _init(settings) { this.type = settings.type; @@ -58,14 +58,6 @@ class MessageSender { this.isMassMail = true; } else if (this.type === MessageType.TEST) { - // We are not within scope of a campaign (i.e. templates in MessageType.TEST message) - // This is to fake the campaign for getMessageLinks, which is called inside formatMessage - this.campaign = { - cid: '[CAMPAIGN_ID]', - from_name_override: null, - from_email_override: null, - reply_to_override: null - }; this.isMassMail = true; } else { @@ -89,6 +81,12 @@ class MessageSender { this.listsByCid.set(list.cid, list); this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id)); + } else if (settings.listCid) { + const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), settings.listCid); + this.listsById.set(list.id, list); + this.listsByCid.set(list.cid, list); + this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id)); + } else if (this.campaign && this.campaign.lists) { for (const listSpec of this.campaign.lists) { const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list); @@ -123,11 +121,22 @@ class MessageSender { } else if (settings.html !== undefined) { this.html = settings.html; this.text = settings.text; + this.tagLanguage = settings.tagLanguage; } else if (this.campaign && this.campaign.source === CampaignSource.TEMPLATE) { this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false); + this.html = this.template.html; + this.text = this.template.text; + this.tagLanguage = this.template.tag_language; + + } else if (this.campaign && (this.campaign.source === CampaignSource.CUSTOM || this.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || this.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN)) { + this.html = this.campaign.data.sourceCustom.html; + this.text = this.campaign.data.sourceCustom.text; + this.tagLanguage = this.campaign.data.sourceCustom.tag_language; } + enforce(this.renderedHtml || (this.campaign && this.campaign.source === CampaignSource.URL) || this.tagLanguage); + if (settings.subject !== undefined) { this.subject = settings.subject; } else if (this.campaign && this.campaign.subject !== undefined) { @@ -155,40 +164,25 @@ class MessageSender { text = this.text; renderTags = true; - } else if (campaign) { - if (campaign.source === CampaignSource.URL) { - const form = tools.getMessageLinks(campaign, list, subscriptionGrouped); - for (const key in mergeTags) { - form[key] = mergeTags[key]; - } - - const response = await request.post({ - uri: campaign.sourceUrl, - form, - resolveWithFullResponse: true - }); - - if (response.statusCode !== 200) { - throw new Error(`Received status code ${httpResponse.statusCode} from ${campaign.sourceUrl}`); - } - - html = response.body; - text = ''; - renderTags = false; - - } else if (campaign.source === CampaignSource.CUSTOM || campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN || campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE) { - html = campaign.data.sourceCustom.html; - text = campaign.data.sourceCustom.text; - renderTags = true; - - } else if (campaign.source === CampaignSource.TEMPLATE) { - const template = this.template; - html = template.html; - text = template.text; - renderTags = true; + } else if (campaign && campaign.source === CampaignSource.URL) { + const form = tools.getMessageLinks(campaign, list, subscriptionGrouped); + for (const key in mergeTags) { + form[key] = mergeTags[key]; } - html = await links.updateLinks(campaign, list, subscriptionGrouped, mergeTags, html); + const response = await request.post({ + uri: campaign.sourceUrl, + form, + resolveWithFullResponse: true + }); + + if (response.statusCode !== 200) { + throw new Error(`Received status code ${httpResponse.statusCode} from ${campaign.sourceUrl}`); + } + + html = response.body; + text = ''; + renderTags = false; } const attachments = this.attachments.slice(); @@ -204,11 +198,23 @@ class MessageSender { }); } - html = renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, true) : html; - text = (text || '').trim() - ? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text) - : htmlToText.fromString(html, {wordwrap: 130}); + if (renderTags) { + if (this.campaign) { + html = await links.updateLinks(html, this.tagLanguage, mergeTags, campaign, list, subscriptionGrouped); + } + + html = tools.formatCampaignTemplate(html, this.tagLanguage, mergeTags, true, campaign, list, subscriptionGrouped); + } + + const generateText = !!(text || '').trim(); + if (generateText) { + text = htmlToText.fromString(html, {wordwrap: 130}); + } else { + if (renderTags) { + text = tools.formatCampaignTemplate(text, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped) + } + } return { html, @@ -220,7 +226,7 @@ class MessageSender { _getExtraTags(campaign) { const tags = {}; - if (campaign.type === CampaignType.RSS_ENTRY) { + if (campaign && campaign.type === CampaignType.RSS_ENTRY) { const rssEntry = campaign.data.rssEntry; tags['RSS_ENTRY_TITLE'] = rssEntry.title; tags['RSS_ENTRY_DATE'] = rssEntry.date; @@ -234,26 +240,10 @@ class MessageSender { return tags; } - async initByCampaignCid(campaignCid) { - await this._init({type: MessageType.REGULAR, campaignCid}); - } - async initByCampaignId(campaignId) { await this._init({type: MessageType.REGULAR, campaignId}); } - async getMessage(listCid, subscriptionCid) { - enforce(this.type === MessageType.REGULAR); - - const list = this.listsByCid.get(listCid); - const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid); - const flds = this.listsFieldsGrouped.get(list.id); - const campaign = this.campaign; - const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign)); - - return await this._getMessage(list, subscriptionGrouped, mergeTags, false); - } - /* subData is one of: @@ -304,55 +294,59 @@ class MessageSender { message = await this._getMessage(list, subscriptionGrouped, mergeTags, true); - const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.'); - let listUnsubscribe = null; if (!list.listunsubscribe_disabled) { - listUnsubscribe = campaign.unsubscribe_url - ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, campaign.unsubscribe_url) + listUnsubscribe = campaign && campaign.unsubscribe_url + ? tools.formatCampaignTemplate(campaign.unsubscribe_url, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped) : getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.cid); } to = { - name: list.to_name === null ? undefined : tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false), + name: list.to_name === null ? undefined : tools.formatCampaignTemplate(list.to_name, toNameTagLangauge, mergeTags, false, campaign, list, subscriptionGrouped), address: subscriptionGrouped.email }; - subject = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, this.subject, false); + subject = this.subject; - if (this.useVerp) { - envelope = { - from: campaignAddress + '@' + sendConfiguration.verp_hostname, - to: subscriptionGrouped.email - }; - } - - if (this.useVerpSenderHeader) { - sender = campaignAddress + '@' + sendConfiguration.verp_hostname; + if (this.tagLanguage) { + subject = tools.formatCampaignTemplate(this.subject, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped); } headers = { - 'x-fbl': campaignAddress, - // custom header for SparkPost - 'x-msys-api': JSON.stringify({ - campaign_id: campaignAddress - }), - // custom header for SendGrid - 'x-smtpapi': JSON.stringify({ - unique_args: { - campaign_id: campaignAddress - } - }), - // custom header for Mailgun - 'x-mailgun-variables': JSON.stringify({ - campaign_id: campaignAddress - }), 'List-ID': { prepared: true, value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>' } }; + if (campaign) { + const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.'); + + if (this.useVerp) { + envelope = { + from: campaignAddress + '@' + sendConfiguration.verp_hostname, + to: subscriptionGrouped.email + }; + } + + if (this.useVerpSenderHeader) { + sender = campaignAddress + '@' + sendConfiguration.verp_hostname; + } + + headers['x-fbl'] = campaignAddress; + headers['x-msys-api'] = JSON.stringify({ + campaign_id: campaignAddress + }); + headers['x-smtpapi'] = JSON.stringify({ + unique_args: { + campaign_id: campaignAddress + } + }); + headers['x-mailgun-variables'] = JSON.stringify({ + campaign_id: campaignAddress + }); + } + listHeader = { unsubscribe: listUnsubscribe }; @@ -375,7 +369,7 @@ class MessageSender { await mailer.throttleWait(); const getOverridable = key => { - if (campaign && sendConfiguration[key + '_overridable'] && this.campaign[key + '_override'] !== null) { + if (campaign && sendConfiguration[key + '_overridable'] && campaign[key + '_override'] !== null) { return campaign[key + '_override'] || ''; } else { return sendConfiguration[key] || ''; @@ -458,7 +452,7 @@ class MessageSender { enforce(subscriptionGrouped); await knex('campaign_messages').insert({ - campaign: this.campaign.id, + campaign: campaign.id, list: list.id, subscription: subscriptionGrouped.id, send_configuration: sendConfiguration.id, @@ -468,7 +462,31 @@ class MessageSender { updated: now }); - } else if (msgType === MessageType.TRIGGERED || msgType === MessageType.TEST || msgType === MessageType.SUBSCRIPTION) { + } + + if (campaign && msgType === MessageType.TEST) { + enforce(list); + enforce(subscriptionGrouped); + + try { + // Insert an entry to test_messages. This allows us to remember test sends to lists that are not + // listed in the campaign - see the check in getMessage + await knex('test_messages').insert({ + campaign: campaign.id, + list: list.id, + subscription: subscriptionGrouped.id + }); + } catch (err) { + if (err.code === 'ER_DUP_ENTRY') { + // The entry is already there, so we can ignore this error + } else { + throw err; + } + } + } + + if (msgType === MessageType.TRIGGERED || msgType === MessageType.TEST || msgType === MessageType.SUBSCRIPTION) { + if (subData.attachments) { for (const attachment of subData.attachments) { try { @@ -515,6 +533,7 @@ async function sendQueuedMessage(queuedMessage) { html: msgData.html, text: msgData.text, subject: msgData.subject, + tagLanguage: msgData.tagLanguage, renderedHtml: msgData.renderedHtml, renderedText: msgData.renderedText }); @@ -588,9 +607,51 @@ async function queueSubscriptionMessage(sendConfigurationId, to, subject, encryp senders.scheduleCheck(); } +async function getMessage(campaignCid, listCid, subscriptionCid) { + const cs = new MessageSender(); + await cs._init({type: MessageType.REGULAR, campaignCid, listCid}); + + const campaign = cs.campaign; + const list = cs.listsByCid.get(listCid); + + const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid); + + let listOk = false; + + for (const listSpec of campaign.lists) { + if (list.id === listSpec.list) { + // This means we send to a list that is associated with the campaign + listOk = true; + break; + } + } + + if (!listOk) { + const row = await knex('test_messages').where({ + campaign: campaign.id, + list: list.id, + subscription: subscriptionGrouped.id + }).first(); + + if (row) { + listOk = true; + } + } + + if (!listOk) { + throw new Error('Message not found'); + } + + const flds = cs.listsFieldsGrouped.get(list.id); + const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, cs._getExtraTags(campaign)); + + return await cs._getMessage(list, subscriptionGrouped, mergeTags, false); +} + module.exports.MessageSender = MessageSender; module.exports.MessageType = MessageType; module.exports.sendQueuedMessage = sendQueuedMessage; module.exports.queueCampaignMessageTx = queueCampaignMessageTx; module.exports.queueSubscriptionMessage = queueSubscriptionMessage; -module.exports.dropQueuedMessage = dropQueuedMessage; \ No newline at end of file +module.exports.dropQueuedMessage = dropQueuedMessage; +module.exports.getMessage = getMessage; \ No newline at end of file diff --git a/server/lib/subscription-mail-helpers.js b/server/lib/subscription-mail-helpers.js index 1c057243..2db2a3e5 100644 --- a/server/lib/subscription-mail-helpers.js +++ b/server/lib/subscription-mail-helpers.js @@ -6,7 +6,7 @@ const settings = require('../models/settings'); const {getTrustedUrl, getPublicUrl} = require('./urls'); const { tUI, tMark } = require('./translate'); const contextHelpers = require('./context-helpers'); -const {getFieldColumn} = require('../../shared/lists'); +const {getFieldColumn, toNameTagLangauge} = require('../../shared/lists'); const forms = require('../models/forms'); const messageSender = require('./message-sender'); const tools = require('./tools'); @@ -118,7 +118,7 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls await messageSender.queueSubscriptionMessage( list.send_configuration, { - name: list.to_name === null ? undefined : tools.formatTemplate(list.to_name, {}, mergeTags, false), + name: list.to_name === null ? undefined : tools.formatTemplate(list.to_name, toNameTagLangauge, mergeTags, false), address: email }, tUI(subjectKey, locale, { list: list.name }), diff --git a/server/lib/tools.js b/server/lib/tools.js index 66c64647..2a662428 100644 --- a/server/lib/tools.js +++ b/server/lib/tools.js @@ -18,6 +18,8 @@ const fs = require('fs-extra'); const { JSDOM } = require('jsdom'); const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate'); +const {TagLanguages} = require('../../shared/templates'); + const templates = new Map(); @@ -142,23 +144,18 @@ function validateEmailGetMessage(result, address, language) { } } -function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) { - const links = campaign && list && subscription ? getMessageLinks(campaign, list, subscription) : {}; - return formatTemplate(message, links, mergeTags, isHTML); +function formatCampaignTemplate(source, tagLanguage, mergeTags, isHTML, campaign, list, subscription) { + const links = getMessageLinks(campaign, list, subscription); + mergeTags = {...mergeTags, ...links}; + return formatTemplate(source, tagLanguage, mergeTags, isHTML); } -function formatTemplate(template, links, mergeTags, isHTML) { - if (!links && !mergeTags) { return template; } +function _formatTemplateSimple(source, mergeTags, isHTML) { + if (!mergeTags) { return source; } const getValue = fullKey => { const keys = (fullKey || '').split('.'); - 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(); @@ -176,7 +173,7 @@ function formatTemplate(template, links, mergeTags, isHTML) { }) : (containsHTML ? htmlToText.fromString(value) : value); }; - return template.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => { + return source.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => { let value = getValue(identifier); if (value === false) { return match; @@ -186,6 +183,21 @@ function formatTemplate(template, links, mergeTags, isHTML) { }); } +function _formatTemplateHbs(source, mergeTags, isHTML) { + const renderer = hbs.handlebars.compile(source); + const options = {}; + + return renderer(mergeTags, options); +} + +function formatTemplate(source, tagLanguage, mergeTags, isHTML) { + if (tagLanguage === TagLanguages.SIMPLE) { + return _formatTemplateSimple(source, mergeTags, isHTML) + } else if (tagLanguage === TagLanguages.HBS) { + return _formatTemplateHbs(source, mergeTags, isHTML) + } +} + async function prepareHtml(html) { const { window } = new JSDOM(html); @@ -212,14 +224,26 @@ async function prepareHtml(html) { } function getMessageLinks(campaign, list, subscription) { - return { - LINK_UNSUBSCRIBE: getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid), - LINK_PREFERENCES: getPublicUrl('/subscription/' + list.cid + '/manage/' + subscription.cid), - LINK_BROWSER: getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid), - CAMPAIGN_ID: campaign.cid, - LIST_ID: list.cid, - SUBSCRIPTION_ID: subscription.cid - }; + const result = {}; + + if (list && subscription) { + if (campaign) { + result.LINK_UNSUBSCRIBE = getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid); + result.LINK_BROWSER = getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid); + } else { + result.LINK_UNSUBSCRIBE = getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid); + } + + result.LINK_PREFERENCES = getPublicUrl('/subscription/' + list.cid + '/manage/' + subscription.cid); + result.LIST_ID = list.cid; + result.SUBSCRIPTION_ID = subscription.cid; + } + + if (campaign) { + result.CAMPAIGN_ID = campaign.cid; + } + + return result; } module.exports = { @@ -228,7 +252,7 @@ module.exports = { getTemplate, prepareHtml, getMessageLinks, - formatMessage, + formatCampaignTemplate, formatTemplate }; diff --git a/server/models/campaigns.js b/server/models/campaigns.js index 5a9c2689..1f3af7a8 100644 --- a/server/models/campaigns.js +++ b/server/models/campaigns.js @@ -888,7 +888,7 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta async function start(context, campaignId, startAt) { - await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE, PAUSED, or FINISHED state', startAt); + await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.SCHEDULED, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE, PAUSED, or FINISHED state', startAt); } async function stop(context, campaignId) { @@ -990,8 +990,9 @@ async function testSend(context, data) { const messageData = { campaignId: campaignId, subject: data.subjectPrepend + campaign.subject + data.subjectAppend, - html: data.html, // The html and text may be undefined + html: data.html, // The html, text and tagLanguage may be undefined text: data.text, + tagLanguage: data.tagLanguage, attachments: [] }; @@ -1048,7 +1049,8 @@ async function testSend(context, data) { const messageData = { subject: 'Test', html: data.html, - text: data.text + text: data.text, + tagLanguage: data.tagLanguage }; const list = await lists.getByCidTx(tx, context, data.listCid); diff --git a/server/models/links.js b/server/models/links.js index 3eb585f2..30922b00 100644 --- a/server/models/links.js +++ b/server/models/links.js @@ -140,10 +140,10 @@ async function addOrGet(campaignId, url) { } } -async function updateLinks(campaign, list, subscription, mergeTags, message) { - if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !message || !message.trim()) { +async function updateLinks(source, tagLanguage, mergeTags, campaign, list, subscription) { + if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !source || !source.trim()) { // tracking is disabled, do not modify the message - return message; + return source; } // insert tracking image @@ -151,12 +151,12 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) { let inserted = false; const imgUrl = getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`); const img = 'mt'; - message = message.replace(/<\/body\b/i, match => { + source = source.replace(/<\/body\b/i, match => { inserted = true; return img + match; }); if (!inserted) { - message = message + img; + source = source + img; } } @@ -165,7 +165,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) { const urlsToBeReplaced = new Set(); - message.replace(re, (match, prefix, encodedUrl) => { + source.replace(re, (match, prefix, encodedUrl) => { const url = he.decode(encodedUrl, {isAttributeValue: true}); urlsToBeReplaced.add(url); }); @@ -173,19 +173,19 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) { const urls = new Map(); // url -> {id, cid} (as returned by add) for (const url of urlsToBeReplaced) { // url might include variables, need to rewrite those just as we do with message content - const expanedUrl = tools.formatMessage(campaign, list, subscription, mergeTags, url); + const expanedUrl = tools.formatCampaignTemplate(url, tagLanguage, mergeTags, false, campaign, list, subscription); const link = await addOrGet(campaign.id, expanedUrl); urls.set(url, link); } - message = message.replace(re, (match, prefix, encodedUrl) => { + source = source.replace(re, (match, prefix, encodedUrl) => { const url = he.decode(encodedUrl, {isAttributeValue: true}); const link = urls.get(url); return prefix + (link ? getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`) : url); }); } - return message; + return source; } module.exports.LinkId = LinkId; diff --git a/server/models/templates.js b/server/models/templates.js index f957aaaa..d947ecb1 100644 --- a/server/models/templates.js +++ b/server/models/templates.js @@ -158,6 +158,7 @@ const MAX_EMAIL_COUNT = 100; async function sendAsTransactionalEmail(context, templateId, sendConfigurationId, emails, subject, mergeTags) { // TODO - Update this to use MessageSender.queueMessageTx (with renderedHtml and renderedText) + /* if (emails.length > MAX_EMAIL_COUNT) { throw new Error(`Cannot send more than ${MAX_EMAIL_COUNT} emails at once`); } @@ -182,6 +183,7 @@ async function sendAsTransactionalEmail(context, templateId, sendConfigurationId }; const html = tools.formatTemplate( + TODO - tag langauge template.html, null, variables, @@ -190,6 +192,7 @@ async function sendAsTransactionalEmail(context, templateId, sendConfigurationId const text = (template.text || '').trim() ? tools.formatTemplate( + TODO - tag langauge template.text, null, variables, @@ -210,6 +213,7 @@ async function sendAsTransactionalEmail(context, templateId, sendConfigurationId ); } }); + */ } diff --git a/server/routes/archive.js b/server/routes/archive.js index 10b602d8..f975a097 100644 --- a/server/routes/archive.js +++ b/server/routes/archive.js @@ -1,13 +1,11 @@ 'use strict'; const router = require('../lib/router-async').create(); -const {MessageSender} = require('../lib/message-sender'); +const messageSender = require('../lib/message-sender'); router.get('/:campaign/:list/:subscription', (req, res, next) => { - const cs = new MessageSender(); - cs.initByCampaignCid(req.params.campaign) - .then(() => cs.getMessage(req.params.list, req.params.subscription)) + messageSender.getMessage(req.params.campaign, req.params.list, req.params.subscription) .then(result => { const {html} = result; diff --git a/server/setup/knex/migrations/20190705220000_test_messages.js b/server/setup/knex/migrations/20190705220000_test_messages.js new file mode 100644 index 00000000..112b0aa6 --- /dev/null +++ b/server/setup/knex/migrations/20190705220000_test_messages.js @@ -0,0 +1,14 @@ +exports.up = (knex, Promise) => (async() => { + await knex.schema.raw('CREATE TABLE `test_messages` (\n' + + ' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' + + ' `campaign` int(10) unsigned NOT NULL,\n' + + ' `list` int(10) unsigned NOT NULL,\n' + + ' `subscription` int(10) unsigned NOT NULL,\n' + + ' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' + + ' PRIMARY KEY (`id`),\n' + + ' UNIQUE KEY `cls` (`campaign`, `list`, `subscription`)\n' + + ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n'); +})(); + +exports.down = (knex, Promise) => (async() => { +})(); diff --git a/shared/lists.js b/shared/lists.js index 61df38a9..8682ed95 100644 --- a/shared/lists.js +++ b/shared/lists.js @@ -1,5 +1,7 @@ 'use strict'; +const {TagLanguages} = require('./templates'); + const UnsubscriptionMode = { MIN: 0, @@ -42,10 +44,13 @@ function getFieldColumn(field) { return field.column || 'grouped_' + field.id; } +const toNameTagLangauge = TagLanguages.SIMPLE; + module.exports = { UnsubscriptionMode, SubscriptionStatus, SubscriptionSource, FieldWizard, - getFieldColumn + getFieldColumn, + toNameTagLangauge }; \ No newline at end of file diff --git a/shared/mosaico-templates.js b/shared/mosaico-templates.js index eba1423b..fc2671a8 100644 --- a/shared/mosaico-templates.js +++ b/shared/mosaico-templates.js @@ -1,14 +1,6 @@ 'use strict'; -const {TagLanguages} = require('./templates'); - -function renderTag(tagLanguage, tag) { - if (tagLanguage === TagLanguages.SIMPLE) { - return `[${tag}]`; - } else if (tagLanguage === TagLanguages.HBS) { - return `{{${tag}}}`; - } -} +const {renderTag} = require('./templates'); function getVersafix(tagLanguage) { const tg = tag => renderTag(tagLanguage, tag); diff --git a/shared/templates.js b/shared/templates.js index 7df28cc4..35d819cf 100644 --- a/shared/templates.js +++ b/shared/templates.js @@ -7,6 +7,14 @@ const TagLanguages = { const allTagLanguages = [TagLanguages.SIMPLE, TagLanguages.HBS]; +function renderTag(tagLanguage, tag) { + if (tagLanguage === TagLanguages.SIMPLE) { + return `[${tag}]`; + } else if (tagLanguage === TagLanguages.HBS) { + return `{{${tag}}}`; + } +} + function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) { if (trustedBaseUrl.endsWith('/')) { trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1); @@ -67,5 +75,6 @@ module.exports = { unbase, getMergeTagsForBases, TagLanguages, - allTagLanguages + allTagLanguages, + renderTag }; \ No newline at end of file