Better idle handlers, make auth optional

master
Vitaliy Filippov 2016-10-02 21:57:43 +03:00
parent ce7d6ec01a
commit e7bb14af6e
3 changed files with 146 additions and 58 deletions

View File

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

150
Syncer.js
View File

@ -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)
{
// <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)
{
// <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++)

View File

@ -31,7 +31,10 @@ function SyncerWeb(syncer, pg, cfg)
SyncerWeb.prototype.get_auth = function(req, res)
{
return res.type('html').send('<form action="/auth" method="post"><input name="login" /> <input name="password" type="password" /> <input type="submit" /></form>');
return res.type('html').send(
'<form action="/auth" method="post"><input name="login" />'+
' <input name="password" type="password" /> <input type="submit" /></form>'
);
}
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' });