Full support for message attachments

This commit is contained in:
Andris Reinman 2016-09-09 23:09:04 +03:00
parent bfc6983c93
commit 35bce32529
6 changed files with 229 additions and 106 deletions

View file

@ -374,7 +374,10 @@ module.exports.getAttachments = (campaign, callback) => {
if (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();
if (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) => {
if (err) {
return callback(err);
}
let query = 'SELECT `filename`, `content_type`, `content` FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1';
connection.query(query, [attachment, id], (err, rows) => {
let query = 'SELECT * FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1';
connection.query(query, [attachment, campaign], (err, rows) => {
connection.release();
if (err) {
return callback(err);

View file

@ -9,8 +9,9 @@ let tools = require('../lib/tools');
let express = require('express');
let request = require('request');
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) => {
if (err) {
req.flash('danger', err.message || err);
@ -53,13 +54,21 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
return next(err);
}
campaigns.getAttachments(campaign.id, (err, attachments) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
}
let renderHtml = (html, renderTags) => {
res.render('archive/view', {
layout: 'archive/layout',
message: renderTags ? tools.formatMessage(serviceUrl, campaign, list, subscription, html) : html,
campaign,
list,
subscription
subscription,
attachments,
csrfToken: req.csrfToken()
});
};
@ -101,6 +110,30 @@ router.get('/:campaign/:list/:subscription', (req, res, next) => {
});
});
});
});
});
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;

View file

@ -828,7 +828,7 @@ router.post('/attachment/download', passport.parseForm, passport.csrfProtection,
req.flash('danger', err && err.message || err);
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
} else if (!attachment) {
req.flash('success', 'Attachment uploaded');
req.flash('warning', 'Attachment not found');
return res.redirect('/campaigns/edit/' + campaign.id + '?tab=attachments');
}

View file

@ -18,6 +18,9 @@ let request = require('request');
let caches = require('../lib/caches');
let libmime = require('libmime');
let attachmentCache = new Map();
let attachmentCacheSize = 0;
function findUnsent(callback) {
let returnUnsent = (row, campaign) => {
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) {
campaigns.get(message.campaignId, false, (err, campaign) => {
if (err) {
@ -255,7 +306,11 @@ function formatMessage(message, callback) {
}
// replace data: images with embedded attachments
let attachments = [];
getAttachments(campaign, (err, attachments) => {
if (err) {
return callback(err);
}
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({
@ -322,6 +377,7 @@ function formatMessage(message, callback) {
encryptionKeys
});
});
});
};
if (campaign.sourceUrl) {

View file

@ -1,6 +1,18 @@
SET UNIQUE_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` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`list` int(11) unsigned NOT NULL,
@ -49,6 +61,7 @@ CREATE TABLE `campaigns` (
`html_prepared` longtext,
`text` longtext,
`status` tinyint(4) unsigned NOT NULL DEFAULT '1',
`tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0',
`scheduled` timestamp NULL DEFAULT NULL,
`status_change` timestamp NULL DEFAULT NULL,
`delivered` int(11) unsigned NOT NULL DEFAULT '0',
@ -72,6 +85,7 @@ CREATE TABLE `confirmations` (
`cid` varchar(255) CHARACTER SET ascii NOT NULL,
`list` int(11) unsigned NOT NULL,
`email` varchar(255) NOT NULL,
`opt_in_ip` varchar(100) DEFAULT NULL,
`data` text NOT NULL,
`created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
@ -186,7 +200,7 @@ CREATE TABLE `segments` (
`created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `list` (`list`),
KEY `name` (`name`(191)),
KEY `name` (`name`),
CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `settings` (
@ -195,7 +209,7 @@ CREATE TABLE `settings` (
`value` text NOT NULL,
PRIMARY KEY (`id`),
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 (2,'smtp_port','465');
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 (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 (17,'db_schema_version','17');
INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','19');
CREATE TABLE `subscription` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`cid` varchar(255) CHARACTER SET ascii NOT NULL,

View file

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