First take on the "send from url" feature
This commit is contained in:
parent
d8b98d2a8b
commit
e4c71f4026
9 changed files with 190 additions and 117 deletions
|
@ -37,6 +37,7 @@ Subscribe to Mailtrain Newsletter [here](http://mailtrain.org/subscription/EysIv
|
||||||
## Upgrade
|
## Upgrade
|
||||||
|
|
||||||
* Replace old files with new ones by running in the Mailtrain folder `git pull origin master`
|
* Replace old files with new ones by running in the Mailtrain folder `git pull origin master`
|
||||||
|
* Run `npm install --production` in the Mailtrain folder
|
||||||
|
|
||||||
## Using environment variables
|
## Using environment variables
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ let Handlebars = require('handlebars');
|
||||||
let fs = require('fs');
|
let fs = require('fs');
|
||||||
let path = require('path');
|
let path = require('path');
|
||||||
let templates = new Map();
|
let templates = new Map();
|
||||||
|
let htmlToText = require('html-to-text');
|
||||||
|
|
||||||
module.exports.transport = false;
|
module.exports.transport = false;
|
||||||
|
|
||||||
|
@ -54,6 +55,10 @@ module.exports.sendMail = (mail, template, callback) => {
|
||||||
|
|
||||||
if (textRenderer) {
|
if (textRenderer) {
|
||||||
mail.text = textRenderer(template.data || {});
|
mail.text = textRenderer(template.data || {});
|
||||||
|
} else if (mail.html) {
|
||||||
|
mail.text = htmlToText.fromString(mail.html, {
|
||||||
|
wordwrap: 130
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.transport.sendMail(mail, callback);
|
module.exports.transport.sendMail(mail, callback);
|
||||||
|
|
|
@ -8,7 +8,7 @@ let segments = require('./segments');
|
||||||
let subscriptions = require('./subscriptions');
|
let subscriptions = require('./subscriptions');
|
||||||
let shortid = require('shortid');
|
let shortid = require('shortid');
|
||||||
|
|
||||||
let allowedKeys = ['description', 'from', 'address', 'subject', 'template', 'list', 'segment', 'html', 'text'];
|
let allowedKeys = ['description', 'from', 'address', 'subject', 'template', 'template_url', 'list', 'segment', 'html', 'text'];
|
||||||
|
|
||||||
module.exports.list = (start, limit, callback) => {
|
module.exports.list = (start, limit, callback) => {
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 3
|
"schemaVersion": 4
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"geoip-ultralight": "^0.1.3",
|
"geoip-ultralight": "^0.1.3",
|
||||||
"handlebars": "^4.0.5",
|
"handlebars": "^4.0.5",
|
||||||
"hbs": "^4.0.0",
|
"hbs": "^4.0.0",
|
||||||
|
"html-to-text": "^2.1.0",
|
||||||
"humanize": "0.0.9",
|
"humanize": "0.0.9",
|
||||||
"isemail": "^2.1.0",
|
"isemail": "^2.1.0",
|
||||||
"morgan": "^1.7.0",
|
"morgan": "^1.7.0",
|
||||||
|
|
|
@ -13,6 +13,8 @@ let settings = require('../lib/models/settings');
|
||||||
let links = require('../lib/models/links');
|
let links = require('../lib/models/links');
|
||||||
let shortid = require('shortid');
|
let shortid = require('shortid');
|
||||||
let url = require('url');
|
let url = require('url');
|
||||||
|
let htmlToText = require('html-to-text');
|
||||||
|
let request = require('request');
|
||||||
|
|
||||||
function findUnsent(callback) {
|
function findUnsent(callback) {
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
|
@ -149,73 +151,98 @@ function formatMessage(message, callback) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
links.updateLinks(campaign, list, message.subscription, configItems.serviceUrl, campaign.html, (err, html) => {
|
let renderAndSend = (html, text, renderTags) => {
|
||||||
if (err) {
|
links.updateLinks(campaign, list, message.subscription, configItems.serviceUrl, html, (err, html) => {
|
||||||
return callback(err);
|
if (err) {
|
||||||
}
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
// replace data: images with embedded attachments
|
// replace data: images with embedded attachments
|
||||||
let attachments = [];
|
let attachments = [];
|
||||||
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||||
let cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop();
|
let cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop();
|
||||||
attachments.push({
|
attachments.push({
|
||||||
path: dataUri,
|
path: dataUri,
|
||||||
cid
|
cid
|
||||||
|
});
|
||||||
|
return prefix + 'cid:' + cid;
|
||||||
});
|
});
|
||||||
return prefix + 'cid:' + cid;
|
|
||||||
});
|
|
||||||
|
|
||||||
let campaignAddress = [campaign.cid, list.cid, message.subscription.cid].join('.');
|
let campaignAddress = [campaign.cid, list.cid, message.subscription.cid].join('.');
|
||||||
|
|
||||||
return callback(null, {
|
let renderedHtml = renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html) : html;
|
||||||
from: {
|
|
||||||
name: campaign.from,
|
|
||||||
address: campaign.address
|
|
||||||
},
|
|
||||||
xMailer: 'Mailtrain Mailer (+https://mailtrain.org)',
|
|
||||||
to: {
|
|
||||||
name: [].concat(message.subscription.firstName || []).concat(message.subscription.lastName || []).join(' '),
|
|
||||||
address: message.subscription.email
|
|
||||||
},
|
|
||||||
sender: useVerp ? campaignAddress + '@' + configItems.verpHostname : false,
|
|
||||||
|
|
||||||
envelope: useVerp ? {
|
let renderedText = (text || '').trim() ? (renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, text) : text) : htmlToText.fromString(renderedHtml, {
|
||||||
from: campaignAddress + '@' + configItems.verpHostname,
|
wordwrap: 130
|
||||||
to: message.subscription.email
|
});
|
||||||
} : false,
|
|
||||||
|
|
||||||
headers: {
|
return callback(null, {
|
||||||
'x-fbl': campaignAddress,
|
from: {
|
||||||
// custom header for SparkPost
|
name: campaign.from,
|
||||||
'x-msys-api': JSON.stringify({
|
address: campaign.address
|
||||||
campaign_id: campaignAddress
|
},
|
||||||
}),
|
xMailer: 'Mailtrain Mailer (+https://mailtrain.org)',
|
||||||
// custom header for SendGrid
|
to: {
|
||||||
'x-smtpapi': JSON.stringify({
|
name: [].concat(message.subscription.firstName || []).concat(message.subscription.lastName || []).join(' '),
|
||||||
unique_args: {
|
address: message.subscription.email
|
||||||
|
},
|
||||||
|
sender: useVerp ? campaignAddress + '@' + configItems.verpHostname : false,
|
||||||
|
|
||||||
|
envelope: useVerp ? {
|
||||||
|
from: campaignAddress + '@' + configItems.verpHostname,
|
||||||
|
to: message.subscription.email
|
||||||
|
} : false,
|
||||||
|
|
||||||
|
headers: {
|
||||||
|
'x-fbl': campaignAddress,
|
||||||
|
// custom header for SparkPost
|
||||||
|
'x-msys-api': JSON.stringify({
|
||||||
campaign_id: campaignAddress
|
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: '"' + list.name.replace(/[^a-z0-9\s'.,\-]/g, '').trim() + '" <' + list.cid + '.' + (url.parse(configItems.serviceUrl).hostname || 'localhost') + '>'
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
// custom header for Mailgun
|
list: {
|
||||||
'x-mailgun-variables': JSON.stringify({
|
unsubscribe: url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid + '?auto=yes')
|
||||||
campaign_id: campaignAddress
|
},
|
||||||
}),
|
subject: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.subject),
|
||||||
'List-ID': {
|
html: renderedHtml,
|
||||||
prepared: true,
|
text: renderedText,
|
||||||
value: '"' + list.name.replace(/[^a-z0-9\s'.,\-]/g, '').trim() + '" <' + list.cid + '.' + (url.parse(configItems.serviceUrl).hostname || 'localhost') + '>'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
unsubscribe: url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid + '?auto=yes')
|
|
||||||
},
|
|
||||||
subject: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.subject),
|
|
||||||
html: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html),
|
|
||||||
text: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.text),
|
|
||||||
|
|
||||||
attachments,
|
attachments,
|
||||||
encryptionKeys
|
encryptionKeys
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (!campaign.template && campaign.templateUrl) {
|
||||||
|
request.post({
|
||||||
|
url: campaign.templateUrl,
|
||||||
|
form: message.subscription.mergeTags
|
||||||
|
}, (err, httpResponse, body) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
if (httpResponse.statusCode !== 200) {
|
||||||
|
return callback(new Error('Received status code ' + httpResponse.statusCode + ' from ' + campaign.templateUrl));
|
||||||
|
}
|
||||||
|
renderAndSend(body && body.toString(), '', false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
renderAndSend(campaign.html, campaign.text, true);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
12
setup/sql/upgrade-00004.sql
Normal file
12
setup/sql/upgrade-00004.sql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Header section
|
||||||
|
# Define incrementing schema version number
|
||||||
|
SET @schema_version = '4';
|
||||||
|
|
||||||
|
# Adds new column 'template_url' to campaigns table
|
||||||
|
# Indicates that this campaign should fetch message content from this URL
|
||||||
|
ALTER TABLE `campaigns` ADD COLUMN `template_url` varchar(255) CHARACTER SET ascii DEFAULT NULL AFTER `template`;
|
||||||
|
|
||||||
|
# Footer section
|
||||||
|
LOCK TABLES `settings` WRITE;
|
||||||
|
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
|
||||||
|
UNLOCK TABLES;
|
|
@ -53,15 +53,29 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="template" class="col-sm-2 control-label">Template</label>
|
<label for="template" class="col-sm-2 control-label">Template</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
<select class="form-control" id="template" name="template">
|
|
||||||
<option value=""> –– Select –– </option>
|
<p class="form-control-static">
|
||||||
{{#each templateItems}}
|
Select a template:
|
||||||
<option value="{{id}}" {{#if selected}} selected {{/if}}>
|
</p>
|
||||||
{{name}}
|
<div>
|
||||||
</option>
|
<select class="form-control" id="template" name="template">
|
||||||
{{/each}}
|
<option value=""> –– Select –– </option>
|
||||||
</select>
|
{{#each templateItems}}
|
||||||
<span class="help-block">Not required. Creates a campaign specific copy from a template that you can later edit</span>
|
<option value="{{id}}" {{#if selected}} selected {{/if}}>
|
||||||
|
{{name}}
|
||||||
|
</option>
|
||||||
|
{{/each}}
|
||||||
|
</select>
|
||||||
|
<span class="help-block">Selecting a template creates a campaign specific copy from it</span>
|
||||||
|
</div>
|
||||||
|
<p class="form-control-static">
|
||||||
|
Or alternatively use an URL as the message content source:
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<input type="url" class="form-control" name="template-url" id="template-url" value="{{templateUrl}}" placeholder="http://example.com/message-render.php">
|
||||||
|
<span class="help-block">If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -106,60 +106,73 @@
|
||||||
Template Settings
|
Template Settings
|
||||||
</legend>
|
</legend>
|
||||||
|
|
||||||
<div class="form-group">
|
{{#if templateUrl}}
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="form-group">
|
||||||
<a class="btn btn-default" role="button" data-toggle="collapse" href="#mergeReference" aria-expanded="false" aria-controls="mergeReference">Merge tag reference</a>
|
<label for="template-url" class="col-sm-2 control-label">Template URL</label>
|
||||||
<div class="collapse" id="mergeReference">
|
<div class="col-sm-10">
|
||||||
<p>
|
<input type="url" class="form-control" name="template-url" id="template-url" value="{{templateUrl}}" placeholder="http://example.com/message-render.php">
|
||||||
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
|
<span class="help-block">If a message is sent then this URL will be POSTed to using Merge Tags as POST body. Use this if you want to generate the HTML message yourself</span>
|
||||||
text value used when <code>TAG_NAME</code> is empty.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<code>[FIRST_NAME]</code> – first name of the subcriber
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>[LAST_NAME]</code> – last name of the subcriber
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>[FULL_NAME]</code> – first and last names of the subcriber joined
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>[LINK_UNSUBSCRIBE]</code> – URL that points to the preferences page of the subscriber
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>[LINK_PREFERENCES]</code> – URL that points to the unsubscribe page
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<code>[LINK_BROWSER]</code> – URL to preview the message in a browser
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
In addition to that any custom field can have its own merge tag.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{{else}}
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="template-html" class="col-sm-2 control-label">Template content (HTML)</label>
|
|
||||||
<div class="col-sm-10">
|
|
||||||
{{#if disableWysiwyg}}
|
|
||||||
<div class="code-editor" id="template-html">{{html}}</div>
|
|
||||||
<input type="hidden" name="html">
|
|
||||||
{{else}}
|
|
||||||
<textarea class="form-control summernote" id="template-html" name="html" rows="8">{{html}}</textarea>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="template-text" class="col-sm-2 control-label">Template content (plaintext)</label>
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<div class="col-sm-10">
|
<a class="btn btn-default" role="button" data-toggle="collapse" href="#mergeReference" aria-expanded="false" aria-controls="mergeReference">Merge tag reference</a>
|
||||||
<textarea class="form-control" id="template-text" name="text" rows="10">{{text}}</textarea>
|
<div class="collapse" id="mergeReference">
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<code>[FIRST_NAME]</code> – first name of the subcriber
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>[LAST_NAME]</code> – last name of the subcriber
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>[FULL_NAME]</code> – first and last names of the subcriber joined
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>[LINK_UNSUBSCRIBE]</code> – URL that points to the preferences page of the subscriber
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>[LINK_PREFERENCES]</code> – URL that points to the unsubscribe page
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<code>[LINK_BROWSER]</code> – URL to preview the message in a browser
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
In addition to that any custom field can have its own merge tag.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-html" class="col-sm-2 control-label">Template content (HTML)</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
{{#if disableWysiwyg}}
|
||||||
|
<div class="code-editor" id="template-html">{{html}}</div>
|
||||||
|
<input type="hidden" name="html">
|
||||||
|
{{else}}
|
||||||
|
<textarea class="form-control summernote" id="template-html" name="html" rows="8">{{html}}</textarea>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-text" class="col-sm-2 control-label">Template content (plaintext)</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<textarea class="form-control" id="template-text" name="text" rows="10">{{text}}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{/if}}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue