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 { return {
html: exportedData.data_sourceCustom_html, 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; const entity = this.props.entity;
if (entity.scheduled) { if (entity.scheduled) {
const date = moment(entity.scheduled); const date = moment.utc(entity.scheduled);
this.populateFormValues({ this.populateFormValues({
sendLater: true, sendLater: true,
date: date.format('YYYY-MM-DD'), date: date.format('YYYY-MM-DD'),

View file

@ -97,6 +97,7 @@ export class TestSendModalDialog extends Component {
const contentData = await this.props.getDataAsync(); const contentData = await this.props.getDataAsync();
data.html = contentData.html; data.html = contentData.html;
data.text = contentData.text; data.text = contentData.text;
data.tagLanguage = contentData.tagLanguage;
} }
if (mode === TestSendModalDialogMode.TEMPLATE) { if (mode === TestSendModalDialogMode.TEMPLATE) {
@ -202,7 +203,7 @@ export class TestSendModalDialog extends Component {
const target = this.getFormValue('target'); const target = this.getFormValue('target');
const mode = this.props.mode; const mode = this.props.mode;
if (mode === TestSendModalDialogMode.CAMPAIGN_STATUS) { if (mode === TestSendModalDialogMode.CAMPAIGN_CONTENT || mode === TestSendModalDialogMode.CAMPAIGN_STATUS) {
const targetOpts = [ const targetOpts = [
{key: Target.CAMPAIGN_ONE, label: t('Single test user of the campaign')}, {key: Target.CAMPAIGN_ONE, label: t('Single test user of the campaign')},
{key: Target.CAMPAIGN_ALL, label: t('All test users 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 { return {
html: exportedData.html, 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 mailtrainConfig from 'mailtrainConfig';
import {ActionLink, Button} from "../lib/bootstrap-components"; import {ActionLink, Button} from "../lib/bootstrap-components";
import {Trans} from "react-i18next"; import {Trans} from "react-i18next";
import {TagLanguages} from "../../../shared/templates"; import {TagLanguages, renderTag} from "../../../shared/templates";
import styles from "../lib/styles.scss"; import styles from "../lib/styles.scss";
@ -507,6 +507,39 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
export function getEditForm(owner, typeKey, prefix = '') { export function getEditForm(owner, typeKey, prefix = '') {
const t = owner.props.t; 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 ( return (
<div> <div>
<AlignedRow> <AlignedRow>
@ -516,12 +549,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
label={t('mergeTagReference')}/> label={t('mergeTagReference')}/>
{owner.state.showMergeTagReference && {owner.state.showMergeTagReference &&
<div style={{marginTop: '15px'}}> <div style={{marginTop: '15px'}}>
<Trans i18nKey="mergeTagsAreTagsThatAreReplacedBefore"> {instructions}
<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>
<table className="table table-bordered table-condensed table-striped"> <table className="table table-bordered table-condensed table-striped">
<thead> <thead>
<tr> <tr>
@ -536,7 +564,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
<tbody> <tbody>
<tr> <tr>
<th scope="row"> <th scope="row">
[LINK_UNSUBSCRIBE] {tg('LINK_UNSUBSCRIBE')}
</th> </th>
<td> <td>
<Trans i18nKey="urlThatPointsToTheUnsubscribePage">URL that points to the unsubscribe page</Trans> <Trans i18nKey="urlThatPointsToTheUnsubscribePage">URL that points to the unsubscribe page</Trans>
@ -544,7 +572,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[LINK_PREFERENCES] {tg('LINK_PREFERENCES')}
</th> </th>
<td> <td>
<Trans i18nKey="urlThatPointsToThePreferencesPageOfThe">URL that points to the preferences page of the subscriber</Trans> <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>
<tr> <tr>
<th scope="row"> <th scope="row">
[LINK_BROWSER] {tg('LINK_BROWSER')}
</th> </th>
<td> <td>
<Trans i18nKey="urlToPreviewTheMessageInABrowser">URL to preview the message in a browser</Trans> <Trans i18nKey="urlToPreviewTheMessageInABrowser">URL to preview the message in a browser</Trans>
@ -560,7 +588,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[EMAIL] {tg('EMAIL')}
</th> </th>
<td> <td>
<Trans i18nKey="emailAddress-1">Email address</Trans> <Trans i18nKey="emailAddress-1">Email address</Trans>
@ -568,7 +596,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[SUBSCRIPTION_ID] {tg('SUBSCRIPTION_ID')}
</th> </th>
<td> <td>
<Trans i18nKey="uniqueIdThatIdentifiesTheRecipient">Unique ID that identifies the recipient</Trans> <Trans i18nKey="uniqueIdThatIdentifiesTheRecipient">Unique ID that identifies the recipient</Trans>
@ -576,7 +604,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[LIST_ID] {tg('LIST_ID')}
</th> </th>
<td> <td>
<Trans i18nKey="uniqueIdThatIdentifiesTheListUsedForThis">Unique ID that identifies the list used for this campaign</Trans> <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>
<tr> <tr>
<th scope="row"> <th scope="row">
[CAMPAIGN_ID] {tg('CAMPAIGN_ID')}
</th> </th>
<td> <td>
<Trans i18nKey="uniqueIdThatIdentifiesCurrentCampaign">Unique ID that identifies current campaign</Trans> <Trans i18nKey="uniqueIdThatIdentifiesCurrentCampaign">Unique ID that identifies current campaign</Trans>
@ -609,7 +637,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
<tbody> <tbody>
<tr> <tr>
<th scope="row"> <th scope="row">
[RSS_ENTRY_TITLE] {tg('RSS_ENTRY_TITLE')}
</th> </th>
<td> <td>
<Trans i18nKey="rssEntryTitle">RSS entry title</Trans> <Trans i18nKey="rssEntryTitle">RSS entry title</Trans>
@ -617,7 +645,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[RSS_ENTRY_DATE] {tg('RSS_ENTRY_DATE')}
</th> </th>
<td> <td>
<Trans i18nKey="rssEntryDate">RSS entry date</Trans> <Trans i18nKey="rssEntryDate">RSS entry date</Trans>
@ -625,7 +653,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[RSS_ENTRY_LINK] {tg('RSS_ENTRY_LINK')}
</th> </th>
<td> <td>
<Trans i18nKey="rssEntryLink">RSS entry link</Trans> <Trans i18nKey="rssEntryLink">RSS entry link</Trans>
@ -633,7 +661,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[RSS_ENTRY_CONTENT] {tg('RSS_ENTRY_CONTENT')}
</th> </th>
<td> <td>
<Trans i18nKey="contentOfAnRssEntry">Content of an RSS entry</Trans> <Trans i18nKey="contentOfAnRssEntry">Content of an RSS entry</Trans>
@ -641,7 +669,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[RSS_ENTRY_SUMMARY] {tg('RSS_ENTRY_SUMMARY')}
</th> </th>
<td> <td>
<Trans i18nKey="rssEntrySummary">RSS entry summary</Trans> <Trans i18nKey="rssEntrySummary">RSS entry summary</Trans>
@ -649,7 +677,7 @@ export function getEditForm(owner, typeKey, prefix = '') {
</tr> </tr>
<tr> <tr>
<th scope="row"> <th scope="row">
[RSS_ENTRY_IMAGE_URL] {tg('RSS_ENTRY_IMAGE_URL')}
</th> </th>
<td> <td>
<Trans i18nKey="rssEntryImageUrl">RSS entry image URL</Trans> <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 sendConfigurations = require('../models/send-configurations');
const links = require('../models/links'); const links = require('../models/links');
const {CampaignSource, CampaignType} = require('../../shared/campaigns'); const {CampaignSource, CampaignType} = require('../../shared/campaigns');
const {SubscriptionStatus} = require('../../shared/lists'); const {SubscriptionStatus, toNameTagLangauge} = require('../../shared/lists');
const tools = require('./tools'); const tools = require('./tools');
const htmlToText = require('html-to-text'); const htmlToText = require('html-to-text');
const request = require('request-promise'); const request = require('request-promise');
@ -39,7 +39,7 @@ class MessageSender {
settings is one of: settings is one of:
- campaignCid / campaignId - campaignCid / campaignId
or or
- sendConfiguration, listId, attachments, html, text, subject - sendConfiguration, listId, attachments, html, text, subject, tagLanguage
*/ */
async _init(settings) { async _init(settings) {
this.type = settings.type; this.type = settings.type;
@ -58,14 +58,6 @@ class MessageSender {
this.isMassMail = true; this.isMassMail = true;
} else if (this.type === MessageType.TEST) { } 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; this.isMassMail = true;
} else { } else {
@ -89,6 +81,12 @@ class MessageSender {
this.listsByCid.set(list.cid, list); this.listsByCid.set(list.cid, list);
this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id)); 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) { } else if (this.campaign && this.campaign.lists) {
for (const listSpec of this.campaign.lists) { for (const listSpec of this.campaign.lists) {
const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list); const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
@ -123,11 +121,22 @@ class MessageSender {
} else if (settings.html !== undefined) { } else if (settings.html !== undefined) {
this.html = settings.html; this.html = settings.html;
this.text = settings.text; this.text = settings.text;
this.tagLanguage = settings.tagLanguage;
} else if (this.campaign && this.campaign.source === CampaignSource.TEMPLATE) { } else if (this.campaign && this.campaign.source === CampaignSource.TEMPLATE) {
this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false); 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) { if (settings.subject !== undefined) {
this.subject = settings.subject; this.subject = settings.subject;
} else if (this.campaign && this.campaign.subject !== undefined) { } else if (this.campaign && this.campaign.subject !== undefined) {
@ -155,8 +164,7 @@ class MessageSender {
text = this.text; text = this.text;
renderTags = true; renderTags = true;
} else if (campaign) { } else if (campaign && campaign.source === CampaignSource.URL) {
if (campaign.source === CampaignSource.URL) {
const form = tools.getMessageLinks(campaign, list, subscriptionGrouped); const form = tools.getMessageLinks(campaign, list, subscriptionGrouped);
for (const key in mergeTags) { for (const key in mergeTags) {
form[key] = mergeTags[key]; form[key] = mergeTags[key];
@ -175,20 +183,6 @@ class MessageSender {
html = response.body; html = response.body;
text = ''; text = '';
renderTags = false; 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;
}
html = await links.updateLinks(campaign, list, subscriptionGrouped, mergeTags, html);
} }
const attachments = this.attachments.slice(); 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() if (renderTags) {
? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text) if (this.campaign) {
: htmlToText.fromString(html, {wordwrap: 130}); 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 { return {
html, html,
@ -220,7 +226,7 @@ class MessageSender {
_getExtraTags(campaign) { _getExtraTags(campaign) {
const tags = {}; const tags = {};
if (campaign.type === CampaignType.RSS_ENTRY) { if (campaign && campaign.type === CampaignType.RSS_ENTRY) {
const rssEntry = campaign.data.rssEntry; const rssEntry = campaign.data.rssEntry;
tags['RSS_ENTRY_TITLE'] = rssEntry.title; tags['RSS_ENTRY_TITLE'] = rssEntry.title;
tags['RSS_ENTRY_DATE'] = rssEntry.date; tags['RSS_ENTRY_DATE'] = rssEntry.date;
@ -234,26 +240,10 @@ class MessageSender {
return tags; return tags;
} }
async initByCampaignCid(campaignCid) {
await this._init({type: MessageType.REGULAR, campaignCid});
}
async initByCampaignId(campaignId) { async initByCampaignId(campaignId) {
await this._init({type: MessageType.REGULAR, 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: subData is one of:
@ -304,21 +294,33 @@ class MessageSender {
message = await this._getMessage(list, subscriptionGrouped, mergeTags, true); message = await this._getMessage(list, subscriptionGrouped, mergeTags, true);
const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
let listUnsubscribe = null; let listUnsubscribe = null;
if (!list.listunsubscribe_disabled) { if (!list.listunsubscribe_disabled) {
listUnsubscribe = campaign.unsubscribe_url listUnsubscribe = campaign && campaign.unsubscribe_url
? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, campaign.unsubscribe_url) ? tools.formatCampaignTemplate(campaign.unsubscribe_url, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped)
: getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.cid); : getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.cid);
} }
to = { 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 address: subscriptionGrouped.email
}; };
subject = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, this.subject, false); subject = this.subject;
if (this.tagLanguage) {
subject = tools.formatCampaignTemplate(this.subject, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped);
}
headers = {
'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) { if (this.useVerp) {
envelope = { envelope = {
@ -331,27 +333,19 @@ class MessageSender {
sender = campaignAddress + '@' + sendConfiguration.verp_hostname; sender = campaignAddress + '@' + sendConfiguration.verp_hostname;
} }
headers = { headers['x-fbl'] = campaignAddress;
'x-fbl': campaignAddress, headers['x-msys-api'] = JSON.stringify({
// custom header for SparkPost
'x-msys-api': JSON.stringify({
campaign_id: campaignAddress campaign_id: campaignAddress
}), });
// custom header for SendGrid headers['x-smtpapi'] = JSON.stringify({
'x-smtpapi': JSON.stringify({
unique_args: { unique_args: {
campaign_id: campaignAddress campaign_id: campaignAddress
} }
}), });
// custom header for Mailgun headers['x-mailgun-variables'] = JSON.stringify({
'x-mailgun-variables': JSON.stringify({
campaign_id: campaignAddress campaign_id: campaignAddress
}), });
'List-ID': {
prepared: true,
value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>'
} }
};
listHeader = { listHeader = {
unsubscribe: listUnsubscribe unsubscribe: listUnsubscribe
@ -375,7 +369,7 @@ class MessageSender {
await mailer.throttleWait(); await mailer.throttleWait();
const getOverridable = key => { 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'] || ''; return campaign[key + '_override'] || '';
} else { } else {
return sendConfiguration[key] || ''; return sendConfiguration[key] || '';
@ -458,7 +452,7 @@ class MessageSender {
enforce(subscriptionGrouped); enforce(subscriptionGrouped);
await knex('campaign_messages').insert({ await knex('campaign_messages').insert({
campaign: this.campaign.id, campaign: campaign.id,
list: list.id, list: list.id,
subscription: subscriptionGrouped.id, subscription: subscriptionGrouped.id,
send_configuration: sendConfiguration.id, send_configuration: sendConfiguration.id,
@ -468,7 +462,31 @@ class MessageSender {
updated: now 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) { if (subData.attachments) {
for (const attachment of subData.attachments) { for (const attachment of subData.attachments) {
try { try {
@ -515,6 +533,7 @@ async function sendQueuedMessage(queuedMessage) {
html: msgData.html, html: msgData.html,
text: msgData.text, text: msgData.text,
subject: msgData.subject, subject: msgData.subject,
tagLanguage: msgData.tagLanguage,
renderedHtml: msgData.renderedHtml, renderedHtml: msgData.renderedHtml,
renderedText: msgData.renderedText renderedText: msgData.renderedText
}); });
@ -588,9 +607,51 @@ async function queueSubscriptionMessage(sendConfigurationId, to, subject, encryp
senders.scheduleCheck(); 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.MessageSender = MessageSender;
module.exports.MessageType = MessageType; module.exports.MessageType = MessageType;
module.exports.sendQueuedMessage = sendQueuedMessage; module.exports.sendQueuedMessage = sendQueuedMessage;
module.exports.queueCampaignMessageTx = queueCampaignMessageTx; module.exports.queueCampaignMessageTx = queueCampaignMessageTx;
module.exports.queueSubscriptionMessage = queueSubscriptionMessage; module.exports.queueSubscriptionMessage = queueSubscriptionMessage;
module.exports.dropQueuedMessage = dropQueuedMessage; 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 {getTrustedUrl, getPublicUrl} = require('./urls');
const { tUI, tMark } = require('./translate'); const { tUI, tMark } = require('./translate');
const contextHelpers = require('./context-helpers'); const contextHelpers = require('./context-helpers');
const {getFieldColumn} = require('../../shared/lists'); const {getFieldColumn, toNameTagLangauge} = require('../../shared/lists');
const forms = require('../models/forms'); const forms = require('../models/forms');
const messageSender = require('./message-sender'); const messageSender = require('./message-sender');
const tools = require('./tools'); const tools = require('./tools');
@ -118,7 +118,7 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
await messageSender.queueSubscriptionMessage( await messageSender.queueSubscriptionMessage(
list.send_configuration, 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 address: email
}, },
tUI(subjectKey, locale, { list: list.name }), tUI(subjectKey, locale, { list: list.name }),

View file

@ -18,6 +18,8 @@ const fs = require('fs-extra');
const { JSDOM } = require('jsdom'); const { JSDOM } = require('jsdom');
const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate'); const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate');
const {TagLanguages} = require('../../shared/templates');
const templates = new Map(); const templates = new Map();
@ -142,23 +144,18 @@ function validateEmailGetMessage(result, address, language) {
} }
} }
function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) { function formatCampaignTemplate(source, tagLanguage, mergeTags, isHTML, campaign, list, subscription) {
const links = campaign && list && subscription ? getMessageLinks(campaign, list, subscription) : {}; const links = getMessageLinks(campaign, list, subscription);
return formatTemplate(message, links, mergeTags, isHTML); mergeTags = {...mergeTags, ...links};
return formatTemplate(source, tagLanguage, mergeTags, isHTML);
} }
function formatTemplate(template, links, mergeTags, isHTML) { function _formatTemplateSimple(source, mergeTags, isHTML) {
if (!links && !mergeTags) { return template; } if (!mergeTags) { return source; }
const getValue = fullKey => { const getValue = fullKey => {
const keys = (fullKey || '').split('.'); const keys = (fullKey || '').split('.');
if (links && links.hasOwnProperty(keys[0])) {
return links[keys[0]];
}
if (!mergeTags) { return false; }
let value = mergeTags; let value = mergeTags;
while (keys.length > 0) { while (keys.length > 0) {
let key = keys.shift(); let key = keys.shift();
@ -176,7 +173,7 @@ function formatTemplate(template, links, mergeTags, isHTML) {
}) : (containsHTML ? htmlToText.fromString(value) : value); }) : (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); let value = getValue(identifier);
if (value === false) { if (value === false) {
return match; 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) { async function prepareHtml(html) {
const { window } = new JSDOM(html); const { window } = new JSDOM(html);
@ -212,14 +224,26 @@ async function prepareHtml(html) {
} }
function getMessageLinks(campaign, list, subscription) { function getMessageLinks(campaign, list, subscription) {
return { const result = {};
LINK_UNSUBSCRIBE: getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid),
LINK_PREFERENCES: getPublicUrl('/subscription/' + list.cid + '/manage/' + subscription.cid), if (list && subscription) {
LINK_BROWSER: getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid), if (campaign) {
CAMPAIGN_ID: campaign.cid, result.LINK_UNSUBSCRIBE = getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid);
LIST_ID: list.cid, result.LINK_BROWSER = getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid);
SUBSCRIPTION_ID: 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 = { module.exports = {
@ -228,7 +252,7 @@ module.exports = {
getTemplate, getTemplate,
prepareHtml, prepareHtml,
getMessageLinks, getMessageLinks,
formatMessage, formatCampaignTemplate,
formatTemplate formatTemplate
}; };

View file

@ -888,7 +888,7 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
async function start(context, campaignId, startAt) { 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) { async function stop(context, campaignId) {
@ -990,8 +990,9 @@ async function testSend(context, data) {
const messageData = { const messageData = {
campaignId: campaignId, campaignId: campaignId,
subject: data.subjectPrepend + campaign.subject + data.subjectAppend, 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, text: data.text,
tagLanguage: data.tagLanguage,
attachments: [] attachments: []
}; };
@ -1048,7 +1049,8 @@ async function testSend(context, data) {
const messageData = { const messageData = {
subject: 'Test', subject: 'Test',
html: data.html, html: data.html,
text: data.text text: data.text,
tagLanguage: data.tagLanguage
}; };
const list = await lists.getByCidTx(tx, context, data.listCid); 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) { async function updateLinks(source, tagLanguage, mergeTags, campaign, list, subscription) {
if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !message || !message.trim()) { if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !source || !source.trim()) {
// tracking is disabled, do not modify the message // tracking is disabled, do not modify the message
return message; return source;
} }
// insert tracking image // insert tracking image
@ -151,12 +151,12 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
let inserted = false; let inserted = false;
const imgUrl = getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`); const imgUrl = getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`);
const img = '<img src="' + imgUrl + '" width="1" height="1" alt="mt">'; 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; inserted = true;
return img + match; return img + match;
}); });
if (!inserted) { if (!inserted) {
message = message + img; source = source + img;
} }
} }
@ -165,7 +165,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
const urlsToBeReplaced = new Set(); const urlsToBeReplaced = new Set();
message.replace(re, (match, prefix, encodedUrl) => { source.replace(re, (match, prefix, encodedUrl) => {
const url = he.decode(encodedUrl, {isAttributeValue: true}); const url = he.decode(encodedUrl, {isAttributeValue: true});
urlsToBeReplaced.add(url); 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) const urls = new Map(); // url -> {id, cid} (as returned by add)
for (const url of urlsToBeReplaced) { for (const url of urlsToBeReplaced) {
// url might include variables, need to rewrite those just as we do with message content // 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); const link = await addOrGet(campaign.id, expanedUrl);
urls.set(url, link); 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 url = he.decode(encodedUrl, {isAttributeValue: true});
const link = urls.get(url); const link = urls.get(url);
return prefix + (link ? getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`) : url); return prefix + (link ? getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`) : url);
}); });
} }
return message; return source;
} }
module.exports.LinkId = LinkId; module.exports.LinkId = LinkId;

View file

@ -158,6 +158,7 @@ const MAX_EMAIL_COUNT = 100;
async function sendAsTransactionalEmail(context, templateId, sendConfigurationId, emails, subject, mergeTags) { async function sendAsTransactionalEmail(context, templateId, sendConfigurationId, emails, subject, mergeTags) {
// TODO - Update this to use MessageSender.queueMessageTx (with renderedHtml and renderedText) // TODO - Update this to use MessageSender.queueMessageTx (with renderedHtml and renderedText)
/*
if (emails.length > MAX_EMAIL_COUNT) { if (emails.length > MAX_EMAIL_COUNT) {
throw new Error(`Cannot send more than ${MAX_EMAIL_COUNT} emails at once`); 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( const html = tools.formatTemplate(
TODO - tag langauge
template.html, template.html,
null, null,
variables, variables,
@ -190,6 +192,7 @@ async function sendAsTransactionalEmail(context, templateId, sendConfigurationId
const text = (template.text || '').trim() const text = (template.text || '').trim()
? tools.formatTemplate( ? tools.formatTemplate(
TODO - tag langauge
template.text, template.text,
null, null,
variables, variables,
@ -210,6 +213,7 @@ async function sendAsTransactionalEmail(context, templateId, sendConfigurationId
); );
} }
}); });
*/
} }

View file

@ -1,13 +1,11 @@
'use strict'; 'use strict';
const router = require('../lib/router-async').create(); 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) => { router.get('/:campaign/:list/:subscription', (req, res, next) => {
const cs = new MessageSender(); messageSender.getMessage(req.params.campaign, req.params.list, req.params.subscription)
cs.initByCampaignCid(req.params.campaign)
.then(() => cs.getMessage(req.params.list, req.params.subscription))
.then(result => { .then(result => {
const {html} = 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'; 'use strict';
const {TagLanguages} = require('./templates');
const UnsubscriptionMode = { const UnsubscriptionMode = {
MIN: 0, MIN: 0,
@ -42,10 +44,13 @@ function getFieldColumn(field) {
return field.column || 'grouped_' + field.id; return field.column || 'grouped_' + field.id;
} }
const toNameTagLangauge = TagLanguages.SIMPLE;
module.exports = { module.exports = {
UnsubscriptionMode, UnsubscriptionMode,
SubscriptionStatus, SubscriptionStatus,
SubscriptionSource, SubscriptionSource,
FieldWizard, FieldWizard,
getFieldColumn getFieldColumn,
toNameTagLangauge
}; };

View file

@ -1,14 +1,6 @@
'use strict'; 'use strict';
const {TagLanguages} = require('./templates'); const {renderTag} = require('./templates');
function renderTag(tagLanguage, tag) {
if (tagLanguage === TagLanguages.SIMPLE) {
return `[${tag}]`;
} else if (tagLanguage === TagLanguages.HBS) {
return `{{${tag}}}`;
}
}
function getVersafix(tagLanguage) { function getVersafix(tagLanguage) {
const tg = tag => renderTag(tagLanguage, tag); const tg = tag => renderTag(tagLanguage, tag);

View file

@ -7,6 +7,14 @@ const TagLanguages = {
const allTagLanguages = [TagLanguages.SIMPLE, TagLanguages.HBS]; 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) { function _getBases(trustedBaseUrl, sandboxBaseUrl, publicBaseUrl) {
if (trustedBaseUrl.endsWith('/')) { if (trustedBaseUrl.endsWith('/')) {
trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1); trustedBaseUrl = trustedBaseUrl.substring(0, trustedBaseUrl.length - 1);
@ -67,5 +75,6 @@ module.exports = {
unbase, unbase,
getMergeTagsForBases, getMergeTagsForBases,
TagLanguages, TagLanguages,
allTagLanguages allTagLanguages,
renderTag
}; };