From e7bb14af6e34657f3cf82182c087e852ab0a03c0 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 2 Oct 2016 21:57:43 +0300 Subject: [PATCH] Better idle handlers, make auth optional --- ImapManager.js | 25 ++++++++- Syncer.js | 150 +++++++++++++++++++++++++++++++++---------------- SyncerWeb.js | 29 +++++++--- 3 files changed, 146 insertions(+), 58 deletions(-) diff --git a/ImapManager.js b/ImapManager.js index b565e36..e3fed69 100644 --- a/ImapManager.js +++ b/ImapManager.js @@ -11,6 +11,8 @@ function ImapManager() this.busy = {}; this.selected = {}; this.queue = {}; + this.onIdle = {}; + this.onStopIdle = {}; } ImapManager.prototype.setServer = function(accountId, settings) @@ -18,17 +20,27 @@ ImapManager.prototype.setServer = function(accountId, settings) this.accounts[accountId] = settings; } -ImapManager.prototype.getConnection = function*(accountId, boxName, connKey) +ImapManager.prototype.getConnection = function*(accountId, boxName, connKey, onIdle, onStopIdle) { var self = this; connKey = accountId+(connKey||''); if (self.connections[connKey]) { + let stoppingIdle = self.queue[connKey].length == 0; if (self.busy[connKey]) + { + // wait for the queue to finish yield self.queue[connKey].push(gen.cb()); + } + if (stoppingIdle && self.onStopIdle[connKey]) + { + // run "stop idle" callback + self.onStopIdle[connKey](accountId, self.connections[connKey]); + } if (boxName && self.selected[connKey] != boxName) { - yield srv.openBox(boxName, true, gen.ef()); + // select different box + yield self.connections[connKey].openBox(boxName, true, gen.ef()); self.selected[connKey] = boxName; } self.busy[connKey] = true; @@ -70,6 +82,8 @@ ImapManager.prototype.getConnection = function*(accountId, boxName, connKey) self.connections[connKey] = srv; self.busy[connKey] = true; self.queue[connKey] = []; + self.onIdle[connKey] = onIdle; + self.onStopIdle[connKey] = onStopIdle; return srv; } @@ -79,7 +93,9 @@ ImapManager.prototype.releaseConnection = function(accountId, connKey, allowClos connKey = accountId+(connKey||''); self.busy[connKey] = false; if (self.queue[connKey].length) + { (self.queue[connKey].shift())(); + } else if (allowClose) { self.connections[connKey].end(); @@ -88,6 +104,11 @@ ImapManager.prototype.releaseConnection = function(accountId, connKey, allowClos delete self.queue[connKey]; delete self.selected[connKey]; } + else + { + if (self.onIdle[connKey]) + self.onIdle[connKey](accountId, self.connections[connKey]); + } } ImapManager.prototype.runFetch = function*(srv, what, params, processor, args) diff --git a/Syncer.js b/Syncer.js index 4e0760e..5a9e8fa 100644 --- a/Syncer.js +++ b/Syncer.js @@ -9,6 +9,8 @@ function Syncer(pg) this.syncInProgress = false; this.pg = pg; this.imap = new ImapManager(); + this.runIdle = this.runIdle.bind(this); + this.stopIdle = this.stopIdle.bind(this); } Syncer.prototype.init = function*(cfg) @@ -33,6 +35,7 @@ Syncer.prototype.addAccount = function*(account) if (row.length) { row = row[0]; + yield this.pg.update('accounts', { settings: { imap: account.imap, folders: account.folders } }).where({ id: row.id }).run(gen.ef()); } else { @@ -58,6 +61,86 @@ Syncer.prototype.loadAccounts = function*() } } +Syncer.prototype.getSyncConnection = function*(accountId, boxName) +{ + var srv = yield* this.imap.getConnection(accountId, null, 'S', this.runIdle, this.stopIdle); + return srv; +} + +Syncer.prototype.idleUidvalidity = function(accountId, uidvalidity) +{ + // uidvalidity changes (FUUUU) remove everything and resync + +} + +Syncer.prototype.idleMail = function(accountId, count) +{ + // new messages arrived while idling, fetch them + var self = this; + gen.run(function*() + { + var srv = yield* self.getSyncConnection(accountId); + yield* self.syncBox(srv, accountId, 'INBOX'); + self.releaseSyncConnection(accountId); + }); +} + +Syncer.prototype.idleVanish = function(accountId, uids) +{ + // messages expunged by uids + var self = this; + gen.run(function*() + { + let [ boxId ] = yield* self.pg.select('id').from('folders') + .where({ name: 'INBOX', account_id: accountId }).val(gen.ef()); + yield* self.deleteVanished(boxId, uids); + }); +} + +Syncer.prototype.idleExpunge = function(accountId, seqno) +{ + // message expunged by (FUUUU) sequence number(s?) + var self = this; + gen.run(function*() + { + var srv = yield* self.getSyncConnection(accountId); + yield* self.syncBox(srv, accountId, 'INBOX'); + self.releaseSyncConnection(accountId); + }); +} + +Syncer.prototype.runIdle = function(accountId, srv) +{ + var self = this; + if (!srv._idleCallbacks) + { + srv._idleCallbacks = { + uidvalidity: this.idleUidvalidity.bind(this, accountId), + mail: this.idleMail.bind(this, accountId), + vanish: this.idleVanish.bind(this, accountId), + expunge: this.idleExpunge.bind(this, accountId) + } + } + for (var i in srv._idleCallbacks) + { + srv.on(i, srv._idleCallbacks[i]); + } + srv.openBox('INBOX', true); +} + +Syncer.prototype.stopIdle = function(accountId, srv) +{ + for (var i in srv._idleCallbacks) + { + srv.removeListener(i, srv._idleCallbacks[i]); + } +} + +Syncer.prototype.releaseSyncConnection = function*(accountId, boxName) +{ + this.imap.releaseConnection(accountId, 'S'); +} + Syncer.prototype.syncAccount = function*(account) { var self = this; @@ -76,15 +159,14 @@ Syncer.prototype.syncAccount = function*(account) }).returning('id').row(gen.ef()); accountId = row.id; } - var srv = yield* self.imap.getConnection(accountId, null, 'S'); + var srv = yield* self.getSyncConnection(accountId); var [ boxes ] = yield srv.getBoxes(gen.ef()); for (var k in boxes) { var boxKind = (boxes[k].special_use_attrib || '').replace('\\', '').toLowerCase(); yield* self.syncBox(srv, accountId, k, boxKind, true); } - yield* self.runIdle(accountId, srv); - self.imap.releaseConnection(accountId, 'S'); + self.releaseSyncConnection(accountId); } Syncer.prototype.syncBox = function*(srv, accountId, boxName, boxKind, doFull) @@ -145,39 +227,6 @@ Syncer.prototype.syncBox = function*(srv, accountId, boxName, boxKind, doFull) }).where({ id: boxRow.id }).run(gen.ef()); } -Syncer.prototype.runIdle = function*(accountId, srv) -{ - var self = this; - yield srv.openBox('INBOX', true, gen.ef()); - srv.on('uidvalidity', function(uidvalidity) - { - // uidvalidity changes (FUUUU) remove everything - - }); - srv.on('mail', function(count) - { - // new messages arrived while idling, fetch them - gen.run(function*() - { - var srv = yield* self.imap.getConnection(accountId, null, 'S'); - yield* self.syncBox(srv, accountId, 'INBOX'); - self.imap.releaseConnection(accountId, 'S'); - }); - }); - srv.on('vanish', function(uids) - { - // messages expunged by uids - console.log([ 'VANISH', uids ]); - - }); - srv.on('expunge', function(seqno) - { - // message expunged by (FUUUU) sequence number - console.log(arguments); - - }); -} - Syncer.prototype.fullResync = function*(srv, boxId, maxUid, missing) { var [ flags ] = yield this.pg.select('uid, flags').from('messages').where({ folder_id: boxId }).rows(gen.ef()); @@ -254,7 +303,7 @@ Syncer.prototype.quickResync = function*(srv, boxId, maxUid, changedSince, missi srv.on('vanish', onVanish); yield* this.imap.runFetch( srv, '1:'+maxUid, { modifiers: { changedsince: changedSince+' VANISHED' } }, - (messages, state) => queueQuickFlags(messages, boxId, state), { updateFlags: updateFlags } + (messages, state) => this.queueQuickFlags(messages, boxId, state), { updateFlags: updateFlags } ); srv.removeListener('vanish', onVanish); var checkedMissing = yield* this.updateFlags(boxId, updateFlags, missing && true); @@ -263,20 +312,25 @@ Syncer.prototype.quickResync = function*(srv, boxId, maxUid, changedSince, missi if (vanished.length) { - let lst = [], dia = []; - for (let i = 0; i < vanished.length; i++) - { - if (vanished[i][1]) - dia.push('uid >= '+vanished[i][0]+' AND uid <= '+vanished[i][1]); - else - lst.push(vanished[i][0]); - } - if (lst.length) - dia.push('uid IN ('+lst.join(',')+')'); - yield* this.deleteMessages(this.pg.sql.and({ folder_id: boxId }, this.pg.sql('('+dia.join(' OR ')+')'))); + yield* this.deleteVanished(boxId, vanished); } } +Syncer.prototype.deleteVanished = function*(boxId, vanished) +{ + let lst = [], dia = []; + for (let i = 0; i < vanished.length; i++) + { + if (vanished[i][1]) + dia.push('uid >= '+vanished[i][0]+' AND uid <= '+vanished[i][1]); + else + lst.push(vanished[i][0]); + } + if (lst.length) + dia.push('uid IN ('+lst.join(',')+')'); + yield* this.deleteMessages(this.pg.sql.and({ folder_id: boxId }, this.pg.sql('('+dia.join(' OR ')+')'))); +} + Syncer.prototype.queueQuickFlags = function*(messages, boxId, fetchState) { for (var i = 0; i < messages.length; i++) diff --git a/SyncerWeb.js b/SyncerWeb.js index 4efdef2..a28ab21 100644 --- a/SyncerWeb.js +++ b/SyncerWeb.js @@ -31,7 +31,10 @@ function SyncerWeb(syncer, pg, cfg) SyncerWeb.prototype.get_auth = function(req, res) { - return res.type('html').send('
'); + return res.type('html').send( + '
'+ + '
' + ); } SyncerWeb.prototype.post_auth = function(req, res) @@ -48,9 +51,14 @@ SyncerWeb.prototype.post_auth = function(req, res) SyncerWeb.prototype.get_folders = function*(req, res) { - if (!req.session || !req.session.auth) + if (this.cfg.login && (!req.session || !req.session.auth)) + { return res.sendStatus(401); - var [ accounts ] = yield this.pg.select('id, name, email').from('accounts').rows(gen.ef()); + } + var [ accounts ] = yield this.pg.select( + 'id, name, email, settings->\'folders\' folderMap,'+ + ' (select count(*) from messages m, folders f where m.folder_id=f.id and f.account_id=a.id and (flags @> array[\'pinned\',\'unread\']::varchar(255)[])) pinned_unread_count' + ).from('accounts a').rows(gen.ef()); var [ folders ] = yield this.pg.select( 'id, account_id, name,'+ ' (select count(*) from messages m where m.folder_id=f.id) total_count,'+ @@ -71,7 +79,7 @@ SyncerWeb.prototype.get_folders = function*(req, res) SyncerWeb.prototype.get_messages = function*(req, res) { - if (!req.session || !req.session.auth) + if (this.cfg.login && (!req.session || !req.session.auth)) return res.sendStatus(401); var folderId = req.query.folderId; if (!folderId) @@ -80,19 +88,24 @@ SyncerWeb.prototype.get_messages = function*(req, res) var offset = req.query.offset || 0; var [ msgs ] = yield this.pg.select('*').from('messages').where({ folder_id: folderId }) .orderBy('time desc').limit(limit).offset(offset).rows(gen.ef()); + for (var i = 0; i < msgs.length; i++) + { + delete msgs[i].text_index; + } return res.send({ messages: msgs }); } SyncerWeb.prototype.get_message = function*(req, res) { - if (!req.session || !req.session.auth) + if (this.cfg.login && (!req.session || !req.session.auth)) return res.sendStatus(401); var msgId = req.query.msgId; var [ msg ] = yield this.pg.select('m.*, f.name folder_name, f.account_id') .from('messages m').join('folders f', this.pg.sql('f.id=m.folder_id')) - .where({ 'm.id': msgId }).row(gen.ef()); - if (!msg) + .where({ 'm.id': msgId }).rows(gen.ef()); + if (!msg.length) return res.send({ error: 'not-found' }); + msg = msg[0]; delete msg.text_index; if (!msg.body_html && !msg.body_text) { @@ -109,7 +122,7 @@ SyncerWeb.prototype.get_message = function*(req, res) SyncerWeb.prototype.post_sync = function*(req, res) { - if (!req.session || !req.session.auth) + if (this.cfg.login && (!req.session || !req.session.auth)) return res.sendStatus(401); if (self.syncer.syncInProgress) return res.send({ error: 'already-running' });