Full support for message attachments
This commit is contained in:
parent
bfc6983c93
commit
35bce32529
6 changed files with 229 additions and 106 deletions
|
@ -374,7 +374,10 @@ module.exports.getAttachments = (campaign, callback) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
connection.query('SELECT `id`, `filename`, `size`, `created` FROM `attachments` WHERE `campaign`=?', [campaign], (err, rows) => {
|
|
||||||
|
let keys = ['id', 'filename', 'content_type', 'size', 'created'];
|
||||||
|
|
||||||
|
connection.query('SELECT `' + keys.join('`, `') + '` FROM `attachments` WHERE `campaign`=?', [campaign], (err, rows) => {
|
||||||
connection.release();
|
connection.release();
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
@ -455,14 +458,14 @@ module.exports.deleteAttachment = (id, attachment, callback) => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports.getAttachment = (id, attachment, callback) => {
|
module.exports.getAttachment = (campaign, attachment, callback) => {
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = 'SELECT `filename`, `content_type`, `content` FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1';
|
let query = 'SELECT * FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1';
|
||||||
connection.query(query, [attachment, id], (err, rows) => {
|
connection.query(query, [attachment, campaign], (err, rows) => {
|
||||||
connection.release();
|
connection.release();
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
|
|
@ -9,8 +9,9 @@ let tools = require('../lib/tools');
|
||||||
let express = require('express');
|
let express = require('express');
|
||||||
let request = require('request');
|
let request = require('request');
|
||||||
let router = new express.Router();
|
let router = new express.Router();
|
||||||
|
let passport = require('../lib/passport');
|
||||||
|
|
||||||
router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
router.get('/:campaign/:list/:subscription', passport.csrfProtection, (req, res, next) => {
|
||||||
settings.get('serviceUrl', (err, serviceUrl) => {
|
settings.get('serviceUrl', (err, serviceUrl) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
req.flash('danger', err.message || err);
|
req.flash('danger', err.message || err);
|
||||||
|
@ -53,54 +54,86 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
|
||||||
return next(err);
|
return next(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
let renderHtml = (html, renderTags) => {
|
campaigns.getAttachments(campaign.id, (err, attachments) => {
|
||||||
res.render('archive/view', {
|
if (err) {
|
||||||
layout: 'archive/layout',
|
req.flash('danger', err.message || err);
|
||||||
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html) : html,
|
return res.redirect('/');
|
||||||
campaign,
|
|
||||||
list,
|
|
||||||
subscription
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let renderAndShow = (html, renderTags) => {
|
|
||||||
if (req.query.track === 'no') {
|
|
||||||
return renderHtml(html, renderTags);
|
|
||||||
}
|
}
|
||||||
// rewrite links to count clicks
|
|
||||||
links.updateLinks(campaign, list, subscription, serviceUrl, html, (err, html) => {
|
|
||||||
if (err) {
|
|
||||||
req.flash('danger', err.message || err);
|
|
||||||
return res.redirect('/');
|
|
||||||
}
|
|
||||||
renderHtml(html, renderTags);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (campaign.sourceUrl) {
|
let renderHtml = (html, renderTags) => {
|
||||||
let form = tools.getMessageLinks(serviceUrl, campaign, list, subscription);
|
res.render('archive/view', {
|
||||||
Object.keys(subscription.mergeTags).forEach(key => {
|
layout: 'archive/layout',
|
||||||
form[key] = subscription.mergeTags[key];
|
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html) : html,
|
||||||
});
|
campaign,
|
||||||
request.post({
|
list,
|
||||||
url: campaign.sourceUrl,
|
subscription,
|
||||||
form
|
attachments,
|
||||||
}, (err, httpResponse, body) => {
|
csrfToken: req.csrfToken()
|
||||||
if (err) {
|
});
|
||||||
return next(err);
|
};
|
||||||
|
|
||||||
|
let renderAndShow = (html, renderTags) => {
|
||||||
|
if (req.query.track === 'no') {
|
||||||
|
return renderHtml(html, renderTags);
|
||||||
}
|
}
|
||||||
if (httpResponse.statusCode !== 200) {
|
// rewrite links to count clicks
|
||||||
return next(new Error('Received status code ' + httpResponse.statusCode + ' from ' + campaign.sourceUrl));
|
links.updateLinks(campaign, list, subscription, serviceUrl, html, (err, html) => {
|
||||||
}
|
if (err) {
|
||||||
renderAndShow(body && body.toString(), false);
|
req.flash('danger', err.message || err);
|
||||||
});
|
return res.redirect('/');
|
||||||
} else {
|
}
|
||||||
renderAndShow(campaign.html, true);
|
renderHtml(html, renderTags);
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (campaign.sourceUrl) {
|
||||||
|
let form = tools.getMessageLinks(serviceUrl, campaign, list, subscription);
|
||||||
|
Object.keys(subscription.mergeTags).forEach(key => {
|
||||||
|
form[key] = subscription.mergeTags[key];
|
||||||
|
});
|
||||||
|
request.post({
|
||||||
|
url: campaign.sourceUrl,
|
||||||
|
form
|
||||||
|
}, (err, httpResponse, body) => {
|
||||||
|
if (err) {
|
||||||
|
return next(err);
|
||||||
|
}
|
||||||
|
if (httpResponse.statusCode !== 200) {
|
||||||
|
return next(new Error('Received status code ' + httpResponse.statusCode + ' from ' + campaign.sourceUrl));
|
||||||
|
}
|
||||||
|
renderAndShow(body && body.toString(), false);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
renderAndShow(campaign.html, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/attachment/download', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||||
|
let url = '/archive/' + encodeURIComponent(req.body.campaign || '') + '/' + encodeURIComponent(req.body.list || '') + '/' + encodeURIComponent(req.body.subscription || '');
|
||||||
|
campaigns.getByCid(req.body.campaign, (err, campaign) => {
|
||||||
|
if (err || !campaign) {
|
||||||
|
req.flash('danger', err && err.message || err || 'Could not find campaign with specified ID');
|
||||||
|
return res.redirect(url);
|
||||||
|
}
|
||||||
|
campaigns.getAttachment(campaign.id, Number(req.body.attachment), (err, attachment) => {
|
||||||
|
if (err) {
|
||||||
|
req.flash('danger', err && err.message || err);
|
||||||
|
return res.redirect(url);
|
||||||
|
} else if (!attachment) {
|
||||||
|
req.flash('warning', 'Attachment not found');
|
||||||
|
return res.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.set('Content-Disposition', 'attachment; filename="' + encodeURIComponent(attachment.filename).replace(/['()]/g, escape) + '"');
|
||||||
|
res.set('Content-Type', attachment.contentType);
|
||||||
|
res.send(attachment.content);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -828,7 +828,7 @@ router.post('/attachment/download', passport.parseForm, passport.csrfProtection,
|
||||||
req.flash('danger', err && err.message || err);
|
req.flash('danger', err && err.message || err);
|
||||||
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
||||||
} else if (!attachment) {
|
} else if (!attachment) {
|
||||||
req.flash('success', 'Attachment uploaded');
|
req.flash('warning', 'Attachment not found');
|
||||||
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,9 @@ let request = require('request');
|
||||||
let caches = require('../lib/caches');
|
let caches = require('../lib/caches');
|
||||||
let libmime = require('libmime');
|
let libmime = require('libmime');
|
||||||
|
|
||||||
|
let attachmentCache = new Map();
|
||||||
|
let attachmentCacheSize = 0;
|
||||||
|
|
||||||
function findUnsent(callback) {
|
function findUnsent(callback) {
|
||||||
let returnUnsent = (row, campaign) => {
|
let returnUnsent = (row, campaign) => {
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
|
@ -195,6 +198,54 @@ function findUnsent(callback) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getAttachments(campaign, callback) {
|
||||||
|
campaigns.getAttachments(campaign.id, (err, attachments) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
if (!attachments) {
|
||||||
|
return callback(null, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = [];
|
||||||
|
let pos = 0;
|
||||||
|
let getNextAttachment = () => {
|
||||||
|
if (pos >= attachments.length) {
|
||||||
|
return callback(null, response);
|
||||||
|
}
|
||||||
|
let attachment = attachments[pos++];
|
||||||
|
let aid = campaign.id + ':' + attachment.id;
|
||||||
|
if (attachmentCache.has(aid)) {
|
||||||
|
response.push(attachmentCache.get(aid));
|
||||||
|
return setImmediate(getNextAttachment);
|
||||||
|
}
|
||||||
|
campaigns.getAttachment(campaign.id, attachment.id, (err, attachment) => {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
if (!attachment || !attachment.content) {
|
||||||
|
return setImmediate(getNextAttachment);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.push(attachment);
|
||||||
|
|
||||||
|
// make sure we do not cache more buffers than 30MB
|
||||||
|
if (attachmentCacheSize + attachment.content.length > 30 * 1024 * 1024) {
|
||||||
|
attachmentCacheSize = 0;
|
||||||
|
attachmentCache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentCache.set(aid, attachment);
|
||||||
|
attachmentCacheSize += attachment.content.length;
|
||||||
|
|
||||||
|
return setImmediate(getNextAttachment);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getNextAttachment();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function formatMessage(message, callback) {
|
function formatMessage(message, callback) {
|
||||||
campaigns.get(message.campaignId, false, (err, campaign) => {
|
campaigns.get(message.campaignId, false, (err, campaign) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -255,71 +306,76 @@ function formatMessage(message, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// replace data: images with embedded attachments
|
// replace data: images with embedded attachments
|
||||||
let attachments = [];
|
getAttachments(campaign, (err, attachments) => {
|
||||||
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
if (err) {
|
||||||
let cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop();
|
return callback(err);
|
||||||
attachments.push({
|
}
|
||||||
path: dataUri,
|
|
||||||
cid
|
html = html.replace(/(<img\b[^>]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
|
||||||
|
let cid = shortid.generate() + '-attachments@' + campaign.address.split('@').pop();
|
||||||
|
attachments.push({
|
||||||
|
path: dataUri,
|
||||||
|
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('.');
|
||||||
|
|
||||||
let renderedHtml = renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html) : html;
|
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, {
|
let renderedText = (text || '').trim() ? (renderTags ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, text) : text) : htmlToText.fromString(renderedHtml, {
|
||||||
wordwrap: 130
|
wordwrap: 130
|
||||||
});
|
});
|
||||||
|
|
||||||
return callback(null, {
|
return callback(null, {
|
||||||
from: {
|
from: {
|
||||||
name: campaign.from,
|
name: campaign.from,
|
||||||
address: campaign.address
|
address: campaign.address
|
||||||
},
|
},
|
||||||
xMailer: 'Mailtrain Mailer (+https://mailtrain.org)',
|
xMailer: 'Mailtrain Mailer (+https://mailtrain.org)',
|
||||||
to: {
|
to: {
|
||||||
name: [].concat(message.subscription.firstName || []).concat(message.subscription.lastName || []).join(' '),
|
name: [].concat(message.subscription.firstName || []).concat(message.subscription.lastName || []).join(' '),
|
||||||
address: message.subscription.email
|
address: message.subscription.email
|
||||||
},
|
},
|
||||||
sender: useVerp ? campaignAddress + '@' + configItems.verpHostname : false,
|
sender: useVerp ? campaignAddress + '@' + configItems.verpHostname : false,
|
||||||
|
|
||||||
envelope: useVerp ? {
|
envelope: useVerp ? {
|
||||||
from: campaignAddress + '@' + configItems.verpHostname,
|
from: campaignAddress + '@' + configItems.verpHostname,
|
||||||
to: message.subscription.email
|
to: message.subscription.email
|
||||||
} : false,
|
} : false,
|
||||||
|
|
||||||
headers: {
|
headers: {
|
||||||
'x-fbl': campaignAddress,
|
'x-fbl': campaignAddress,
|
||||||
// custom header for SparkPost
|
// custom header for SparkPost
|
||||||
'x-msys-api': JSON.stringify({
|
'x-msys-api': JSON.stringify({
|
||||||
campaign_id: campaignAddress
|
|
||||||
}),
|
|
||||||
// custom header for SendGrid
|
|
||||||
'x-smtpapi': JSON.stringify({
|
|
||||||
unique_args: {
|
|
||||||
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: libmime.encodeWords(list.name) + ' <' + 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: libmime.encodeWords(list.name) + ' <' + 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: renderedHtml,
|
|
||||||
text: renderedText,
|
|
||||||
|
|
||||||
attachments,
|
attachments,
|
||||||
encryptionKeys
|
encryptionKeys
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,18 @@
|
||||||
SET UNIQUE_CHECKS=0;
|
SET UNIQUE_CHECKS=0;
|
||||||
SET FOREIGN_KEY_CHECKS=0;
|
SET FOREIGN_KEY_CHECKS=0;
|
||||||
|
|
||||||
|
CREATE TABLE `attachments` (
|
||||||
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
|
`campaign` int(11) unsigned NOT NULL,
|
||||||
|
`filename` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
|
||||||
|
`content_type` varchar(100) CHARACTER SET ascii NOT NULL DEFAULT '',
|
||||||
|
`content` longblob,
|
||||||
|
`size` int(11) NOT NULL,
|
||||||
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
KEY `campaign` (`campaign`),
|
||||||
|
CONSTRAINT `attachments_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
|
||||||
CREATE TABLE `campaign` (
|
CREATE TABLE `campaign` (
|
||||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`list` int(11) unsigned NOT NULL,
|
`list` int(11) unsigned NOT NULL,
|
||||||
|
@ -49,6 +61,7 @@ CREATE TABLE `campaigns` (
|
||||||
`html_prepared` longtext,
|
`html_prepared` longtext,
|
||||||
`text` longtext,
|
`text` longtext,
|
||||||
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
|
||||||
|
`tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0',
|
||||||
`scheduled` timestamp NULL DEFAULT NULL,
|
`scheduled` timestamp NULL DEFAULT NULL,
|
||||||
`status_change` timestamp NULL DEFAULT NULL,
|
`status_change` timestamp NULL DEFAULT NULL,
|
||||||
`delivered` int(11) unsigned NOT NULL DEFAULT '0',
|
`delivered` int(11) unsigned NOT NULL DEFAULT '0',
|
||||||
|
@ -72,6 +85,7 @@ CREATE TABLE `confirmations` (
|
||||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
`list` int(11) unsigned NOT NULL,
|
`list` int(11) unsigned NOT NULL,
|
||||||
`email` varchar(255) NOT NULL,
|
`email` varchar(255) NOT NULL,
|
||||||
|
`opt_in_ip` varchar(100) DEFAULT NULL,
|
||||||
`data` text NOT NULL,
|
`data` text NOT NULL,
|
||||||
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
|
@ -186,7 +200,7 @@ CREATE TABLE `segments` (
|
||||||
`created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
`created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
KEY `list` (`list`),
|
KEY `list` (`list`),
|
||||||
KEY `name` (`name`(191)),
|
KEY `name` (`name`),
|
||||||
CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
|
CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
CREATE TABLE `settings` (
|
CREATE TABLE `settings` (
|
||||||
|
@ -195,7 +209,7 @@ CREATE TABLE `settings` (
|
||||||
`value` text NOT NULL,
|
`value` text NOT NULL,
|
||||||
PRIMARY KEY (`id`),
|
PRIMARY KEY (`id`),
|
||||||
UNIQUE KEY `key` (`key`)
|
UNIQUE KEY `key` (`key`)
|
||||||
) ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8mb4;
|
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4;
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','465');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','465');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','TLS');
|
||||||
|
@ -212,7 +226,7 @@ INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awes
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','http://localhost:3000/');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','http://localhost:3000/');
|
||||||
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','17');
|
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','19');
|
||||||
CREATE TABLE `subscription` (
|
CREATE TABLE `subscription` (
|
||||||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
|
||||||
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
|
||||||
|
|
|
@ -1,2 +1,19 @@
|
||||||
|
{{{message}}}
|
||||||
|
|
||||||
{{{message}}}
|
{{#if attachments}}
|
||||||
|
<ul class="list-group">
|
||||||
|
{{#each attachments}}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<span class="badge">{{size}}</span>
|
||||||
|
<form method="post" action="/archive/attachment/download">
|
||||||
|
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
|
||||||
|
<input type="hidden" name="campaign" value="{{../campaign.cid}}" />
|
||||||
|
<input type="hidden" name="list" value="{{../list.cid}}" />
|
||||||
|
<input type="hidden" name="subscription" value="{{../subscription.cid}}" />
|
||||||
|
<input type="hidden" name="attachment" value="{{id}}" />
|
||||||
|
<button type="submit" class="btn btn-link btn-xs"><i class="glyphicon glyphicon-cloud-download"></i> {{filename}}</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue