1151 lines
		
	
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			1151 lines
		
	
	
	
		
			38 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| let tools = require('../tools');
 | |
| let db = require('../db');
 | |
| let lists = require('./lists');
 | |
| let templates = require('./templates');
 | |
| let segments = require('./segments');
 | |
| let subscriptions = require('./subscriptions');
 | |
| let shortid = require('shortid');
 | |
| let isUrl = require('is-url');
 | |
| let feed = require('../feed');
 | |
| let log = require('npmlog');
 | |
| let mailer = require('../mailer');
 | |
| let humanize = require('humanize');
 | |
| let _ = require('../translate')._;
 | |
| let util = require('util');
 | |
| let tableHelpers = require('../table-helpers');
 | |
| 
 | |
| let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'click_tracking_disabled', 'open_tracking_disabled'];
 | |
| 
 | |
| module.exports.list = (start, limit, callback) => {
 | |
|     tableHelpers.list('campaigns', ['*'], 'scheduled', null, start, limit, callback);
 | |
| };
 | |
| 
 | |
| module.exports.filter = (request, parent, callback) => {
 | |
|     let queryData;
 | |
|     if (parent) {
 | |
|         queryData = {
 | |
|             // only find normal and RSS parent campaigns at this point
 | |
|             where: '`parent`=?',
 | |
|             values: [parent]
 | |
|         };
 | |
|     } else {
 | |
|         queryData = {
 | |
|             // only find normal and RSS parent campaigns at this point
 | |
|             where: '`type` IN (?,?,?)',
 | |
|             values: [1, 2, 4]
 | |
|         };
 | |
|     }
 | |
| 
 | |
|     tableHelpers.filter('campaigns', ['*'], request, ['#', 'name', 'description', 'status', 'created'], ['name'], 'created DESC', queryData, callback);
 | |
| };
 | |
| 
 | |
| module.exports.filterQuicklist = (request, callback) => {
 | |
|     tableHelpers.filter('campaigns', ['id', 'name', 'description', 'created'], request, ['#', 'name', 'description', 'created'], ['name'], 'name ASC', null, callback);
 | |
| };
 | |
| 
 | |
| module.exports.filterClickedSubscribers = (campaign, linkId, request, columns, callback) => {
 | |
|     let queryData = {
 | |
|         where: 'campaign_tracker__' + campaign.id + '.list=? AND campaign_tracker__' + campaign.id + '.link=?',
 | |
|         values: [campaign.list, linkId]
 | |
|     };
 | |
| 
 | |
|     tableHelpers.filter('subscription__' + campaign.list + ' JOIN campaign_tracker__' + campaign.id + ' ON campaign_tracker__' + campaign.id + '.subscriber=subscription__' + campaign.list + '.id', ['*'], request, columns, ['email', 'first_name', 'last_name'], 'email ASC', queryData, callback);
 | |
| };
 | |
| 
 | |
| module.exports.statsClickedSubscribersByColumn = (campaign, linkId, request, column, limit, callback) => {
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let query_template = 'SELECT %s AS data, COUNT(*) AS cnt FROM `subscription__%d` JOIN `campaign_tracker__%d` ON `campaign_tracker__%d`.`list`=%d AND `campaign_tracker__%d`.`subscriber`=`subscription__%d`.`id` AND `campaign_tracker__%d`.`link`=%d  GROUP BY `%s` ORDER BY COUNT(`%s`) DESC, `%s`';
 | |
|         let query = util.format(query_template, column, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId, column, column, column);
 | |
| 
 | |
|         connection.query(query, (err, rows) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             let data = {};
 | |
|             let dataList = [];
 | |
|             let total = 0;
 | |
| 
 | |
|             rows.forEach((row, index) => {
 | |
|                 if (index < limit) {
 | |
|                     data[row.data] = row.cnt;
 | |
|                 } else {
 | |
|                     data.other = (data.other ? data.other : 0) + row.cnt;
 | |
|                 }
 | |
|                 total += row.cnt;
 | |
|             });
 | |
|             Object.keys(data).forEach(key => {
 | |
|                 let name = key + ': ' + data[key];
 | |
|                 dataList.push([name, data[key]]);
 | |
|             });
 | |
|             return callback(null, dataList, total);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.filterStatusSubscribers = (campaign, status, request, columns, callback) => {
 | |
|     let queryData = {
 | |
|         where: 'campaign__' + campaign.id + '.list=? AND campaign__' + campaign.id + '.segment=? AND campaign__' + campaign.id + '.status=?',
 | |
|         values: [campaign.list, campaign.segment && campaign.segment.id || 0, status]
 | |
|     };
 | |
| 
 | |
|     tableHelpers.filter('subscription__' + campaign.list + ' JOIN campaign__' + campaign.id + ' ON campaign__' + campaign.id + '.subscription=subscription__' + campaign.list + '.id', ['*'], request, columns, ['email', 'first_name', 'last_name'], 'email ASC', queryData, callback);
 | |
| };
 | |
| 
 | |
| module.exports.getByCid = (cid, callback) => {
 | |
|     cid = (cid || '').toString().trim();
 | |
|     if (!cid) {
 | |
|         return callback(new Error(_('Missing Campaign ID')));
 | |
|     }
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         connection.query('SELECT * FROM campaigns WHERE cid=?', [cid], (err, rows) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             if (!rows || !rows.length) {
 | |
|                 return callback(null, false);
 | |
|             }
 | |
| 
 | |
|             let campaign = tools.convertKeys(rows[0]);
 | |
|             return callback(null, campaign);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.get = (id, withSegment, callback) => {
 | |
|     id = Number(id) || 0;
 | |
| 
 | |
|     if (id < 1) {
 | |
|         return callback(new Error(_('Missing Campaign ID')));
 | |
|     }
 | |
| 
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         connection.query('SELECT * FROM campaigns WHERE id=?', [id], (err, rows) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             if (!rows || !rows.length) {
 | |
|                 return callback(null, false);
 | |
|             }
 | |
| 
 | |
|             let campaign = tools.convertKeys(rows[0]);
 | |
| 
 | |
|             let handleSegment = () => {
 | |
| 
 | |
|                 if (!campaign.segment || !withSegment) {
 | |
|                     return callback(null, campaign);
 | |
|                 } else {
 | |
|                     segments.get(campaign.segment, (err, segment) => {
 | |
|                         if (err || !segment) {
 | |
|                             // ignore
 | |
|                             return callback(null, campaign);
 | |
|                         }
 | |
|                         segments.subscribers(segment.id, true, (err, subscribers) => {
 | |
|                             if (err || !subscribers) {
 | |
|                                 segment.subscribers = 0;
 | |
|                             } else {
 | |
|                                 segment.subscribers = subscribers;
 | |
|                             }
 | |
|                             campaign.segment = segment;
 | |
|                             return callback(null, campaign);
 | |
|                         });
 | |
|                     });
 | |
|                 }
 | |
|             };
 | |
| 
 | |
|             if (!campaign.parent) {
 | |
|                 return handleSegment();
 | |
|             }
 | |
| 
 | |
|             db.getConnection((err, connection) => {
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 connection.query('SELECT `id`, `cid`, `name` FROM campaigns WHERE id=?', [campaign.parent], (err, rows) => {
 | |
|                     connection.release();
 | |
|                     if (err) {
 | |
|                         return callback(err);
 | |
|                     }
 | |
| 
 | |
|                     if (!rows || !rows.length) {
 | |
|                         return handleSegment();
 | |
|                     }
 | |
| 
 | |
|                     campaign.parent = tools.convertKeys(rows[0]);
 | |
|                     return handleSegment();
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.getAttachments = (campaign, callback) => {
 | |
|     campaign = Number(campaign) || 0;
 | |
| 
 | |
|     if (campaign < 1) {
 | |
|         return callback(new Error(_('Missing Campaign ID')));
 | |
|     }
 | |
| 
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         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);
 | |
|             }
 | |
| 
 | |
|             if (!rows || !rows.length) {
 | |
|                 return callback(null, []);
 | |
|             }
 | |
| 
 | |
|             let attachments = rows.map((row, i) => {
 | |
|                 row = tools.convertKeys(row);
 | |
|                 row.index = i + 1;
 | |
|                 row.size = humanize.filesize(Number(row.size) || 0);
 | |
|                 return row;
 | |
|             });
 | |
|             return callback(null, attachments);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.addAttachment = (id, attachment, callback) => {
 | |
| 
 | |
|     let size = attachment.content ? attachment.content.length : 0;
 | |
| 
 | |
|     if (!size) {
 | |
|         return callback(new Error(_('Emtpy or too large attahcment')));
 | |
|     }
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let keys = ['campaign', 'size'];
 | |
|         let values = [id, size];
 | |
| 
 | |
|         Object.keys(attachment).forEach(key => {
 | |
|             let value;
 | |
|             if (Buffer.isBuffer(attachment[key])) {
 | |
|                 value = attachment[key];
 | |
|             } else {
 | |
|                 value = typeof attachment[key] === 'number' ? attachment[key] : (attachment[key] || '').toString().trim();
 | |
|             }
 | |
| 
 | |
|             key = tools.toDbKey(key);
 | |
|             keys.push(key);
 | |
|             values.push(value);
 | |
|         });
 | |
| 
 | |
|         let query = 'INSERT INTO `attachments` (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
 | |
|         connection.query(query, values, (err, result) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             let attachmentId = result && result.insertId || false;
 | |
|             return callback(null, attachmentId);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.deleteAttachment = (id, attachment, callback) => {
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let query = 'DELETE FROM `attachments` WHERE `id`=? AND `campaign`=? LIMIT 1';
 | |
|         connection.query(query, [attachment, id], (err, result) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             let deleted = result && result.affectedRows || false;
 | |
|             return callback(null, deleted);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.getAttachment = (campaign, attachment, callback) => {
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         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);
 | |
|             }
 | |
|             if (!rows || !rows.length) {
 | |
|                 return callback(null, false);
 | |
|             }
 | |
| 
 | |
|             let attachment = tools.convertKeys(rows[0]);
 | |
|             return callback(null, attachment);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.getLinks = (id, linkId, callback) => {
 | |
|     if (!callback && typeof linkId === 'function') {
 | |
|         callback = linkId;
 | |
|         linkId = false;
 | |
|     }
 | |
| 
 | |
|     id = Number(id) || 0;
 | |
|     linkId = Number(linkId) || 0;
 | |
| 
 | |
|     if (id < 1) {
 | |
|         return callback(new Error(_('Missing Campaign ID')));
 | |
|     }
 | |
| 
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let query;
 | |
|         let values;
 | |
| 
 | |
|         if (!linkId) {
 | |
|             query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `campaign`=? LIMIT 1000';
 | |
|             values = [id];
 | |
|         } else {
 | |
|             query = 'SELECT `id`, `url`, `clicks` FROM links WHERE `id`=? AND `campaign`=? LIMIT 1';
 | |
|             values = [linkId, id];
 | |
|         }
 | |
| 
 | |
|         connection.query(query, values, (err, rows) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             if (!rows || !rows.length) {
 | |
|                 return callback(null, []);
 | |
|             }
 | |
| 
 | |
|             let links = rows.map(
 | |
|                 row => tools.convertKeys(row)
 | |
|             ).sort((a, b) => (
 | |
|                 a.url.replace(/^https?:\/\/(www.)?/, '').toLowerCase()).localeCompare(b.url.replace(/^https?:\/\/(www.)?/, '').toLowerCase()));
 | |
| 
 | |
|             return callback(null, links);
 | |
|         });
 | |
| 
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.create = (campaign, opts, callback) => {
 | |
| 
 | |
|     campaign = tools.convertKeys(campaign);
 | |
|     let name = (campaign.name || '').toString().trim();
 | |
| 
 | |
|     campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0;
 | |
|     campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0;
 | |
| 
 | |
|     opts = opts || {};
 | |
| 
 | |
|     if (/^\d+:\d+$/.test(campaign.list)) {
 | |
|         campaign.segment = Number(campaign.list.split(':').pop());
 | |
|         campaign.list = Number(campaign.list.split(':').shift());
 | |
|     } else {
 | |
|         campaign.list = Number(campaign.list) || 0;
 | |
|         campaign.segment = 0;
 | |
|     }
 | |
| 
 | |
|     switch ((campaign.type || '').toString().trim().toLowerCase()) {
 | |
|         case 'triggered':
 | |
|             campaign.type = 4;
 | |
|             break;
 | |
|         case 'rss':
 | |
|             campaign.type = 2;
 | |
|             break;
 | |
|         case 'entry':
 | |
|             if (opts.parent) {
 | |
|                 campaign.type = 3;
 | |
|             } else {
 | |
|                 campaign.type = 1;
 | |
|             }
 | |
|             break;
 | |
|         case 'normal':
 | |
|         default:
 | |
|             campaign.type = 1;
 | |
|     }
 | |
| 
 | |
|     campaign.template = Number(campaign.template) || 0;
 | |
| 
 | |
|     if (!name) {
 | |
|         return callback(new Error(_('Campaign Name must be set')));
 | |
|     }
 | |
| 
 | |
|     if (campaign.type === 2 && (!campaign.sourceUrl || !isUrl(campaign.sourceUrl))) {
 | |
|         return callback(new Error(_('RSS URL must be set and needs to be a valid URL')));
 | |
|     }
 | |
| 
 | |
|     let getList = (listId, callback) => {
 | |
|         if (campaign.type === 4) {
 | |
|             return callback(null, false);
 | |
|         }
 | |
| 
 | |
|         lists.get(listId, (err, list) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
|             return callback(null, list || {
 | |
|                 id: listId
 | |
|             });
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     getList(campaign.list, err => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let keys = ['name', 'type'];
 | |
|         let values = [name, campaign.type];
 | |
| 
 | |
|         if (campaign.type === 2) {
 | |
|             keys.push('status');
 | |
|             values.push(5); // inactive
 | |
|         }
 | |
| 
 | |
|         if (campaign.type === 3) {
 | |
|             keys.push('status', 'parent');
 | |
|             values.push(2, opts.parent);
 | |
|         }
 | |
| 
 | |
|         if (campaign.type === 4) {
 | |
|             keys.push('status');
 | |
|             values.push(6); // active
 | |
|         }
 | |
| 
 | |
|         let create = next => {
 | |
|             Object.keys(campaign).forEach(key => {
 | |
|                 let value = typeof campaign[key] === 'number' ? campaign[key] : (campaign[key] || '').toString().trim();
 | |
|                 key = tools.toDbKey(key);
 | |
|                 if (key === 'description') {
 | |
|                     value = tools.purifyHTML(value);
 | |
|                 }
 | |
|                 if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
 | |
|                     keys.push(key);
 | |
|                     values.push(value);
 | |
|                 }
 | |
|             });
 | |
| 
 | |
|             let cid = shortid.generate();
 | |
|             keys.push('cid');
 | |
|             values.push(cid);
 | |
| 
 | |
|             tools.prepareHtml(campaign.html, (err, preparedHtml) => {
 | |
|                 if (err) {
 | |
|                     log.error('jsdom', err);
 | |
|                 }
 | |
| 
 | |
|                 if (!preparedHtml) {
 | |
|                     preparedHtml = campaign.html;
 | |
|                 }
 | |
| 
 | |
|                 keys.push('html_prepared');
 | |
|                 values.push(preparedHtml);
 | |
| 
 | |
|                 db.getConnection((err, connection) => {
 | |
|                     if (err) {
 | |
|                         return next(err);
 | |
|                     }
 | |
| 
 | |
|                     let query = 'INSERT INTO campaigns (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(',') + ')';
 | |
|                     connection.query(query, values, (err, result) => {
 | |
|                         connection.release();
 | |
|                         if (err) {
 | |
|                             return next(err);
 | |
|                         }
 | |
| 
 | |
|                         let campaignId = result && result.insertId || false;
 | |
|                         if (!campaignId) {
 | |
|                             return next(null, false);
 | |
|                         }
 | |
| 
 | |
|                         // we are going to aqcuire a lot of log info, so we are putting
 | |
|                         // sending logs into separate tables
 | |
|                         createCampaignTables(campaignId, err => {
 | |
|                             if (err) {
 | |
|                                 // FIXME: rollback
 | |
|                                 return next(err);
 | |
|                             }
 | |
|                             return next(null, campaignId);
 | |
|                         });
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
|         };
 | |
| 
 | |
|         if (campaign.type === 2) {
 | |
|             feed.fetch(campaign.sourceUrl, (err, entries) => {
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
| 
 | |
|                 mailer.getTemplate('emails/rss-html.hbs', (err, rendererHtml) => {
 | |
|                     if (err) {
 | |
|                         return callback(err);
 | |
|                     }
 | |
| 
 | |
|                     campaign.html = rendererHtml();
 | |
| 
 | |
|                     create((err, campaignId) => {
 | |
|                         if (err) {
 | |
|                             return callback(err);
 | |
|                         }
 | |
|                         if (!campaignId || !entries.length) {
 | |
|                             return callback(null, campaignId);
 | |
|                         }
 | |
| 
 | |
|                         db.getConnection((err, connection) => {
 | |
|                             if (err) {
 | |
|                                 return callback(err);
 | |
|                             }
 | |
| 
 | |
|                             // store references to already existing feed entries
 | |
|                             // this is needed to detect new entries
 | |
|                             let query = 'INSERT IGNORE INTO `rss` (`parent`,`guid`,`pubdate`) VALUES ' + entries.map(() => '(?,?,?)').join(',');
 | |
| 
 | |
|                             values = [];
 | |
|                             entries.forEach(entry => {
 | |
|                                 values.push(campaignId, entry.guid, entry.date);
 | |
|                             });
 | |
| 
 | |
|                             connection.query(query, values, err => {
 | |
|                                 connection.release();
 | |
|                                 if (err) {
 | |
|                                     // too late to report as failed
 | |
|                                     log.error('RSS', err);
 | |
|                                 }
 | |
| 
 | |
|                                 return callback(null, campaignId);
 | |
|                             });
 | |
|                         });
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
|             return;
 | |
|         } else if (campaign.template) {
 | |
|             templates.get(campaign.template, (err, template) => {
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 if (!template) {
 | |
|                     return callback(new Error(_('Selected template not found')));
 | |
|                 }
 | |
| 
 | |
|                 campaign.editorName = template.editorName;
 | |
|                 campaign.editorData = template.editorData;
 | |
|                 campaign.html = template.html;
 | |
|                 campaign.text = template.text;
 | |
| 
 | |
|                 create(callback);
 | |
|             });
 | |
|             return;
 | |
|         } else {
 | |
|             return create(callback);
 | |
|         }
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.update = (id, updates, callback) => {
 | |
|     updates = updates || {};
 | |
|     id = Number(id) || 0;
 | |
| 
 | |
|     if (id < 1) {
 | |
|         return callback(new Error(_('Missing Campaign ID')));
 | |
|     }
 | |
| 
 | |
|     let campaign = tools.convertKeys(updates);
 | |
|     let name = (campaign.name || '').toString().trim();
 | |
| 
 | |
|     campaign.openTrackingDisabled = campaign.openTrackingDisabled ? 1 : 0;
 | |
|     campaign.clickTrackingDisabled = campaign.clickTrackingDisabled ? 1 : 0;
 | |
| 
 | |
|     if (!name) {
 | |
|         return callback(new Error(_('Campaign Name must be set')));
 | |
|     }
 | |
| 
 | |
|     if (/^\d+:\d+$/.test(campaign.list)) {
 | |
|         campaign.segment = Number(campaign.list.split(':').pop());
 | |
|         campaign.list = Number(campaign.list.split(':').shift());
 | |
|     } else {
 | |
|         campaign.list = Number(campaign.list) || 0;
 | |
|         campaign.segment = 0;
 | |
|     }
 | |
| 
 | |
|     let getList = (listId, callback) => {
 | |
|         lists.get(listId, (err, list) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
|             return callback(null, list || {
 | |
|                 id: listId
 | |
|             });
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     getList(campaign.list, err => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let keys = ['name'];
 | |
|         let values = [name];
 | |
| 
 | |
|         Object.keys(campaign).forEach(key => {
 | |
|             let value = typeof campaign[key] === 'number' ? campaign[key] : (campaign[key] || '').toString().trim();
 | |
|             key = tools.toDbKey(key);
 | |
|             if (key === 'description') {
 | |
|                 value = tools.purifyHTML(value);
 | |
|             }
 | |
|             if (allowedKeys.indexOf(key) >= 0 && keys.indexOf(key) < 0) {
 | |
|                 keys.push(key);
 | |
|                 values.push(value);
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         tools.prepareHtml(campaign.html, (err, preparedHtml) => {
 | |
|             if (err) {
 | |
|                 log.error('jsdom', err);
 | |
|             }
 | |
| 
 | |
|             if (!preparedHtml) {
 | |
|                 preparedHtml = campaign.html;
 | |
|             }
 | |
| 
 | |
|             keys.push('html_prepared');
 | |
|             values.push(preparedHtml);
 | |
| 
 | |
|             db.getConnection((err, connection) => {
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
| 
 | |
|                 values.push(id);
 | |
| 
 | |
|                 connection.query('UPDATE campaigns SET ' + keys.map(key => '`' + key + '`=?').join(', ') + ' WHERE id=? LIMIT 1', values, (err, result) => {
 | |
|                     if (err) {
 | |
|                         connection.release();
 | |
|                         return callback(err);
 | |
|                     }
 | |
|                     let affected = result && result.affectedRows || false;
 | |
| 
 | |
|                     if (!affected) {
 | |
|                         connection.release();
 | |
|                         return callback(null, affected);
 | |
|                     }
 | |
| 
 | |
|                     connection.query('SELECT `type`, `source_url` FROM campaigns WHERE id=? LIMIT 1', [id], (err, rows) => {
 | |
|                         connection.release();
 | |
|                         if (err) {
 | |
|                             return callback(err);
 | |
|                         }
 | |
| 
 | |
|                         if (!rows || !rows[0] || rows[0].type !== 2) {
 | |
|                             // if not RSS, then nothing to do here
 | |
|                             return callback(null, affected);
 | |
|                         }
 | |
| 
 | |
|                         // update seen rss entries to avoid sending old entries to subscribers
 | |
|                         feed.fetch(rows[0].source_url, (err, entries) => {
 | |
|                             if (err) {
 | |
|                                 return callback(err);
 | |
|                             }
 | |
| 
 | |
|                             db.getConnection((err, connection) => {
 | |
|                                 if (err) {
 | |
|                                     return callback(err);
 | |
|                                 }
 | |
| 
 | |
|                                 let query = 'INSERT IGNORE INTO `rss` (`parent`,`guid`,`pubdate`) VALUES ' + entries.map(() => '(?,?,?)').join(',');
 | |
| 
 | |
|                                 values = [];
 | |
|                                 entries.forEach(entry => {
 | |
|                                     values.push(id, entry.guid, entry.date);
 | |
|                                 });
 | |
| 
 | |
|                                 connection.query(query, values, err => {
 | |
|                                     connection.release();
 | |
|                                     if (err) {
 | |
|                                         // too late to report as failed
 | |
|                                         log.error('RSS', err);
 | |
|                                     }
 | |
|                                     return callback(null, affected);
 | |
|                                 });
 | |
|                             });
 | |
|                         });
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.delete = (id, callback) => {
 | |
|     id = Number(id) || 0;
 | |
| 
 | |
|     if (id < 1) {
 | |
|         return callback(new Error(_('Missing Campaign ID')));
 | |
|     }
 | |
| 
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         connection.query('DELETE FROM campaigns WHERE id=? LIMIT 1', [id], (err, result) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             let affected = result && result.affectedRows || 0;
 | |
|             removeCampaignTables(id, err => {
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
| 
 | |
|                 db.clearCache('sender', () => {
 | |
|                     callback(null, affected);
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.send = (id, scheduled, callback) => {
 | |
|     module.exports.get(id, false, (err, campaign) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         if (campaign.status === 2) { // already sending
 | |
|             return callback(null, false);
 | |
|         }
 | |
| 
 | |
|         db.getConnection((err, connection) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             let query;
 | |
|             let values;
 | |
|             if (scheduled) {
 | |
|                 query = 'UPDATE campaigns SET `status`=2, `scheduled`=?, `status_change`=NOW() WHERE id=? LIMIT 1';
 | |
|                 values = [scheduled, id];
 | |
|             } else {
 | |
|                 query = 'UPDATE campaigns SET `status`=2, `status_change`=NOW() WHERE id=? LIMIT 1';
 | |
|                 values = [id];
 | |
|             }
 | |
| 
 | |
|             // campaigns marked as status=2 should be picked up by the sending processes
 | |
|             connection.query(query, values, err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 return callback(null, true);
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.pause = (id, callback) => {
 | |
|     module.exports.get(id, false, (err, campaign) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         if (campaign.status !== 2) {
 | |
|             return callback(null, false);
 | |
|         }
 | |
| 
 | |
|         db.getConnection((err, connection) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             // campaigns marked as status=4 are paused
 | |
|             connection.query('UPDATE campaigns SET `status`=4, `status_change`=NOW() WHERE id=? LIMIT 1', [id], err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 db.clearCache('sender', () => {
 | |
|                     callback(null, true);
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.reset = (id, callback) => {
 | |
|     module.exports.get(id, false, (err, campaign) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         if (campaign.status !== 3 && campaign.status !== 4 && !(campaign.status === 2 && campaign.scheduled > new Date())) {
 | |
|             return callback(null, false);
 | |
|         }
 | |
| 
 | |
|         db.getConnection((err, connection) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             connection.query('UPDATE campaigns SET `status`=1, `status_change`=NULL, `delivered`=0, `opened`=0, `clicks`=0, `bounced`=0, `complained`=0, `unsubscribed`=0, `blacklisted`=0 WHERE id=? LIMIT 1', [id], err => {
 | |
|                 if (err) {
 | |
|                     connection.release();
 | |
|                     return callback(err);
 | |
|                 }
 | |
| 
 | |
|                 db.clearCache('sender', () => {
 | |
|                     connection.query('UPDATE links SET `clicks`=0 WHERE campaign=?', [id], err => {
 | |
|                         if (err) {
 | |
|                             connection.release();
 | |
|                             return callback(err);
 | |
|                         }
 | |
|                         connection.query('TRUNCATE TABLE `campaign__' + id + '`', [id], err => {
 | |
|                             if (err) {
 | |
|                                 connection.release();
 | |
|                                 return callback(err);
 | |
|                             }
 | |
|                             connection.query('TRUNCATE TABLE `campaign_tracker__' + id + '`', [id], err => {
 | |
|                                 connection.release();
 | |
|                                 if (err) {
 | |
|                                     return callback(err);
 | |
|                                 }
 | |
|                                 return callback(null, true);
 | |
|                             });
 | |
|                         });
 | |
|                     });
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.activate = (id, callback) => {
 | |
|     module.exports.get(id, false, (err, campaign) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         if (campaign.status !== 5) {
 | |
|             return callback(null, false);
 | |
|         }
 | |
| 
 | |
|         db.getConnection((err, connection) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             // campaigns marked as status=5 are paused
 | |
|             connection.query('UPDATE campaigns SET `status`=6, `status_change`=NOW() WHERE id=? LIMIT 1', [id], err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 return callback(null, true);
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.inactivate = (id, callback) => {
 | |
|     module.exports.get(id, false, (err, campaign) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         if (campaign.status !== 6) {
 | |
|             return callback(null, false);
 | |
|         }
 | |
| 
 | |
|         db.getConnection((err, connection) => {
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             // campaigns marked as status=6 are paused
 | |
|             connection.query('UPDATE campaigns SET `status`=5, `status_change`=NOW() WHERE id=? LIMIT 1', [id], err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 return callback(null, true);
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.getMail = (campaignId, listId, subscriptionId, callback) => {
 | |
|     campaignId = Number(campaignId) || 0;
 | |
|     listId = Number(listId) || 0;
 | |
|     subscriptionId = Number(subscriptionId) || 0;
 | |
| 
 | |
|     if (campaignId < 1 || listId < 1 || subscriptionId < 1) {
 | |
|         return callback(new Error(_('Invalid or missing message ID')));
 | |
|     }
 | |
| 
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         connection.query('SELECT * FROM `campaign__' + campaignId + '` WHERE list=? AND subscription=?', [listId, subscriptionId], (err, rows) => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
| 
 | |
|             if (!rows || !rows.length) {
 | |
|                 return callback(null, false);
 | |
|             }
 | |
| 
 | |
|             let campaign = tools.convertKeys(rows[0]);
 | |
|             return callback(null, campaign);
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.findMailByResponse = (responseId, callback) => {
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         connection.query('SELECT id FROM campaigns ORDER BY id DESC', [], (err, campaignList) => {
 | |
|             if (err) {
 | |
|                 connection.release();
 | |
|                 return callback(err);
 | |
|             }
 | |
|             if (!campaignList || !campaignList.length) {
 | |
|                 connection.release();
 | |
|                 return callback(null, false);
 | |
|             }
 | |
| 
 | |
|             let pos = 0;
 | |
|             let checkNext = () => {
 | |
|                 if (pos >= campaignList.length) {
 | |
|                     // all campaigns checked, result not found
 | |
|                     connection.release();
 | |
|                     return callback(null, false);
 | |
|                 }
 | |
|                 let campaign = campaignList[pos++];
 | |
| 
 | |
|                 connection.query('SELECT id, list, segment, subscription FROM `campaign__' + campaign.id + '` WHERE `response_id`=? LIMIT 1', [responseId], (err, rows) => {
 | |
|                     if (err || !rows || !rows.length) {
 | |
|                         return checkNext();
 | |
|                     }
 | |
|                     connection.release();
 | |
| 
 | |
|                     let message = rows[0];
 | |
|                     message.campaign = campaign.id;
 | |
|                     return callback(null, message);
 | |
|                 });
 | |
|             };
 | |
| 
 | |
|             checkNext();
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.findMailByCampaign = (campaignHeader, callback) => {
 | |
|     if (!campaignHeader) {
 | |
|         return callback(null, false);
 | |
|     }
 | |
| 
 | |
|     let parts = campaignHeader.split('.');
 | |
|     let cCid = parts.shift();
 | |
|     let sCid = parts.pop();
 | |
| 
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
|         let query = 'SELECT `id`, `list`, `segment` FROM `campaigns` WHERE `cid`=? LIMIT 1';
 | |
|         connection.query(query, [cCid], (err, rows) => {
 | |
|             if (err) {
 | |
|                 connection.release();
 | |
|                 return callback(err);
 | |
|             }
 | |
|             if (!rows || !rows.length) {
 | |
|                 connection.release();
 | |
|                 return callback(null, false);
 | |
|             }
 | |
| 
 | |
|             let campaignId = rows[0].id;
 | |
|             let listId = rows[0].list;
 | |
|             let segmentId = rows[0].segment;
 | |
| 
 | |
|             let query = 'SELECT id FROM `subscription__' + listId + '` WHERE cid=? LIMIT 1';
 | |
|             connection.query(query, [sCid], (err, rows) => {
 | |
|                 if (err) {
 | |
|                     connection.release();
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 if (!rows || !rows.length) {
 | |
|                     connection.release();
 | |
|                     return callback(null, false);
 | |
|                 }
 | |
| 
 | |
|                 let subscriptionId = rows[0].id;
 | |
| 
 | |
|                 let query = 'SELECT `id`, `list`, `segment`, `subscription` FROM `campaign__' + campaignId + '` WHERE `list`=? AND `segment`=? AND `subscription`=? LIMIT 1';
 | |
|                 connection.query(query, [listId, segmentId, subscriptionId], (err, rows) => {
 | |
|                     connection.release();
 | |
|                     if (err) {
 | |
|                         return callback(err);
 | |
|                     }
 | |
|                     if (!rows || !rows.length) {
 | |
|                         return callback(null, false);
 | |
|                     }
 | |
| 
 | |
|                     let message = rows[0];
 | |
|                     message.campaign = campaignId;
 | |
| 
 | |
|                     return callback(null, message);
 | |
|                 });
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.updateMessage = (message, status, updateSubscription, callback) => {
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let statusCode;
 | |
|         if (status === 'unsubscribed') {
 | |
|             statusCode = subscriptions.Status.UNSUBSCRIBED;
 | |
|         } else if (status === 'bounced') {
 | |
|             statusCode = subscriptions.Status.BOUNCED;
 | |
|         } else if (status === 'complained') {
 | |
|             statusCode = subscriptions.Status.COMPLAINED;
 | |
|         } else {
 | |
|             return callback(new Error(_('Unrecognized message status')));
 | |
|         }
 | |
| 
 | |
|         let query = 'UPDATE `campaigns` SET `' + status + '`=`' + status + '`+1 WHERE id=? LIMIT 1';
 | |
|         connection.query(query, [message.campaign], () => {
 | |
| 
 | |
|             let query = 'UPDATE `campaign__' + message.campaign + '` SET status=?, updated=NOW() WHERE id=? LIMIT 1';
 | |
|             connection.query(query, [statusCode, message.id], err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
| 
 | |
|                 if (updateSubscription) {
 | |
|                     subscriptions.changeStatus(message.list, message.subscription, statusCode === subscriptions.Status.UNSUBSCRIBED ? message.campaign : false, statusCode, callback);
 | |
|                 } else {
 | |
|                     return callback(null, true);
 | |
|                 }
 | |
|             });
 | |
|         });
 | |
| 
 | |
|     });
 | |
| };
 | |
| 
 | |
| module.exports.updateMessageResponse = (message, response, response_id, callback) => {
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
| 
 | |
|         let query = 'UPDATE `campaign__' + message.campaign + '` SET `response`=?, `response_id`=? WHERE id=? LIMIT 1';
 | |
|         connection.query(query, [response, response_id, message.id], err => {
 | |
|             connection.release();
 | |
|             if (err) {
 | |
|                 return callback(err);
 | |
|             }
 | |
|             return callback(null, true);
 | |
|         });
 | |
| 
 | |
|     });
 | |
| };
 | |
| 
 | |
| function createCampaignTables(id, callback) {
 | |
|     let query = 'CREATE TABLE `campaign__' + id + '` LIKE campaign';
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
|         connection.query(query, err => {
 | |
|             if (err) {
 | |
|                 connection.release();
 | |
|                 return callback(err);
 | |
|             }
 | |
|             let query = 'CREATE TABLE `campaign_tracker__' + id + '` LIKE campaign_tracker';
 | |
|             connection.query(query, err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 return callback(null, true);
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| }
 | |
| 
 | |
| function removeCampaignTables(id, callback) {
 | |
|     let query = 'DROP TABLE IF EXISTS `campaign__' + id + '`';
 | |
|     db.getConnection((err, connection) => {
 | |
|         if (err) {
 | |
|             return callback(err);
 | |
|         }
 | |
|         connection.query(query, err => {
 | |
|             if (err) {
 | |
|                 connection.release();
 | |
|                 return callback(err);
 | |
|             }
 | |
|             let query = 'DROP TABLE IF EXISTS `campaign_tracker__' + id + '`';
 | |
|             connection.query(query, err => {
 | |
|                 connection.release();
 | |
|                 if (err) {
 | |
|                     return callback(err);
 | |
|                 }
 | |
|                 return callback(null, true);
 | |
|             });
 | |
|         });
 | |
|     });
 | |
| }
 |