2019-05-18 03:01:13 +03:00
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const crypto = require('crypto');
|
|
|
|
|
|
2016-09-11 22:04:37 +03:00
|
|
|
|
const Imap = require('imap');
|
2016-10-03 16:55:09 +03:00
|
|
|
|
const EventEmitter = require('events').EventEmitter;
|
2016-10-05 13:50:33 +03:00
|
|
|
|
const iconv = require('iconv-lite');
|
2019-05-17 16:57:37 +03:00
|
|
|
|
const MailParser = require('mailparser').MailParser;
|
2016-10-09 21:06:57 +03:00
|
|
|
|
const mimelib = require('mimelib');
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-18 03:01:13 +03:00
|
|
|
|
const fsp = require('./fsp.js');
|
2019-05-10 01:26:31 +03:00
|
|
|
|
const ImapManager = require('./ImapManager.js');
|
2019-05-17 16:57:37 +03:00
|
|
|
|
const sanitizeHtml = require('./sanitize.js');
|
2019-05-10 01:26:31 +03:00
|
|
|
|
const SQL = require('./select-builder-pgsql.js');
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
class Syncer
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
constructor(pg)
|
|
|
|
|
{
|
|
|
|
|
this.syncInProgress = false;
|
|
|
|
|
this.pg = pg;
|
|
|
|
|
this.imap = new ImapManager();
|
|
|
|
|
this.runIdle = this.runIdle.bind(this);
|
|
|
|
|
this.stopIdle = this.stopIdle.bind(this);
|
|
|
|
|
this.events = new EventEmitter();
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-17 16:57:37 +03:00
|
|
|
|
// public
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async init(cfg)
|
|
|
|
|
{
|
2019-05-18 03:01:13 +03:00
|
|
|
|
this.files_path = path.resolve(cfg.files_path);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
fs.accessSync(this.files_path, fs.constants.R_OK || fs.constants.W_OK);
|
|
|
|
|
}
|
|
|
|
|
catch (e)
|
|
|
|
|
{
|
|
|
|
|
throw new Error(this.files_path+' is not writable');
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < cfg.accounts.length; i++)
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{
|
|
|
|
|
await this.addAccount(cfg.accounts[i]);
|
|
|
|
|
}
|
|
|
|
|
await this.loadAccounts();
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-17 16:57:37 +03:00
|
|
|
|
// public
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async syncAll()
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
this.syncInProgress = true;
|
|
|
|
|
for (let id in this.accounts)
|
|
|
|
|
{
|
|
|
|
|
await this.syncAccount(this.accounts[id]);
|
|
|
|
|
}
|
|
|
|
|
this.syncInProgress = false;
|
|
|
|
|
this.events.emit('sync', { state: 'complete' });
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
|
|
|
|
|
async addAccount(account)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let row = await SQL.select(this.pg, 'accounts', 'id', { email: account.email }, null, SQL.MS_ROW);
|
|
|
|
|
if (row)
|
|
|
|
|
{
|
|
|
|
|
await SQL.update(this.pg, 'accounts', {
|
2019-05-10 01:26:31 +03:00
|
|
|
|
settings: JSON.stringify({ imap: account.imap, folders: account.folders })
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}, { id: row.id });
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
row = (await SQL.insert('accounts', {
|
|
|
|
|
name: account.name,
|
|
|
|
|
email: account.email,
|
2019-05-10 01:26:31 +03:00
|
|
|
|
settings: JSON.stringify({
|
2019-05-08 16:39:14 +03:00
|
|
|
|
imap: account.imap,
|
|
|
|
|
folders: account.folders
|
2019-05-10 01:26:31 +03:00
|
|
|
|
})
|
|
|
|
|
}, { returning: '*' }))[0];
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
|
|
|
|
return row.id;
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async loadAccounts()
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let rows = await SQL.select(this.pg, 'accounts', '*', []);
|
|
|
|
|
this.accounts = {};
|
|
|
|
|
for (let i = 0; i < rows.length; i++)
|
|
|
|
|
{
|
|
|
|
|
this.accounts[rows[i].id] = rows[i];
|
|
|
|
|
this.imap.setServer(rows[i].id, rows[i].settings.imap);
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async getSyncConnection(accountId, boxName)
|
|
|
|
|
{
|
|
|
|
|
return await this.imap.getConnection(accountId, null, 'S', this.runIdle, this.stopIdle);
|
|
|
|
|
}
|
2016-10-02 21:57:43 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
idleUidvalidity(accountId, uidvalidity)
|
|
|
|
|
{
|
|
|
|
|
// FIXME uidvalidity changes (FUUUU) remove everything and resync
|
|
|
|
|
}
|
2016-10-02 21:57:43 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
idleMail(accountId, count)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
// <count> new messages arrived while idling, fetch them
|
|
|
|
|
(async () =>
|
|
|
|
|
{
|
|
|
|
|
let srv = await this.getSyncConnection(accountId);
|
|
|
|
|
await this.syncBox(srv, accountId, 'INBOX');
|
|
|
|
|
this.releaseSyncConnection(accountId);
|
2019-05-10 01:26:31 +03:00
|
|
|
|
})().catch(e => console.error(e.stack));
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2016-10-02 21:57:43 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
idleVanish(accountId, uids)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
// messages expunged by uids
|
|
|
|
|
(async () =>
|
|
|
|
|
{
|
|
|
|
|
let boxId = await SQL.select(
|
|
|
|
|
this.pg, 'folders', 'id', { name: 'INBOX', account_id: accountId }, null, SQL.MS_VALUE
|
|
|
|
|
);
|
|
|
|
|
await this.deleteVanished(boxId, uids);
|
2019-05-10 01:26:31 +03:00
|
|
|
|
})().catch(e => console.error(e.stack));
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2016-10-02 21:57:43 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
idleExpunge(accountId, seqno)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
// message expunged by (FUUUU) sequence number(s?)
|
|
|
|
|
(async () =>
|
|
|
|
|
{
|
|
|
|
|
let srv = await this.getSyncConnection(accountId);
|
|
|
|
|
await this.syncBox(srv, accountId, 'INBOX');
|
|
|
|
|
this.releaseSyncConnection(accountId);
|
2019-05-10 01:26:31 +03:00
|
|
|
|
})().catch(e => console.error(e.stack));
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2016-10-02 21:57:43 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
runIdle(accountId, srv)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
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 (let i in srv._idleCallbacks)
|
|
|
|
|
{
|
|
|
|
|
srv.on(i, srv._idleCallbacks[i]);
|
2016-10-02 21:57:43 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
srv.openBox('INBOX', true, () => {});
|
2016-10-02 21:57:43 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
|
|
|
|
|
stopIdle(accountId, srv)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
for (let i in srv._idleCallbacks)
|
|
|
|
|
{
|
|
|
|
|
srv.removeListener(i, srv._idleCallbacks[i]);
|
|
|
|
|
}
|
2016-10-02 21:57:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
releaseSyncConnection(accountId, boxName)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
this.imap.releaseConnection(accountId, 'S');
|
2016-10-02 21:57:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-17 16:57:37 +03:00
|
|
|
|
// public
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async syncAccount(account)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let accountId = await SQL.select(this.pg, 'accounts', 'id', { email: account.email }, null, SQL.MS_VALUE);
|
2019-05-10 01:26:31 +03:00
|
|
|
|
if (!accountId)
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{
|
|
|
|
|
let row = (await SQL.insert(this.pg, 'accounts', {
|
|
|
|
|
name: account.name,
|
|
|
|
|
email: account.email,
|
2019-05-10 01:26:31 +03:00
|
|
|
|
settings: JSON.stringify({
|
2019-05-08 16:39:14 +03:00
|
|
|
|
imap: account.imap
|
2019-05-10 01:26:31 +03:00
|
|
|
|
})
|
|
|
|
|
}, { returning: 'id' }))[0];
|
2019-05-08 16:39:14 +03:00
|
|
|
|
accountId = row.id;
|
|
|
|
|
}
|
|
|
|
|
let srv = await this.getSyncConnection(accountId);
|
2019-05-13 16:23:53 +03:00
|
|
|
|
let boxes = await new Promise((res, err) => srv.getBoxes((e, r) => e ? err(e) : res(r)));
|
2019-05-08 16:39:14 +03:00
|
|
|
|
for (let k in boxes)
|
|
|
|
|
{
|
|
|
|
|
let boxKind = (boxes[k].special_use_attrib || '').replace('\\', '').toLowerCase();
|
|
|
|
|
await this.syncBox(srv, accountId, k, boxKind, true);
|
|
|
|
|
}
|
|
|
|
|
this.releaseSyncConnection(accountId);
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
|
|
|
|
|
async syncBox(srv, accountId, boxName, boxKind, doFull)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-10 01:26:31 +03:00
|
|
|
|
let boxStatus = await new Promise((r, e) => srv.openBox(boxName, true, (err, info) => err ? e(err) : r(info)));
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
// 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);
|
|
|
|
|
if (boxRow)
|
|
|
|
|
{
|
|
|
|
|
if (boxRow.uidvalidity != boxStatus.uidvalidity)
|
|
|
|
|
{
|
|
|
|
|
await this.deleteMessages({ folder_id: boxRow.id, 'uid is not null': [] });
|
|
|
|
|
boxRow.uidvalidity = boxStatus.uidvalidity;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
boxRow = (await SQL.insert(this.pg, 'folders', {
|
|
|
|
|
name: boxStatus.name,
|
|
|
|
|
uidvalidity: boxStatus.uidvalidity,
|
|
|
|
|
account_id: accountId,
|
|
|
|
|
highestmodseq: 0,
|
|
|
|
|
kind: boxKind||''
|
2019-05-10 01:26:31 +03:00
|
|
|
|
}, { returning: '*' }))[0];
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
// fetch new messages
|
|
|
|
|
let missing = [];
|
|
|
|
|
let maxUid = await SQL.select(this.pg, 'messages', 'MAX(uid)', { folder_id: boxRow.id }, null, SQL.MS_VALUE);
|
|
|
|
|
if (boxRow.highestmodseq)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
this.events.emit('sync', { state: 'start', quick: true, email: this.accounts[accountId].email, folder: boxRow.name });
|
|
|
|
|
process.stderr.write(this.accounts[accountId].email+'/'+boxRow.name+': quick resync\n');
|
|
|
|
|
await this.quickResync(srv, boxRow.id, maxUid, boxRow.highestmodseq, missing);
|
|
|
|
|
boxRow.highestmodseq = boxStatus.highestmodseq;
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
else if (doFull && maxUid)
|
|
|
|
|
{
|
|
|
|
|
// list messages, update flags and version tag
|
|
|
|
|
this.events.emit('sync', { state: 'start', email: this.accounts[accountId].email, folder: boxRow.name });
|
|
|
|
|
process.stderr.write(this.accounts[accountId].email+'/'+boxRow.name+': full resync\n');
|
|
|
|
|
await this.fullResync(srv, boxRow.id, maxUid, missing, boxStatus.messages.total);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
missing.push((maxUid ? maxUid+1 : 1)+':*');
|
|
|
|
|
await this.imap.runFetch(srv, missing, {
|
|
|
|
|
size: true,
|
|
|
|
|
bodies: 'HEADER',
|
|
|
|
|
struct: true,
|
2019-05-14 13:54:56 +03:00
|
|
|
|
}, (messages, state) => this.saveMessages(messages, boxRow.id, state));
|
2019-05-08 16:39:14 +03:00
|
|
|
|
|
|
|
|
|
await SQL.update(this.pg, 'folders', {
|
|
|
|
|
uidvalidity: boxStatus.uidvalidity,
|
|
|
|
|
highestmodseq: boxStatus.highestmodseq||0
|
|
|
|
|
}, { id: boxRow.id });
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
|
|
|
|
|
async fullResync(srv, boxId, maxUid, missing, total)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-13 16:23:53 +03:00
|
|
|
|
let flags = await SQL.select(this.pg, 'messages', 'uid, flags', { folder_id: boxId });
|
2019-05-08 16:39:14 +03:00
|
|
|
|
flags = flags.reduce((o, row) => { o[row.uid] = row.flags; return o; }, {});
|
|
|
|
|
|
|
|
|
|
let updateFlags = [];
|
|
|
|
|
|
|
|
|
|
process.stderr.write('\rsynchronizing 0');
|
|
|
|
|
await this.imap.runFetch(
|
|
|
|
|
srv, '1:'+maxUid, {},
|
2019-05-10 01:26:31 +03:00
|
|
|
|
async (messages, state) => this.queueFlags(messages, boxId, state),
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{ flags: flags, updateFlags: updateFlags, missing: missing||[], total: total, nopause: true }
|
|
|
|
|
);
|
|
|
|
|
process.stderr.write('\n');
|
|
|
|
|
this.events.emit('sync', { state: 'finish-box' });
|
|
|
|
|
|
|
|
|
|
await this.updateFlags(boxId, updateFlags);
|
|
|
|
|
|
|
|
|
|
// delete messages removed from IMAP server
|
|
|
|
|
flags = Object.keys(flags);
|
|
|
|
|
if (flags.length)
|
|
|
|
|
{
|
|
|
|
|
await this.deleteMessages({ folder_id: boxId, uid: flags });
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
queueFlags(messages, boxId, fetchState)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
for (let i = 0; i < messages.length; i++)
|
|
|
|
|
{
|
|
|
|
|
let m = messages[i][0];
|
|
|
|
|
if (!fetchState.flags[m.uid])
|
|
|
|
|
{
|
|
|
|
|
fetchState.missing.push(m.uid);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2019-05-20 19:35:28 +03:00
|
|
|
|
let flags = this.transformFlags(m.flags);
|
|
|
|
|
if (fetchState.flags[m.uid].join(',') != flags.join(','))
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{
|
2019-05-20 02:21:28 +03:00
|
|
|
|
fetchState.updateFlags.push({ uid: m.uid, flags: toPgArray(flags) });
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
|
|
|
|
delete fetchState.flags[m.uid];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
fetchState.synced += messages.length;
|
2019-05-20 19:35:28 +03:00
|
|
|
|
if (fetchState.synced-(fetchState.prevSynced||0) >= fetchState.total/100)
|
|
|
|
|
{
|
|
|
|
|
this.events.emit('sync', { state: 'progress', done: fetchState.synced, total: fetchState.total });
|
|
|
|
|
fetchState.prevSynced = fetchState.synced;
|
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
process.stderr.write('\rsynchronizing '+fetchState.synced);
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
|
|
|
|
|
async updateFlags(boxId, updateFlags, checkMissing)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
if (updateFlags.length)
|
|
|
|
|
{
|
|
|
|
|
let updated = await SQL.update(
|
|
|
|
|
this.pg, { m: 'messages', t: SQL.values(updateFlags) },
|
2019-05-16 21:29:58 +03:00
|
|
|
|
[ 'flags = t.flags::text[]' ],
|
2019-05-10 01:26:31 +03:00
|
|
|
|
{ 'm.folder_id': boxId, 'm.uid = t.uid::int': [] },
|
2019-05-08 16:39:14 +03:00
|
|
|
|
checkMissing ? { returning: 'm.uid' } : null
|
|
|
|
|
);
|
|
|
|
|
if (checkMissing)
|
|
|
|
|
{
|
|
|
|
|
let missing = {};
|
|
|
|
|
for (let i = 0; i < updateFlags.length; i++)
|
|
|
|
|
missing[updateFlags[i].uid] = true;
|
|
|
|
|
for (let i = 0; i < updated.length; i++)
|
|
|
|
|
delete missing[updated[i].uid];
|
|
|
|
|
return Object.keys(missing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return [];
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async quickResync(srv, boxId, maxUid, changedSince, missing)
|
|
|
|
|
{
|
|
|
|
|
let updateFlags = [];
|
|
|
|
|
let vanished = [];
|
|
|
|
|
let onVanish = function(dias)
|
|
|
|
|
{
|
|
|
|
|
vanished = vanished.concat(vanished, dias);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
srv.on('vanish', onVanish);
|
|
|
|
|
await this.imap.runFetch(
|
|
|
|
|
srv, '1:'+maxUid, { modifiers: { changedsince: changedSince+' VANISHED' } },
|
2019-05-13 16:23:53 +03:00
|
|
|
|
async (messages, state) => this.queueQuickFlags(messages, boxId, state),
|
|
|
|
|
{ updateFlags: updateFlags }
|
2019-05-08 16:39:14 +03:00
|
|
|
|
);
|
|
|
|
|
srv.removeListener('vanish', onVanish);
|
|
|
|
|
let checkedMissing = await this.updateFlags(boxId, updateFlags, missing && true);
|
|
|
|
|
if (missing)
|
|
|
|
|
{
|
|
|
|
|
missing.push.apply(missing, checkedMissing);
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
if (vanished.length)
|
|
|
|
|
{
|
|
|
|
|
await this.deleteVanished(boxId, vanished);
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async deleteVanished(boxId, vanished)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let lst = [], dia = [];
|
|
|
|
|
for (let i = 0; i < vanished.length; i++)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
if (vanished[i][1])
|
2019-05-10 01:26:31 +03:00
|
|
|
|
{
|
|
|
|
|
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]);
|
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
else
|
|
|
|
|
lst.push(vanished[i][0]);
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
if (lst.length)
|
2019-05-10 01:26:31 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
dia.push('uid IN ('+lst.join(',')+')');
|
2019-05-10 01:26:31 +03:00
|
|
|
|
}
|
|
|
|
|
await this.deleteMessages({ folder_id: boxId, ['('+dia.join(' OR ')+')']: [] });
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
queueQuickFlags(messages, boxId, fetchState)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
for (let i = 0; i < messages.length; i++)
|
|
|
|
|
{
|
|
|
|
|
let m = messages[i][0];
|
|
|
|
|
fetchState.updateFlags.push({ uid: m.uid, flags: toPgArray(m.flags) });
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async deleteMessages(where)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-14 13:54:56 +03:00
|
|
|
|
let cond = SQL.where_builder(where);
|
2019-05-10 01:26:31 +03:00
|
|
|
|
let q = SQL.select_builder('messages', 'id', where);
|
2019-05-14 13:54:56 +03:00
|
|
|
|
await this.pg.query(
|
|
|
|
|
SQL.quote_positional(
|
|
|
|
|
'WITH deleting_messages AS (SELECT id FROM messages WHERE '+cond.sql+')'+
|
|
|
|
|
', updated_threads AS ('+
|
|
|
|
|
'UPDATE threads SET first_msg=('+
|
|
|
|
|
'SELECT m.id FROM messages m WHERE m.thread_id=threads.id'+
|
|
|
|
|
' AND m.id NOT IN (SELECT id FROM deleting_messages) ORDER BY time LIMIT 1'+
|
|
|
|
|
') WHERE first_msg IN (SELECT id FROM deleting_messages)'+
|
|
|
|
|
' RETURNING id, first_msg'+
|
|
|
|
|
'), deleted_threads AS ('+
|
|
|
|
|
'DELETE FROM threads WHERE id IN (SELECT id FROM updated_threads WHERE first_msg IS NULL)'+
|
|
|
|
|
' RETURNING id'+
|
|
|
|
|
') DELETE FROM messages WHERE id IN (SELECT id FROM deleting_messages)'
|
|
|
|
|
), cond.bind
|
2019-05-08 16:39:14 +03:00
|
|
|
|
);
|
2016-10-02 21:57:43 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async saveMessages(messages, boxId)
|
2016-10-02 21:57:43 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let uids = messages.map(m => m[1].uid);
|
|
|
|
|
let exist = await SQL.select(this.pg, 'messages', 'uid, flags', { folder_id: boxId, uid: uids });
|
|
|
|
|
uids = {};
|
|
|
|
|
for (let i = 0; i < exist.length; i++)
|
|
|
|
|
{
|
|
|
|
|
uids[exist[i].uid] = true;
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < messages.length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (!uids[messages[i][1].uid])
|
|
|
|
|
{
|
|
|
|
|
await this.addMessage(boxId, messages[i][0], messages[i][1]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-18 03:01:13 +03:00
|
|
|
|
async parseMsg(msg_text)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let parser = new MailParser({ streamAttachments: false, defaultCharset: 'windows-1251' });
|
2019-05-18 03:01:13 +03:00
|
|
|
|
let msg = await new Promise((resolve, reject) =>
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{
|
2019-05-18 03:01:13 +03:00
|
|
|
|
parser.on('error', reject);
|
|
|
|
|
parser.once('end', resolve);
|
|
|
|
|
parser.write(msg_text);
|
2019-05-13 16:23:53 +03:00
|
|
|
|
parser.end();
|
2019-05-08 16:39:14 +03:00
|
|
|
|
});
|
2019-05-18 03:01:13 +03:00
|
|
|
|
let byid = {};
|
|
|
|
|
for (let a of msg.attachments||[])
|
|
|
|
|
{
|
|
|
|
|
byid[a.contentId||''] = a;
|
|
|
|
|
}
|
2019-05-19 15:25:14 +03:00
|
|
|
|
msg.html = (msg.html||'').replace(/(<img[^<>]*src=["']?)cid:([^'"\s]{1,256})/g, (m, m1, m2) =>
|
2019-05-18 03:01:13 +03:00
|
|
|
|
{
|
|
|
|
|
if (!byid[m2])
|
|
|
|
|
{
|
|
|
|
|
return m1 + 'cid:' + m2;
|
|
|
|
|
}
|
|
|
|
|
return m1 + 'data:' + byid[m2].contentType + ';base64,' + byid[m2].toString('base64');
|
|
|
|
|
});
|
|
|
|
|
let attachments = [];
|
|
|
|
|
for (let a of msg.attachments||[])
|
|
|
|
|
{
|
|
|
|
|
let hash = crypto.createHash('sha1');
|
|
|
|
|
hash.update(a.content);
|
|
|
|
|
let sha1 = hash.digest('hex');
|
|
|
|
|
let subdir = sha1.substr(0, 2)+'/'+sha1.substr(2, 2);
|
|
|
|
|
let filename = subdir+'/'+sha1+'.bin';
|
|
|
|
|
if (!await fsp.exists(this.files_path+'/'+filename))
|
|
|
|
|
{
|
|
|
|
|
if (!await fsp.exists(this.files_path+'/'+sha1.substr(0, 2)))
|
|
|
|
|
{
|
|
|
|
|
await fsp.mkdir(this.files_path+'/'+sha1.substr(0, 2));
|
|
|
|
|
}
|
|
|
|
|
if (!await fsp.exists(this.files_path+'/'+subdir))
|
|
|
|
|
{
|
|
|
|
|
await fsp.mkdir(this.files_path+'/'+subdir);
|
|
|
|
|
}
|
|
|
|
|
await fsp.writeFile(this.files_path+'/'+filename, a.content);
|
|
|
|
|
}
|
|
|
|
|
attachments.push({
|
|
|
|
|
id: a.contentId,
|
|
|
|
|
name: a.fileName,
|
|
|
|
|
mimetype: a.contentType,
|
|
|
|
|
size: a.length,
|
|
|
|
|
sha1,
|
|
|
|
|
filename,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
msg.attachments = attachments;
|
|
|
|
|
return msg;
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-20 19:35:28 +03:00
|
|
|
|
transformFlags(flags)
|
|
|
|
|
{
|
|
|
|
|
// the absence of 'seen' is transformed into the added 'unread' flag
|
|
|
|
|
// 'unseen' is something that mail.ru adds by itself
|
|
|
|
|
// 'unread' is removed to not mess up with our 'unread'
|
|
|
|
|
flags = flags.filter(f => f != 'unseen' && f != 'recent' && f != 'unread');
|
|
|
|
|
if (!flags.filter(f => f == 'seen').length)
|
|
|
|
|
flags = [ ...flags, 'unread' ];
|
|
|
|
|
else
|
|
|
|
|
flags = flags.filter(f => f != 'seen');
|
|
|
|
|
flags = flags.sort();
|
|
|
|
|
return flags;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
extractAttachments(struct, attachments)
|
2016-10-05 01:57:10 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
attachments = attachments || [];
|
|
|
|
|
for (let i = 0; i < struct.length; i++)
|
|
|
|
|
{
|
|
|
|
|
if (struct[i] instanceof Array)
|
2019-05-13 16:23:53 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
this.extractAttachments(struct[i], attachments);
|
2019-05-13 16:23:53 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
else if (struct[i].disposition && struct[i].disposition.type == 'attachment')
|
|
|
|
|
{
|
2019-05-18 03:01:13 +03:00
|
|
|
|
attachments.push({
|
|
|
|
|
name: mimelib.parseMimeWords(struct[i].disposition.params && struct[i].disposition.params.filename || struct[i].description || ''),
|
|
|
|
|
mimetype: struct[i].type+'/'+struct[i].subtype,
|
|
|
|
|
size: struct[i].size,
|
|
|
|
|
});
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2016-10-05 01:57:10 +03:00
|
|
|
|
}
|
2019-05-08 16:39:14 +03:00
|
|
|
|
return attachments;
|
2016-10-05 01:57:10 +03:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async addMessage(boxId, msgrow, attrs)
|
2016-09-11 22:04:37 +03:00
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
await this.pg.query('BEGIN');
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await this.addMessageImpl(boxId, msgrow, attrs);
|
|
|
|
|
await this.pg.query('COMMIT');
|
|
|
|
|
}
|
|
|
|
|
catch (e)
|
|
|
|
|
{
|
|
|
|
|
await this.pg.query('ROLLBACK');
|
2019-05-13 16:23:53 +03:00
|
|
|
|
throw e;
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
async addMessageImpl(boxId, msgrow, attrs)
|
|
|
|
|
{
|
|
|
|
|
let header = await this.parseMsg(msgrow.headers);
|
2016-10-05 01:46:35 +03:00
|
|
|
|
header.references = header.references || [];
|
2016-09-11 22:04:37 +03:00
|
|
|
|
if (header.references.length)
|
|
|
|
|
{
|
2016-10-05 01:46:35 +03:00
|
|
|
|
if (!header.inReplyTo || !header.inReplyTo[0])
|
|
|
|
|
header.inReplyTo = [ header.references[header.references.length-1] ];
|
|
|
|
|
else if (header.references[header.references.length-1] != header.inReplyTo[0])
|
|
|
|
|
header.references.push(header.inReplyTo[0]);
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
if (!header.date)
|
|
|
|
|
header.date = new Date(attrs.date);
|
|
|
|
|
|
2016-10-05 13:50:33 +03:00
|
|
|
|
if (JSON.stringify(header).indexOf('<27>') >= 0)
|
|
|
|
|
{
|
|
|
|
|
// Charset error!
|
|
|
|
|
console.log(iconv.decode(msgrow.headers, 'cp1251'));
|
|
|
|
|
console.log(header);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-05 01:46:35 +03:00
|
|
|
|
delete msgrow.headers;
|
2016-09-11 22:04:37 +03:00
|
|
|
|
msgrow.folder_id = boxId;
|
2016-10-05 01:46:35 +03:00
|
|
|
|
msgrow.subject = header.subject || '';
|
|
|
|
|
msgrow.props = JSON.stringify({
|
|
|
|
|
from: ((header.from||[]).map((a) => [ a.name, a.address ]))[0],
|
|
|
|
|
to: (header.to||[]).map((a) => [ a.name, a.address ]),
|
|
|
|
|
cc: (header.cc||[]).map((a) => [ a.name, a.address ]),
|
|
|
|
|
bcc: (header.bcc||[]).map((a) => [ a.name, a.address ]),
|
2016-10-05 12:51:11 +03:00
|
|
|
|
replyto: (header.replyTo||[]).map((a) => [ a.name, a.address ])[0],
|
2016-10-05 01:57:10 +03:00
|
|
|
|
attachments: this.extractAttachments(attrs.struct),
|
2019-05-20 02:21:28 +03:00
|
|
|
|
inout: (header.headers.received||[]).length ? 'in' : 'out',
|
2016-10-05 01:46:35 +03:00
|
|
|
|
});
|
|
|
|
|
msgrow.messageid = header.messageId || '';
|
|
|
|
|
msgrow.inreplyto = header.inReplyTo && header.inReplyTo[0] || '';
|
2016-09-11 22:04:37 +03:00
|
|
|
|
msgrow.time = header.date;
|
2016-10-05 01:46:35 +03:00
|
|
|
|
msgrow.size = attrs.size;
|
2019-05-20 19:35:28 +03:00
|
|
|
|
msgrow.flags = toPgArray(this.transformFlags(msgrow.flags));
|
2016-09-11 22:04:37 +03:00
|
|
|
|
msgrow.refs = toPgArray(header.references);
|
2016-10-05 01:46:35 +03:00
|
|
|
|
for (let i in msgrow)
|
|
|
|
|
if (typeof msgrow[i] == 'string')
|
|
|
|
|
msgrow[i] = msgrow[i].replace(/\x00/g, '');
|
2016-09-11 22:04:37 +03:00
|
|
|
|
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let thisIsFirst = false;
|
2016-09-11 22:04:37 +03:00
|
|
|
|
if (header.references.length)
|
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let threadId = await SQL.select(
|
|
|
|
|
this.pg, 'messages', 'MAX(thread_id)',
|
|
|
|
|
{ messageid: header.references }, null, SQL.MS_VALUE
|
|
|
|
|
);
|
2016-09-11 22:04:37 +03:00
|
|
|
|
if (!threadId)
|
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
threadId = await SQL.select(
|
|
|
|
|
this.pg, 'messages', 'MAX(thread_id)',
|
2019-05-16 21:29:58 +03:00
|
|
|
|
{ 'refs @> array[?]': msgrow.messageid }, null, SQL.MS_VALUE
|
2019-05-08 16:39:14 +03:00
|
|
|
|
);
|
2016-09-11 22:04:37 +03:00
|
|
|
|
if (threadId)
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{
|
2016-09-11 22:04:37 +03:00
|
|
|
|
thisIsFirst = true;
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
msgrow.thread_id = threadId;
|
|
|
|
|
}
|
2016-10-05 01:46:35 +03:00
|
|
|
|
console.log(msgrow.time+' '+(header.from && header.from[0] && header.from[0].address || '?')+' '+msgrow.subject);
|
2019-05-08 16:39:14 +03:00
|
|
|
|
msgrow.id = (await SQL.insert(this.pg, 'messages', msgrow, { returning: 'id' }))[0].id;
|
2016-09-11 22:04:37 +03:00
|
|
|
|
if (!msgrow.thread_id)
|
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
msgrow.thread_id = (await SQL.insert(this.pg, 'threads', {
|
2016-09-11 22:04:37 +03:00
|
|
|
|
first_msg: msgrow.id,
|
|
|
|
|
msg_count: 1
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}, { returning: 'id' }))[0].id;
|
|
|
|
|
await SQL.update(this.pg, 'messages', { thread_id: msgrow.thread_id }, { id: msgrow.id });
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2019-05-08 16:39:14 +03:00
|
|
|
|
let upd = { 'msg_count=msg_count+1': [] };
|
2016-09-11 22:04:37 +03:00
|
|
|
|
if (thisIsFirst)
|
2019-05-08 16:39:14 +03:00
|
|
|
|
{
|
2016-09-11 22:04:37 +03:00
|
|
|
|
upd.first_msg = msgrow.id;
|
2019-05-08 16:39:14 +03:00
|
|
|
|
}
|
2019-05-14 13:54:56 +03:00
|
|
|
|
await SQL.update(this.pg, 'threads', upd, { id: msgrow.thread_id });
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-17 16:57:37 +03:00
|
|
|
|
|
|
|
|
|
async fetchFullMessage(account_id, folder_id, folder_name, msg_uid)
|
|
|
|
|
{
|
|
|
|
|
let srv = await this.imap.getConnection(account_id, folder_name);
|
|
|
|
|
let upd = await this.imap.runFetch(
|
|
|
|
|
srv, msg_uid, { bodies: '' },
|
2019-05-18 03:01:13 +03:00
|
|
|
|
(messages, state) => this._parseBody(messages, folder_id)
|
2019-05-17 16:57:37 +03:00
|
|
|
|
);
|
|
|
|
|
this.imap.releaseConnection(account_id);
|
|
|
|
|
return upd;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async _parseBody(messages, boxId)
|
|
|
|
|
{
|
|
|
|
|
for (let i = 0; i < messages.length; i++)
|
|
|
|
|
{
|
|
|
|
|
let msg = messages[i];
|
|
|
|
|
let obj = await this.parseMsg(msg[0].headers);
|
|
|
|
|
obj.html = sanitizeHtml(obj.html);
|
2019-05-18 03:01:13 +03:00
|
|
|
|
let upd = {
|
|
|
|
|
body_text: obj.text||'',
|
|
|
|
|
body_html: obj.html,
|
2019-05-21 00:48:09 +03:00
|
|
|
|
body_html_text: obj.html.replace(/<style[^>]*>[\s\S]*?<\/style\s*>|<\/?[^>]*>/g, ''),
|
2019-05-18 03:01:13 +03:00
|
|
|
|
};
|
2019-05-19 15:25:14 +03:00
|
|
|
|
await SQL.update(
|
2019-05-18 03:01:13 +03:00
|
|
|
|
this.pg, 'messages m', {
|
|
|
|
|
...upd,
|
|
|
|
|
'props = props || ?': [ { attachments: obj.attachments } ]
|
|
|
|
|
}, { folder_id: boxId, uid: msg[0].uid }
|
2019-05-19 15:25:14 +03:00
|
|
|
|
);
|
2019-05-17 16:57:37 +03:00
|
|
|
|
if (messages.length == 1)
|
|
|
|
|
{
|
2019-05-18 03:01:13 +03:00
|
|
|
|
upd.props = { attachments: obj.attachments };
|
2019-05-17 16:57:37 +03:00
|
|
|
|
return [ upd ];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2019-05-20 02:21:28 +03:00
|
|
|
|
|
|
|
|
|
// flags = lowercase and without \
|
|
|
|
|
async processFlags(msgIds, action, flags)
|
|
|
|
|
{
|
|
|
|
|
flags = flags instanceof Array ? flags : [ flags ];
|
|
|
|
|
const bad_flags = flags.filter(f => f != 'seen' && f != 'answered' && f != 'flagged' && f != 'deleted' && f != 'draft');
|
|
|
|
|
if (bad_flags.length)
|
|
|
|
|
{
|
|
|
|
|
throw new Error('bad flags: '+bad_flags.join(', '));
|
|
|
|
|
}
|
|
|
|
|
let rows = await SQL.select(
|
|
|
|
|
this.pg, { m: 'messages', f: 'folders' },
|
|
|
|
|
'f.account_id, m.folder_id, f.name folder_name, m.uid',
|
2019-05-20 19:35:28 +03:00
|
|
|
|
{ 'm.id': msgIds, 'f.id=m.folder_id': [] },
|
2019-05-20 02:21:28 +03:00
|
|
|
|
{ order_by: 'm.folder_id, m.uid' }
|
|
|
|
|
);
|
|
|
|
|
if (!rows.length)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let uids = [];
|
|
|
|
|
if (!(action == 'add' || action == 'set' || action == 'del'))
|
|
|
|
|
{
|
|
|
|
|
throw new Error('processFlags: bad action = '+action);
|
|
|
|
|
}
|
|
|
|
|
for (let i = 0; i < rows.length; i++)
|
|
|
|
|
{
|
2019-05-20 19:35:28 +03:00
|
|
|
|
uids.push(rows[i].uid);
|
2019-05-20 02:21:28 +03:00
|
|
|
|
if (i == rows.length-1 || i > 0 && rows[i].folder_id != rows[i-1].folder_id)
|
|
|
|
|
{
|
|
|
|
|
let srv = await this.imap.getConnection(rows[i].account_id, rows[i].folder_name);
|
2019-05-20 19:35:28 +03:00
|
|
|
|
await new Promise((ok, no) => srv[action+'Flags'](
|
|
|
|
|
uids,
|
|
|
|
|
flags.map(f => '\\'+f.substr(0, 1).toUpperCase()+f.substr(1)),
|
|
|
|
|
(err, res) => err ? no(err) : ok(res)
|
|
|
|
|
));
|
2019-05-20 02:21:28 +03:00
|
|
|
|
this.imap.releaseConnection(rows[i].account_id);
|
|
|
|
|
uids = [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let upd = 'flags', bind = [];
|
|
|
|
|
if (action == 'add')
|
|
|
|
|
{
|
|
|
|
|
for (let flag of flags)
|
|
|
|
|
{
|
2019-05-20 19:35:28 +03:00
|
|
|
|
if (flag == 'seen')
|
|
|
|
|
{
|
|
|
|
|
// instead of the 'seen' flag we store the absence of 'unread'
|
|
|
|
|
upd = 'array_remove(' + upd + ', ?)';
|
|
|
|
|
bind.push('unread');
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
upd = upd + ' || (case when flags @> array[?] then \'{}\' else array[?] end)';
|
|
|
|
|
bind.push(flag, flag);
|
|
|
|
|
}
|
2019-05-20 02:21:28 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (action == 'del')
|
|
|
|
|
{
|
|
|
|
|
for (let flag of flags)
|
|
|
|
|
{
|
2019-05-20 19:35:28 +03:00
|
|
|
|
if (flag == 'seen')
|
|
|
|
|
{
|
|
|
|
|
// instead of the absence of 'seen' flag we store 'unread'
|
|
|
|
|
upd = upd + ' || (case when flags @> array[?] then \'{}\' else array[?] end)';
|
|
|
|
|
bind.push('unread', 'unread');
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
upd = 'array_remove('+upd+', ?)';
|
|
|
|
|
bind.push(flag);
|
|
|
|
|
}
|
2019-05-20 02:21:28 +03:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
2019-05-20 19:35:28 +03:00
|
|
|
|
flags = flags.filter(f => f == 'seen').length > 0
|
|
|
|
|
? flags.filter(f => f != 'seen')
|
|
|
|
|
: [ ...flags, 'unread' ];
|
2019-05-20 02:21:28 +03:00
|
|
|
|
upd = 'array[' + flags.map(f => '?').join(', ') + ']::text[]';
|
|
|
|
|
bind = [ ...flags ];
|
|
|
|
|
}
|
2019-05-20 19:35:28 +03:00
|
|
|
|
await SQL.update(this.pg, 'messages m', { ['flags = '+upd]: bind }, { 'm.id': msgIds });
|
2019-05-20 02:21:28 +03:00
|
|
|
|
}
|
2016-09-11 22:04:37 +03:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toPgArray(a)
|
|
|
|
|
{
|
|
|
|
|
a = JSON.stringify(a);
|
|
|
|
|
return '{'+a.substring(1, a.length-1)+'}';
|
|
|
|
|
}
|
2019-05-10 01:26:31 +03:00
|
|
|
|
|
2019-05-17 16:57:37 +03:00
|
|
|
|
function mapToHash(map)
|
|
|
|
|
{
|
|
|
|
|
let h = {};
|
|
|
|
|
for (let v of map)
|
|
|
|
|
h[v[0]] = v[1];
|
|
|
|
|
return h;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-10 01:26:31 +03:00
|
|
|
|
module.exports = Syncer;
|