First variant of flag change - synchronous

master
Vitaliy Filippov 2019-05-20 02:21:28 +03:00
parent b8634a9269
commit 54f872e264
4 changed files with 92 additions and 11 deletions

View File

@ -283,9 +283,11 @@ class Syncer
}
else
{
if (fetchState.flags[m.uid].join(',') != m.flags.join(','))
// recent тут уже не будет
let flags = m.flags.filter(f => f != 'recent').sort();
if (fetchState.flags[m.uid].sort().join(',') != flags.join(','))
{
fetchState.updateFlags.push({ uid: m.uid, flags: toPgArray(m.flags) });
fetchState.updateFlags.push({ uid: m.uid, flags: toPgArray(flags) });
}
delete fetchState.flags[m.uid];
}
@ -539,16 +541,13 @@ class Syncer
bcc: (header.bcc||[]).map((a) => [ a.name, a.address ]),
replyto: (header.replyTo||[]).map((a) => [ a.name, a.address ])[0],
attachments: this.extractAttachments(attrs.struct),
inout: (header.headers.received||[]).length ? 'in' : 'out',
});
msgrow.messageid = header.messageId || '';
msgrow.inreplyto = header.inReplyTo && header.inReplyTo[0] || '';
msgrow.time = header.date;
msgrow.size = attrs.size;
if (!header.headers.received || !header.headers.received.length)
msgrow.flags.push('out');
else
msgrow.flags.push('in');
msgrow.flags = toPgArray(msgrow.flags);
msgrow.flags = toPgArray(msgrow.flags.filter(f => f != 'recent'));
msgrow.refs = toPgArray(header.references);
for (let i in msgrow)
if (typeof msgrow[i] == 'string')
@ -632,6 +631,69 @@ class Syncer
}
return null;
}
// 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',
{ id: msgIds, 'f.id=m.folder_id': [] },
{ 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++)
{
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);
await new Promise((r, j) => srv[action+'Flags'](uids, flags.map(f => '\\'+f.substr(0, 1).toUpperCase()+f.substr(1)), r));
this.imap.releaseConnection(rows[i].account_id);
uids = [];
}
else
{
uids.push(rows[i].uid);
}
}
let upd = 'flags', bind = [];
if (action == 'add')
{
for (let flag of flags)
{
upd = upd + ' || (case when flags @> array[?] then \'{}\' else array[?] end)';
bind.push(flag, flag);
}
}
else if (action == 'del')
{
for (let flag of flags)
{
upd = 'array_remove('+upd+', ?)';
bind.push(flag);
}
}
else
{
upd = 'array[' + flags.map(f => '?').join(', ') + ']::text[]';
bind = [ ...flags ];
}
await SQL.update(this.pg, 'messages', { ['flags = '+upd]: bind }, { id: msgIds });
}
}
function toPgArray(a)

View File

@ -37,6 +37,7 @@ class SyncerWeb
this.app.get('/messages', wrapAsync(this, 'get_messages'));
this.app.get('/message', wrapAsync(this, 'get_message'));
this.app.get('/attachment', wrapAsync(this, 'get_attachment'));
this.app.post('/mark', wrapAsync(this, 'post_mark'));
this.app.post('/sync', wrapAsync(this, 'post_sync'));
this.syncer.events.on('sync', (params) => this.syncer_sync(params));
}
@ -120,18 +121,22 @@ class SyncerWeb
.filter(f => f[1]);
p['(f.account_id, f.name) NOT IN ('+folders.map(f => '(?, ?)').join(', ')+')'] =
[].concat.apply([], folders);
p['(flags @> array[\'in\'])'] = [];
p['f.kind NOT IN (?, ?, ?)'] = [ 'sent', 'drafts', 'trash' ];
}
else if (query.folderType == 'sent')
{
// Все отправленные
p['(flags @> array[\'out\'])'] = [];
p['f.kind'] = 'sent';
}
else if (query.folderType == 'outbox')
{
// FIXME это "папка" для локально составленных сообщений, не сохранённых в IMAP
}
else if (query.folderType == 'drafts' || query.folderType == 'spam' || query.folderType == 'trash')
else if (query.folderType == 'drafts' || query.folderType == 'trash')
{
p['f.kind'] = query.folderType;
}
else if (query.folderType == 'spam')
{
let folders = Object.keys(this.syncer.accounts)
.map(id => [ id, this.syncer.accounts[id].settings.folders[query.folderType] ])
@ -297,6 +302,20 @@ class SyncerWeb
res.end();
}
async post_mark(req, res)
{
if (this.cfg.login && (!req.session || !req.session.auth))
{
return res.sendStatus(401);
}
if (!req.query.msgId || !req.query.flag || req.query.flag == 'deleted')
{
return res.sendStatus(400);
}
await this.syncer.processFlags(req.query.msgId, req.query.del ? 'del' : 'add', req.query.flag);
return res.send({ ok: true });
}
syncer_sync(params)
{
this.io.emit('sync', params);

View File

@ -56,7 +56,6 @@ create table messages (
-- FIXME bigint
time timestamptz not null,
size int not null,
-- FIXME jsonb
flags text[] not null,
foreign key (folder_id) references folders (id) on delete cascade on update cascade
);

View File

@ -1,6 +1,7 @@
/**
* TODO:
* + скачивание вложений
* - сохранять заголовки в БД и возможно сохранять сообщения целиком ещё где-то
* - пометка прочитанным, просмотренным (seen)
* - фоновая индексация всех текстов сообщений в ящике
* - написание сообщений