Merge branch 'v1.3' of github.com:andris9/mailtrain into v1.3
This commit is contained in:
commit
1b0b7d7a14
11 changed files with 250 additions and 141 deletions
|
@ -37,6 +37,7 @@ Subscribe to Mailtrain Newsletter [here](http://mailtrain.org/subscription/EysIv
|
|||
## Upgrade
|
||||
|
||||
* 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
|
||||
|
||||
|
@ -80,9 +81,9 @@ In the IDE, start Mailtrain via `Run > Start Mailtrain` and access your site via
|
|||
Mailtrain uses webhooks integration to detect bounces and spam complaints. Currently supported webhooks are:
|
||||
|
||||
* **AWS SES** – create a SNS topic for complaints and bounces and use `http://domain/webhooks/aws` as the subscriber URL for these topics
|
||||
* **SparkPost** – use `http://domain/webhooks/sparkpost` as the webhook URL for bounces and complaints
|
||||
* **SendGrid** – use `http://domain/webhooks/sendgrid` as the webhook URL for bounces and complaints
|
||||
* **Mailgun** – use `http://domain/webhooks/mailgun` as the webhook URL for bounces and complaints
|
||||
* **SparkPost** – use `http://domain/webhooks/sparkpost` as the webhook URL for bounces and complaints ([instructions](https://github.com/andris9/mailtrain/wiki/Setting-up-Webhooks-for-SparkPost))
|
||||
* **SendGrid** – use `http://domain/webhooks/sendgrid` as the webhook URL for bounces and complaints ([instructions](https://github.com/andris9/mailtrain/wiki/Setting-up-Webhooks-for-SendGrid))
|
||||
* **Mailgun** – use `http://domain/webhooks/mailgun` as the webhook URL for bounces and complaints ([instructions](https://github.com/andris9/mailtrain/wiki/Setting-up-Webhooks-for-Mailgun))
|
||||
|
||||
Additionally Mailtrain (v1.1+) is able to use VERP-based bounce handling. This would require to have a compatible SMTP relay (the services mentioned above strip out or block VERP addresses in the SMTP envelope) and you also need to set up special MX DNS name that points to your Mailtrain installation server.
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ let Handlebars = require('handlebars');
|
|||
let fs = require('fs');
|
||||
let path = require('path');
|
||||
let templates = new Map();
|
||||
let htmlToText = require('html-to-text');
|
||||
|
||||
module.exports.transport = false;
|
||||
|
||||
|
@ -54,6 +55,10 @@ module.exports.sendMail = (mail, template, callback) => {
|
|||
|
||||
if (textRenderer) {
|
||||
mail.text = textRenderer(template.data || {});
|
||||
} else if (mail.html) {
|
||||
mail.text = htmlToText.fromString(mail.html, {
|
||||
wordwrap: 130
|
||||
});
|
||||
}
|
||||
|
||||
module.exports.transport.sendMail(mail, callback);
|
||||
|
|
|
@ -8,7 +8,7 @@ let segments = require('./segments');
|
|||
let subscriptions = require('./subscriptions');
|
||||
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) => {
|
||||
db.getConnection((err, connection) => {
|
||||
|
|
23
lib/tools.js
23
lib/tools.js
|
@ -15,7 +15,8 @@ module.exports = {
|
|||
createSlug,
|
||||
updateMenu,
|
||||
validateEmail,
|
||||
formatMessage
|
||||
formatMessage,
|
||||
getMessageLinks
|
||||
};
|
||||
|
||||
function toDbKey(key) {
|
||||
|
@ -147,17 +148,23 @@ function validateEmail(address, checkBlocked, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function getMessageLinks(serviceUrl, campaign, list, subscription) {
|
||||
return {
|
||||
LINK_UNSUBSCRIBE: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?auto=yes&c=' + campaign.cid),
|
||||
LINK_PREFERENCES: urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid),
|
||||
LINK_BROWSER: urllib.resolve(serviceUrl, '/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid)
|
||||
};
|
||||
}
|
||||
|
||||
function formatMessage(serviceUrl, campaign, list, subscription, message, filter) {
|
||||
filter = typeof filter === 'function' ? filter : (str => str);
|
||||
|
||||
let links = getMessageLinks(serviceUrl, campaign, list, subscription);
|
||||
|
||||
let getValue = key => {
|
||||
switch ((key || '').toString().toUpperCase().trim()) {
|
||||
case 'LINK_UNSUBSCRIBE':
|
||||
return urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?auto=yes&c=' + campaign.cid);
|
||||
case 'LINK_PREFERENCES':
|
||||
return urllib.resolve(serviceUrl, '/subscription/' + list.cid + '/manage/' + subscription.cid);
|
||||
case 'LINK_BROWSER':
|
||||
return urllib.resolve(serviceUrl, '/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid);
|
||||
key = (key || '').toString().toUpperCase().trim();
|
||||
if (links.hasOwnProperty(key)) {
|
||||
return links[key];
|
||||
}
|
||||
if (subscription.mergeTags.hasOwnProperty(key)) {
|
||||
return subscription.mergeTags[key];
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 3
|
||||
"schemaVersion": 4
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"geoip-ultralight": "^0.1.3",
|
||||
"handlebars": "^4.0.5",
|
||||
"hbs": "^4.0.0",
|
||||
"html-to-text": "^2.1.0",
|
||||
"humanize": "0.0.9",
|
||||
"isemail": "^2.1.0",
|
||||
"morgan": "^1.7.0",
|
||||
|
|
|
@ -7,6 +7,7 @@ let lists = require('../lib/models/lists');
|
|||
let subscriptions = require('../lib/models/subscriptions');
|
||||
let tools = require('../lib/tools');
|
||||
let express = require('express');
|
||||
let request = require('request');
|
||||
let router = new express.Router();
|
||||
|
||||
router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
||||
|
@ -64,8 +65,10 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
|||
return next(err);
|
||||
}
|
||||
|
||||
let renderAndShow = (html, renderTags) => {
|
||||
|
||||
// rewrite links to count clicks
|
||||
links.updateLinks(campaign, list, subscription, serviceUrl, campaign.html, (err, html) => {
|
||||
links.updateLinks(campaign, list, subscription, serviceUrl, html, (err, html) => {
|
||||
if (err) {
|
||||
req.flash('danger', err.message || err);
|
||||
return res.redirect('/');
|
||||
|
@ -73,12 +76,34 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
|||
|
||||
res.render('archive/view', {
|
||||
layout: 'archive/layout',
|
||||
message: tools.formatMessage(serviceUrl, campaign, list, subscription, html),
|
||||
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html) : html,
|
||||
campaign,
|
||||
list,
|
||||
subscription
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (campaign.templateUrl) {
|
||||
let form = tools.getMessageLinks(serviceUrl, campaign, list, subscription);
|
||||
Object.keys(subscription.mergeTags).forEach(key => {
|
||||
form[key] = subscription.mergeTags[key];
|
||||
});
|
||||
request.post({
|
||||
url: campaign.templateUrl,
|
||||
form
|
||||
}, (err, httpResponse, body) => {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (httpResponse.statusCode !== 200) {
|
||||
return next(new Error('Received status code ' + httpResponse.statusCode + ' from ' + campaign.templateUrl));
|
||||
}
|
||||
renderAndShow(body && body.toString(), false);
|
||||
});
|
||||
} else {
|
||||
renderAndShow(campaign.html, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,6 +13,8 @@ let settings = require('../lib/models/settings');
|
|||
let links = require('../lib/models/links');
|
||||
let shortid = require('shortid');
|
||||
let url = require('url');
|
||||
let htmlToText = require('html-to-text');
|
||||
let request = require('request');
|
||||
|
||||
function findUnsent(callback) {
|
||||
db.getConnection((err, connection) => {
|
||||
|
@ -149,7 +151,8 @@ function formatMessage(message, callback) {
|
|||
}
|
||||
});
|
||||
|
||||
links.updateLinks(campaign, list, message.subscription, configItems.serviceUrl, campaign.html, (err, html) => {
|
||||
let renderAndSend = (html, text, renderTags) => {
|
||||
links.updateLinks(campaign, list, message.subscription, configItems.serviceUrl, html, (err, html) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -167,6 +170,12 @@ function formatMessage(message, callback) {
|
|||
|
||||
let campaignAddress = [campaign.cid, list.cid, message.subscription.cid].join('.');
|
||||
|
||||
let renderedHtml = renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html) : html;
|
||||
|
||||
let renderedText = (text || '').trim() ? (renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, text) : text) : htmlToText.fromString(renderedHtml, {
|
||||
wordwrap: 130
|
||||
});
|
||||
|
||||
return callback(null, {
|
||||
from: {
|
||||
name: campaign.from,
|
||||
|
@ -209,13 +218,35 @@ function formatMessage(message, callback) {
|
|||
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),
|
||||
html: renderedHtml,
|
||||
text: renderedText,
|
||||
|
||||
attachments,
|
||||
encryptionKeys
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (campaign.templateUrl) {
|
||||
let form = tools.getMessageLinks(configItems.serviceUrl, campaign, list, message.subscription);
|
||||
Object.keys(message.subscription.mergeTags).forEach(key => {
|
||||
form[key] = message.subscription.mergeTags[key];
|
||||
});
|
||||
request.post({
|
||||
url: campaign.templateUrl,
|
||||
form
|
||||
}, (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,6 +53,11 @@
|
|||
<div class="form-group">
|
||||
<label for="template" class="col-sm-2 control-label">Template</label>
|
||||
<div class="col-sm-10">
|
||||
|
||||
<p class="form-control-static">
|
||||
Select a template:
|
||||
</p>
|
||||
<div>
|
||||
<select class="form-control" id="template" name="template">
|
||||
<option value=""> –– Select –– </option>
|
||||
{{#each templateItems}}
|
||||
|
@ -61,7 +66,16 @@
|
|||
</option>
|
||||
{{/each}}
|
||||
</select>
|
||||
<span class="help-block">Not required. Creates a campaign specific copy from a template that you can later edit</span>
|
||||
<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>
|
||||
|
||||
|
|
|
@ -106,6 +106,17 @@
|
|||
Template Settings
|
||||
</legend>
|
||||
|
||||
{{#if templateUrl}}
|
||||
<div class="form-group">
|
||||
<label for="template-url" class="col-sm-2 control-label">Template URL</label>
|
||||
<div class="col-sm-10">
|
||||
<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>
|
||||
{{else}}
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 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>
|
||||
|
@ -160,6 +171,8 @@
|
|||
<textarea class="form-control" id="template-text" name="text" rows="10">{{text}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{/if}}
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Reference in a new issue