Work in progress on tag language

Fix - message sent to a list not associated with a campaign couldn't be shown in archive - to know which message to show even if the list is not at the campaign, we store test messages in table test_messages
This commit is contained in:
Tomas Bures 2019-07-05 23:23:02 +02:00
parent 00e328a914
commit 4113cb8476
17 changed files with 312 additions and 172 deletions

View file

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

View file

@ -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'),

View file

@ -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')},

View file

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

View file

@ -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 = (
<>
<Trans i18nKey="mergeTagsAreTagsThatAreReplacedBefore">
<p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>{tg('TAG_NAME')}</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value used when <code>TAG_NAME</code> is empty.</p>
</Trans>
<Trans i18nKey="youCanUseAnyOfTheStandardMergeTagsBelow">
<p>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.</p>
</Trans>
</>
);
} else if (tagLanguage === TagLanguages.HBS) {
instructions = (
<>
<Trans>
<p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>{tg('TAG_NAME')}</code>. </p>
</Trans>
<Trans i18nKey="youCanUseAnyOfTheStandardMergeTagsBelow">
<p>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.</p>
</Trans>
<Trans>
<p>The whole message is interpreted as Handlebars template (see <a href="http://handlebarsjs.com/">http://handlebarsjs.com/</a>). You can use any Handlebars blocks and expressions
in the template. The merge tags form the root context of the Handlebars template.</p>
</Trans>
</>
);
}
return (
<div>
<AlignedRow>
@ -516,12 +549,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
label={t('mergeTagReference')}/>
{owner.state.showMergeTagReference &&
<div style={{marginTop: '15px'}}>
<Trans i18nKey="mergeTagsAreTagsThatAreReplacedBefore">
<p>Merge tags are tags that are replaced before sending out the message. The format of the merge tag is the following: <code>[TAG_NAME]</code> or <code>[TAG_NAME/fallback]</code> where <code>fallback</code> is an optional text value used when <code>TAG_NAME</code> is empty.</p>
</Trans>
<Trans i18nKey="youCanUseAnyOfTheStandardMergeTagsBelow">
<p>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.</p>
</Trans>
{instructions}
<table className="table table-bordered table-condensed table-striped">
<thead>
<tr>
@ -536,7 +564,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
<tbody>
<tr>
<th scope="row">
[LINK_UNSUBSCRIBE]
{tg('LINK_UNSUBSCRIBE')}
</th>
<td>
<Trans i18nKey="urlThatPointsToTheUnsubscribePage">URL that points to the unsubscribe page</Trans>
@ -544,7 +572,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[LINK_PREFERENCES]
{tg('LINK_PREFERENCES')}
</th>
<td>
<Trans i18nKey="urlThatPointsToThePreferencesPageOfThe">URL that points to the preferences page of the subscriber</Trans>
@ -552,7 +580,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[LINK_BROWSER]
{tg('LINK_BROWSER')}
</th>
<td>
<Trans i18nKey="urlToPreviewTheMessageInABrowser">URL to preview the message in a browser</Trans>
@ -560,7 +588,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[EMAIL]
{tg('EMAIL')}
</th>
<td>
<Trans i18nKey="emailAddress-1">Email address</Trans>
@ -568,7 +596,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[SUBSCRIPTION_ID]
{tg('SUBSCRIPTION_ID')}
</th>
<td>
<Trans i18nKey="uniqueIdThatIdentifiesTheRecipient">Unique ID that identifies the recipient</Trans>
@ -576,7 +604,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[LIST_ID]
{tg('LIST_ID')}
</th>
<td>
<Trans i18nKey="uniqueIdThatIdentifiesTheListUsedForThis">Unique ID that identifies the list used for this campaign</Trans>
@ -584,7 +612,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[CAMPAIGN_ID]
{tg('CAMPAIGN_ID')}
</th>
<td>
<Trans i18nKey="uniqueIdThatIdentifiesCurrentCampaign">Unique ID that identifies current campaign</Trans>
@ -609,7 +637,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
<tbody>
<tr>
<th scope="row">
[RSS_ENTRY_TITLE]
{tg('RSS_ENTRY_TITLE')}
</th>
<td>
<Trans i18nKey="rssEntryTitle">RSS entry title</Trans>
@ -617,7 +645,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_DATE]
{tg('RSS_ENTRY_DATE')}
</th>
<td>
<Trans i18nKey="rssEntryDate">RSS entry date</Trans>
@ -625,7 +653,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_LINK]
{tg('RSS_ENTRY_LINK')}
</th>
<td>
<Trans i18nKey="rssEntryLink">RSS entry link</Trans>
@ -633,7 +661,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_CONTENT]
{tg('RSS_ENTRY_CONTENT')}
</th>
<td>
<Trans i18nKey="contentOfAnRssEntry">Content of an RSS entry</Trans>
@ -641,7 +669,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_SUMMARY]
{tg('RSS_ENTRY_SUMMARY')}
</th>
<td>
<Trans i18nKey="rssEntrySummary">RSS entry summary</Trans>
@ -649,7 +677,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr>
<tr>
<th scope="row">
[RSS_ENTRY_IMAGE_URL]
{tg('RSS_ENTRY_IMAGE_URL')}
</th>
<td>
<Trans i18nKey="rssEntryImageUrl">RSS entry image URL</Trans>

@ -1 +1 @@
Subproject commit ec89af43120f95dcf7f14dc92a2b3811bf50d7e8
Subproject commit fca588ecbe7df717d2219bac82acdc3da1bd6f40

View file

@ -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;
module.exports.getMessage = getMessage;

View file

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

View file

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

View file

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

View file

@ -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 = '<img src="' + imgUrl + '" width="1" height="1" alt="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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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