diff --git a/ImapManager.js b/ImapManager.js index 98d78cb..e2297ee 100644 --- a/ImapManager.js +++ b/ImapManager.js @@ -1,7 +1,5 @@ const Imap = require('imap'); -module.exports = ImapManager; - class ImapManager { constructor() @@ -29,7 +27,10 @@ class ImapManager if (this.busy[connKey]) { // wait for the queue to finish - await this.queue[connKey](); + await new Promise((r, e) => + { + this.queue[connKey].push(r); + }); } if (stoppingIdle && this.onStopIdle[connKey]) { @@ -46,7 +47,7 @@ class ImapManager return this.connections[connKey]; } - let srv = new Imap(self.accounts[accountId]); + let srv = new Imap(this.accounts[accountId]); // FIXME handle connection errors await new Promise((r, e) => { @@ -56,10 +57,10 @@ class ImapManager await new Promise((r, e) => srv._enqueue('ENABLE QRESYNC', r)); // Monkey-patch node-imap to support VANISHED responses - var oldUT = srv._parser._resUntagged; - srv._parser._resUntagged = () => + let oldUT = srv._parser._resUntagged; + srv._parser._resUntagged = function() { - var m; + let m; if (m = /^\* VANISHED( \(EARLIER\))? ([\d:,]+)/.exec(this._buffer)) { srv.emit('vanish', m[2].split(/,/).map(s => s.split(':'))); @@ -258,3 +259,5 @@ class ImapManager return [ msgrow, attrs ]; } } + +module.exports = ImapManager; diff --git a/Syncer.js b/Syncer.js index c91b9a9..50a5306 100644 --- a/Syncer.js +++ b/Syncer.js @@ -1,11 +1,11 @@ const Imap = require('imap'); -const ImapManager = require('./ImapManager.js'); const EventEmitter = require('events').EventEmitter; const MailParser = require('mailparser').MailParser; const iconv = require('iconv-lite'); const mimelib = require('mimelib'); -module.exports = Syncer; +const ImapManager = require('./ImapManager.js'); +const SQL = require('./select-builder-pgsql.js'); class Syncer { @@ -45,7 +45,7 @@ class Syncer if (row) { await SQL.update(this.pg, 'accounts', { - settings: { imap: account.imap, folders: account.folders } + settings: JSON.stringify({ imap: account.imap, folders: account.folders }) }, { id: row.id }); } else @@ -53,11 +53,11 @@ class Syncer row = (await SQL.insert('accounts', { name: account.name, email: account.email, - settings: { + settings: JSON.stringify({ imap: account.imap, folders: account.folders - } - }, '*'))[0]; + }) + }, { returning: '*' }))[0]; } return row.id; } @@ -91,7 +91,7 @@ class Syncer let srv = await this.getSyncConnection(accountId); await this.syncBox(srv, accountId, 'INBOX'); this.releaseSyncConnection(accountId); - })().catch(console.error); + })().catch(e => console.error(e.stack)); } idleVanish(accountId, uids) @@ -103,7 +103,7 @@ class Syncer this.pg, 'folders', 'id', { name: 'INBOX', account_id: accountId }, null, SQL.MS_VALUE ); await this.deleteVanished(boxId, uids); - })().catch(console.error); + })().catch(e => console.error(e.stack)); } idleExpunge(accountId, seqno) @@ -114,7 +114,7 @@ class Syncer let srv = await this.getSyncConnection(accountId); await this.syncBox(srv, accountId, 'INBOX'); this.releaseSyncConnection(accountId); - })().catch(console.error); + })().catch(e => console.error(e.stack)); } runIdle(accountId, srv) @@ -151,15 +151,15 @@ class Syncer async syncAccount(account) { let accountId = await SQL.select(this.pg, 'accounts', 'id', { email: account.email }, null, SQL.MS_VALUE); - if (accountId) + if (!accountId) { let row = (await SQL.insert(this.pg, 'accounts', { name: account.name, email: account.email, - settings: { + settings: JSON.stringify({ imap: account.imap - } - }, 'id'))[0]; + }) + }, { returning: 'id' }))[0]; accountId = row.id; } let srv = await this.getSyncConnection(accountId); @@ -174,7 +174,7 @@ class Syncer async syncBox(srv, accountId, boxName, boxKind, doFull) { - let boxStatus = await new Promise((r, e) => srv.openBox(boxName, true, r)); + let boxStatus = await new Promise((r, e) => srv.openBox(boxName, true, (err, info) => err ? e(err) : r(info))); // IMAP sync: http://tools.ietf.org/html/rfc4549 let boxRow = await SQL.select(this.pg, 'folders', '*', { account_id: accountId, name: boxStatus.name }, null, SQL.MS_ROW); @@ -194,7 +194,7 @@ class Syncer account_id: accountId, highestmodseq: 0, kind: boxKind||'' - }, '*'))[0]; + }, { returning: '*' }))[0]; } // fetch new messages @@ -220,7 +220,7 @@ class Syncer size: true, bodies: 'HEADER', struct: true, - }, (messages, state) => this.saveMessages(messages, boxRow.id, state)); + }, async (messages, state) => await this.saveMessages(messages, boxRow.id, state)); await SQL.update(this.pg, 'folders', { uidvalidity: boxStatus.uidvalidity, @@ -238,7 +238,7 @@ class Syncer process.stderr.write('\rsynchronizing 0'); await this.imap.runFetch( srv, '1:'+maxUid, {}, - (messages, state) => this.queueFlags(messages, boxId, state), + async (messages, state) => this.queueFlags(messages, boxId, state), { flags: flags, updateFlags: updateFlags, missing: missing||[], total: total, nopause: true } ); process.stderr.write('\n'); @@ -283,8 +283,8 @@ class Syncer { let updated = await SQL.update( this.pg, { m: 'messages', t: SQL.values(updateFlags) }, - { 'flags = t.flags::varchar(255)[]' }, - { 'm.folder_id': boxId, 'm.uid=t.uid': [] }, + [ 'flags = t.flags::varchar(255)[]' ], + { 'm.folder_id': boxId, 'm.uid = t.uid::int': [] }, checkMissing ? { returning: 'm.uid' } : null ); if (checkMissing) @@ -312,7 +312,7 @@ class Syncer srv.on('vanish', onVanish); await this.imap.runFetch( srv, '1:'+maxUid, { modifiers: { changedsince: changedSince+' VANISHED' } }, - (messages, state) => this.queueQuickFlags(messages, boxId, state), { updateFlags: updateFlags } + async (messages, state) => this.queueQuickFlags(messages, boxId, state), { updateFlags: updateFlags } ); srv.removeListener('vanish', onVanish); let checkedMissing = await this.updateFlags(boxId, updateFlags, missing && true); @@ -333,16 +333,22 @@ class Syncer for (let i = 0; i < vanished.length; i++) { if (vanished[i][1]) - dia.push('uid >= '+vanished[i][0]+' AND uid <= '+vanished[i][1]); + { + if (Number(vanished[i][1]) > Number(vanished[i][0]) + 1) + dia.push('uid >= '+vanished[i][0]+' AND uid <= '+vanished[i][1]); + else + lst.push(vanished[i][0], vanished[i][1]); + } else lst.push(vanished[i][0]); } if (lst.length) + { dia.push('uid IN ('+lst.join(',')+')'); - await this.deleteMessages({ folder_id: boxId, '('+dia.join(' OR ')+')': [] }); + } + await this.deleteMessages({ folder_id: boxId, ['('+dia.join(' OR ')+')']: [] }); } - // FIXME: async queueQuickFlags(messages, boxId, fetchState) { for (let i = 0; i < messages.length; i++) @@ -354,16 +360,15 @@ class Syncer async deleteMessages(where) { + let q = SQL.select_builder('messages', 'id', where); await SQL.update( this.pg, 'threads', { first_msg: null }, - { 'first_msg IN ('+SQL.select_builder('messages', 'id', where)+')': [] }, + { ['first_msg IN ('+q.sql+')']: q.bind }, ); await SQL.delete(this.pg, 'messages', where); await SQL.update( this.pg, 'threads', - { ['first_msg=('+SQL.select_builder( - 'messages', 'id', { 'thread_id=threads.id': [] }, { order_by: 'time', limit: 1 } - )+')']: [] }, + [ 'first_msg=(select id from messages where thread_id=threads.id order by time limit 1)' ], { first_msg: null } ); await SQL.delete(this.pg, 'threads', { first_msg: null }); @@ -523,3 +528,5 @@ function toPgArray(a) a = JSON.stringify(a); return '{'+a.substring(1, a.length-1)+'}'; } + +module.exports = Syncer; diff --git a/SyncerWeb.js b/SyncerWeb.js index b9eb058..515972c 100644 --- a/SyncerWeb.js +++ b/SyncerWeb.js @@ -14,8 +14,6 @@ const SQL = require('./select-builder-pgsql.js'); const MAX_FETCH = 100; -module.exports = SyncerWeb; - class SyncerWeb { constructor(syncer, pg, cfg) @@ -32,14 +30,14 @@ class SyncerWeb resave: false, saveUninitialized: false })); - this.app.get('/auth', this.get_auth); - this.app.post('/auth', this.post_auth); - this.app.get('/folders', wrapAsync(this.get_folders)); - this.app.get('/groups', wrapAsync(this.get_groups)); - this.app.get('/messages', wrapAsync(this.get_messages)); - this.app.get('/message', wrapAsync(this.get_message)); - this.app.post('/sync', wrapAsync(this.post_sync)); - this.syncer.events.on('sync', this.syncer_sync); + this.app.get('/auth', (req, res) => this.get_auth(req, res)); + this.app.post('/auth', (req, res) => this.post_auth(req, res)); + this.app.get('/folders', wrapAsync(this, 'get_folders')); + this.app.get('/groups', wrapAsync(this, 'get_groups')); + this.app.get('/messages', wrapAsync(this, 'get_messages')); + this.app.get('/message', wrapAsync(this, 'get_message')); + this.app.post('/sync', wrapAsync(this, 'post_sync')); + this.syncer.events.on('sync', (params) => this.syncer_sync(params)); } listen(port) @@ -47,7 +45,7 @@ class SyncerWeb this.http.listen(port); } - get_auth = (req, res) => + get_auth(req, res) { return res.type('html').send( '