2017-03-06 05:45:50 +08:00
|
|
|
'use strict';
|
|
|
|
|
2017-07-16 19:37:33 +08:00
|
|
|
const config = require('wild-config');
|
2017-03-30 01:06:09 +08:00
|
|
|
const tools = require('./tools');
|
2017-07-18 16:17:36 +08:00
|
|
|
const consts = require('./consts');
|
2017-03-06 05:45:50 +08:00
|
|
|
const crypto = require('crypto');
|
|
|
|
const EventEmitter = require('events').EventEmitter;
|
2017-03-10 02:05:29 +08:00
|
|
|
const redis = require('redis');
|
2017-03-30 16:44:18 +08:00
|
|
|
const log = require('npmlog');
|
2017-07-18 16:17:36 +08:00
|
|
|
const counters = require('./counters');
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
class ImapNotifier extends EventEmitter {
|
|
|
|
constructor(options) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
this.database = options.database;
|
2017-07-17 21:32:31 +08:00
|
|
|
this.publisher = options.redis || redis.createClient(tools.redisConfig(config.redis));
|
2017-07-18 16:17:36 +08:00
|
|
|
this.cachedcounter = counters(this.publisher).cachedcounter;
|
2017-03-30 01:06:09 +08:00
|
|
|
|
2017-03-06 05:45:50 +08:00
|
|
|
this.logger = options.logger || {
|
2017-03-30 16:44:18 +08:00
|
|
|
info: log.silly.bind(log, 'IMAP'),
|
|
|
|
debug: log.silly.bind(log, 'IMAP'),
|
|
|
|
error: log.error.bind(log, 'IMAP')
|
2017-03-06 05:45:50 +08:00
|
|
|
};
|
|
|
|
|
2017-03-21 06:07:23 +08:00
|
|
|
if (options.pushOnly) {
|
|
|
|
// do not need to set up the following if we do not care about updates
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-07-17 21:32:31 +08:00
|
|
|
// Subscriber needs its own client connection. This is relevant only in the context of IMAP
|
2017-03-30 01:06:09 +08:00
|
|
|
this.subsriber = redis.createClient(tools.redisConfig(config.redis));
|
2017-03-06 05:45:50 +08:00
|
|
|
this._listeners = new EventEmitter();
|
|
|
|
this._listeners.setMaxListeners(0);
|
|
|
|
|
2017-03-19 21:57:53 +08:00
|
|
|
let publishTimers = new Map();
|
|
|
|
let scheduleDataEvent = ev => {
|
|
|
|
let data;
|
|
|
|
|
|
|
|
let fire = () => {
|
|
|
|
clearTimeout(data.timeout);
|
|
|
|
publishTimers.delete(ev);
|
|
|
|
this._listeners.emit(ev);
|
2017-07-18 22:38:05 +08:00
|
|
|
this._listeners.emit(ev.split(':').shift() + ':*');
|
2017-03-19 21:57:53 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
if (publishTimers.has(ev)) {
|
|
|
|
data = publishTimers.get(ev) || {};
|
|
|
|
clearTimeout(data.timeout);
|
|
|
|
data.count++;
|
|
|
|
|
|
|
|
if (data.initial < Date.now() - 1000) {
|
2017-07-18 22:38:05 +08:00
|
|
|
// if the event has been held back already for a second, then fire immediatelly
|
2017-03-19 21:57:53 +08:00
|
|
|
return fire();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// initialize new event object
|
|
|
|
data = {
|
|
|
|
ev,
|
|
|
|
count: 1,
|
|
|
|
initial: Date.now(),
|
|
|
|
timeout: null
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
data.timeout = setTimeout(fire, 100);
|
|
|
|
data.timeout.unref();
|
|
|
|
|
|
|
|
if (!publishTimers.has(ev)) {
|
|
|
|
publishTimers.set(ev, data);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2017-03-10 02:05:29 +08:00
|
|
|
this.subsriber.on('message', (channel, message) => {
|
|
|
|
if (channel === 'wd_events') {
|
|
|
|
try {
|
|
|
|
let data = JSON.parse(message);
|
2017-03-19 21:57:53 +08:00
|
|
|
if (data.e && !data.p) {
|
|
|
|
scheduleDataEvent(data.e);
|
2017-07-18 22:38:05 +08:00
|
|
|
} else if (data.e) {
|
2017-03-19 21:57:53 +08:00
|
|
|
this._listeners.emit(data.e, data.p);
|
2017-07-18 22:38:05 +08:00
|
|
|
this._listeners.emit(data.e.split(':').shift() + ':*', data.p);
|
2017-03-19 21:57:53 +08:00
|
|
|
}
|
2017-03-10 02:05:29 +08:00
|
|
|
} catch (E) {
|
|
|
|
//
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.subsriber.subscribe('wd_events');
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-03-22 16:30:10 +08:00
|
|
|
* Generates hashed event names for mailbox:user pairs
|
2017-03-06 05:45:50 +08:00
|
|
|
*
|
|
|
|
* @param {String} path
|
2017-03-22 16:30:10 +08:00
|
|
|
* @param {String} user
|
2017-03-06 05:45:50 +08:00
|
|
|
* @returns {String} md5 hex
|
|
|
|
*/
|
2017-07-18 22:38:05 +08:00
|
|
|
_eventName(user, path) {
|
|
|
|
if (path.length >= 32) {
|
|
|
|
path = crypto.createHash('md5').update(path).digest('hex');
|
|
|
|
}
|
|
|
|
return user + ':' + path;
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-03-22 16:30:10 +08:00
|
|
|
* Registers an event handler for path:userid events
|
2017-03-06 05:45:50 +08:00
|
|
|
*
|
2017-03-22 16:30:10 +08:00
|
|
|
* @param {String} user
|
2017-03-06 05:45:50 +08:00
|
|
|
* @param {String} path
|
|
|
|
* @param {Function} handler Function to run once there are new entries in the journal
|
|
|
|
*/
|
|
|
|
addListener(session, path, handler) {
|
2017-03-22 16:30:10 +08:00
|
|
|
let eventName = this._eventName(session.user.id.toString(), path);
|
2017-03-06 05:45:50 +08:00
|
|
|
this._listeners.addListener(eventName, handler);
|
|
|
|
|
2017-05-16 19:05:14 +08:00
|
|
|
this.logger.debug('[%s] New journal listener for %s ("%s:%s")', session.id, eventName, session.user.username, path);
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-03-22 16:30:10 +08:00
|
|
|
* Unregisters an event handler for path:user events
|
2017-03-06 05:45:50 +08:00
|
|
|
*
|
2017-03-22 16:30:10 +08:00
|
|
|
* @param {String} user
|
2017-03-06 05:45:50 +08:00
|
|
|
* @param {String} path
|
|
|
|
* @param {Function} handler Function to run once there are new entries in the journal
|
|
|
|
*/
|
|
|
|
removeListener(session, path, handler) {
|
2017-03-22 16:30:10 +08:00
|
|
|
let eventName = this._eventName(session.user.id.toString(), path);
|
2017-03-06 05:45:50 +08:00
|
|
|
this._listeners.removeListener(eventName, handler);
|
|
|
|
|
2017-05-16 19:05:14 +08:00
|
|
|
this.logger.debug('[%s] Removed journal listener from %s ("%s:%s")', session.id, eventName, session.user.username, path);
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stores multiple journal entries to db
|
|
|
|
*
|
2017-03-22 16:30:10 +08:00
|
|
|
* @param {String} user
|
2017-03-06 05:45:50 +08:00
|
|
|
* @param {String} path
|
|
|
|
* @param {Array|Object} entries An array of entries to be journaled
|
|
|
|
* @param {Function} callback Runs once the entry is either stored or an error occurred
|
|
|
|
*/
|
2017-03-22 16:30:10 +08:00
|
|
|
addEntries(user, path, entries, callback) {
|
2017-03-06 05:45:50 +08:00
|
|
|
if (entries && !Array.isArray(entries)) {
|
|
|
|
entries = [entries];
|
|
|
|
} else if (!entries || !entries.length) {
|
|
|
|
return callback(null, false);
|
|
|
|
}
|
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
// find list of message ids that need to be updated
|
|
|
|
let updated = entries.filter(entry => !entry.modseq && entry.message).map(entry => entry.message);
|
|
|
|
|
|
|
|
let getMailbox = next => {
|
|
|
|
let mailbox;
|
|
|
|
|
|
|
|
if (user && typeof user === 'object' && user._id) {
|
|
|
|
mailbox = user;
|
|
|
|
user = false;
|
2017-03-19 21:57:53 +08:00
|
|
|
}
|
2017-03-12 00:18:32 +08:00
|
|
|
|
2017-06-03 14:51:58 +08:00
|
|
|
let mailboxQuery = mailbox
|
|
|
|
? {
|
|
|
|
_id: mailbox._id
|
|
|
|
}
|
|
|
|
: {
|
|
|
|
user,
|
|
|
|
path
|
|
|
|
};
|
2017-03-19 21:57:53 +08:00
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
if (updated.length) {
|
|
|
|
// provision new modseq value
|
2017-03-19 21:57:53 +08:00
|
|
|
return this.database.collection('mailboxes').findOneAndUpdate(mailboxQuery, {
|
|
|
|
$inc: {
|
2017-04-13 02:59:30 +08:00
|
|
|
modifyIndex: 1
|
2017-03-19 21:57:53 +08:00
|
|
|
}
|
2017-04-13 02:59:30 +08:00
|
|
|
}, {
|
|
|
|
returnOriginal: false
|
|
|
|
}, (err, item) => {
|
2017-03-19 21:57:53 +08:00
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
|
|
|
|
next(null, item && item.value);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
if (mailbox) {
|
|
|
|
return next(null, mailbox);
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
2017-03-19 21:57:53 +08:00
|
|
|
this.database.collection('mailboxes').findOne(mailboxQuery, next);
|
|
|
|
};
|
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
// final action to push entries to journal
|
|
|
|
let pushToJournal = () => {
|
|
|
|
this.database.collection('journal').insertMany(entries, {
|
|
|
|
w: 1,
|
|
|
|
ordered: false
|
|
|
|
}, (err, r) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2017-07-18 16:17:36 +08:00
|
|
|
|
|
|
|
setImmediate(() => this.updateCounters(entries));
|
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
return callback(null, r.insertedCount);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2017-03-19 21:57:53 +08:00
|
|
|
getMailbox((err, mailbox) => {
|
2017-03-06 05:45:50 +08:00
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2017-03-19 21:57:53 +08:00
|
|
|
if (!mailbox) {
|
2017-03-06 05:45:50 +08:00
|
|
|
return callback(null, new Error('Selected mailbox does not exist'));
|
|
|
|
}
|
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
let modseq = mailbox.modifyIndex;
|
|
|
|
let created = new Date();
|
|
|
|
entries.forEach(entry => {
|
|
|
|
entry.modseq = entry.modseq || modseq;
|
|
|
|
entry.created = entry.created || created;
|
2017-04-13 03:51:13 +08:00
|
|
|
entry.mailbox = entry.mailbox || mailbox._id;
|
2017-07-18 22:38:05 +08:00
|
|
|
entry.user = mailbox.user;
|
2017-04-13 02:59:30 +08:00
|
|
|
});
|
2017-03-06 05:45:50 +08:00
|
|
|
|
2017-04-13 02:59:30 +08:00
|
|
|
if (updated.length) {
|
|
|
|
this.database.collection('messages').updateMany({
|
|
|
|
_id: {
|
|
|
|
$in: updated
|
2017-07-13 22:04:41 +08:00
|
|
|
},
|
2017-07-16 00:08:33 +08:00
|
|
|
mailbox: mailbox._id
|
2017-04-13 02:59:30 +08:00
|
|
|
}, {
|
|
|
|
// only update modseq if the new value is larger than old one
|
|
|
|
$max: {
|
|
|
|
modseq
|
|
|
|
}
|
|
|
|
}, err => {
|
|
|
|
if (err) {
|
|
|
|
this.logger.error('Error updating modseq for messages. %s', err.message);
|
|
|
|
}
|
|
|
|
pushToJournal();
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
pushToJournal();
|
|
|
|
}
|
2017-03-06 05:45:50 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends a notification that there are new updates in the selected mailbox
|
|
|
|
*
|
2017-03-22 16:30:10 +08:00
|
|
|
* @param {String} user
|
2017-03-06 05:45:50 +08:00
|
|
|
* @param {String} path
|
|
|
|
*/
|
2017-03-22 16:30:10 +08:00
|
|
|
fire(user, path, payload) {
|
|
|
|
let eventName = this._eventName(user, path);
|
2017-03-06 05:45:50 +08:00
|
|
|
setImmediate(() => {
|
2017-03-10 02:05:29 +08:00
|
|
|
let data = JSON.stringify({
|
|
|
|
e: eventName,
|
|
|
|
p: payload
|
|
|
|
});
|
|
|
|
this.publisher.publish('wd_events', data);
|
2017-03-06 05:45:50 +08:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns all entries from the journal that have higher than provided modification index
|
|
|
|
*
|
|
|
|
* @param {String} session
|
|
|
|
* @param {String} path
|
|
|
|
* @param {Number} modifyIndex Last known modification id
|
|
|
|
* @param {Function} callback Returns update entries as an array
|
|
|
|
*/
|
|
|
|
getUpdates(session, path, modifyIndex, callback) {
|
|
|
|
modifyIndex = Number(modifyIndex) || 0;
|
2017-03-22 16:30:10 +08:00
|
|
|
let user = session.user.id;
|
2017-03-06 05:45:50 +08:00
|
|
|
|
|
|
|
this.database.collection('mailboxes').findOne({
|
2017-03-22 16:30:10 +08:00
|
|
|
user,
|
2017-03-06 05:45:50 +08:00
|
|
|
path
|
|
|
|
}, (err, mailbox) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
if (!mailbox) {
|
|
|
|
return callback(null, 'NONEXISTENT');
|
|
|
|
}
|
2017-06-03 14:51:58 +08:00
|
|
|
this.database
|
|
|
|
.collection('journal')
|
|
|
|
.find({
|
|
|
|
mailbox: mailbox._id,
|
|
|
|
modseq: {
|
|
|
|
$gt: modifyIndex
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.sort([['modseq', 1]])
|
|
|
|
.toArray(callback);
|
2017-03-06 05:45:50 +08:00
|
|
|
});
|
|
|
|
}
|
2017-07-18 16:17:36 +08:00
|
|
|
|
|
|
|
updateCounters(entries) {
|
|
|
|
if (!entries) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let counters = new Map();
|
|
|
|
(Array.isArray(entries) ? entries : [].concat(entries || [])).forEach(entry => {
|
|
|
|
let m = entry.mailbox.toString();
|
|
|
|
if (!counters.has(m)) {
|
|
|
|
counters.set(m, 0);
|
|
|
|
}
|
|
|
|
switch (entry && entry.command) {
|
|
|
|
case 'EXISTS':
|
|
|
|
counters.set(m, counters.get(m) + 1);
|
|
|
|
break;
|
|
|
|
case 'EXPUNGE':
|
|
|
|
counters.set(m, counters.get(m) - 1);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
let pos = 0;
|
|
|
|
let rows = Array.from(counters);
|
|
|
|
let updateCounter = () => {
|
|
|
|
if (pos >= rows.length) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let row = rows[pos++];
|
|
|
|
if (!row || !row.length) {
|
|
|
|
return updateCounter();
|
|
|
|
}
|
|
|
|
let mailbox = row[0];
|
|
|
|
let delta = row[1];
|
|
|
|
|
|
|
|
this.cachedcounter('sum:' + mailbox, delta, consts.MAILBOX_COUNTER_TTL, updateCounter);
|
|
|
|
};
|
|
|
|
|
|
|
|
updateCounter();
|
|
|
|
}
|
2017-03-06 05:45:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = ImapNotifier;
|