First variant of flag change - synchronous
parent
b8634a9269
commit
54f872e264
76
Syncer.js
76
Syncer.js
|
@ -283,9 +283,11 @@ class Syncer
|
||||||
}
|
}
|
||||||
else
|
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];
|
delete fetchState.flags[m.uid];
|
||||||
}
|
}
|
||||||
|
@ -539,16 +541,13 @@ class Syncer
|
||||||
bcc: (header.bcc||[]).map((a) => [ a.name, a.address ]),
|
bcc: (header.bcc||[]).map((a) => [ a.name, a.address ]),
|
||||||
replyto: (header.replyTo||[]).map((a) => [ a.name, a.address ])[0],
|
replyto: (header.replyTo||[]).map((a) => [ a.name, a.address ])[0],
|
||||||
attachments: this.extractAttachments(attrs.struct),
|
attachments: this.extractAttachments(attrs.struct),
|
||||||
|
inout: (header.headers.received||[]).length ? 'in' : 'out',
|
||||||
});
|
});
|
||||||
msgrow.messageid = header.messageId || '';
|
msgrow.messageid = header.messageId || '';
|
||||||
msgrow.inreplyto = header.inReplyTo && header.inReplyTo[0] || '';
|
msgrow.inreplyto = header.inReplyTo && header.inReplyTo[0] || '';
|
||||||
msgrow.time = header.date;
|
msgrow.time = header.date;
|
||||||
msgrow.size = attrs.size;
|
msgrow.size = attrs.size;
|
||||||
if (!header.headers.received || !header.headers.received.length)
|
msgrow.flags = toPgArray(msgrow.flags.filter(f => f != 'recent'));
|
||||||
msgrow.flags.push('out');
|
|
||||||
else
|
|
||||||
msgrow.flags.push('in');
|
|
||||||
msgrow.flags = toPgArray(msgrow.flags);
|
|
||||||
msgrow.refs = toPgArray(header.references);
|
msgrow.refs = toPgArray(header.references);
|
||||||
for (let i in msgrow)
|
for (let i in msgrow)
|
||||||
if (typeof msgrow[i] == 'string')
|
if (typeof msgrow[i] == 'string')
|
||||||
|
@ -632,6 +631,69 @@ class Syncer
|
||||||
}
|
}
|
||||||
return null;
|
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)
|
function toPgArray(a)
|
||||||
|
|
25
SyncerWeb.js
25
SyncerWeb.js
|
@ -37,6 +37,7 @@ class SyncerWeb
|
||||||
this.app.get('/messages', wrapAsync(this, 'get_messages'));
|
this.app.get('/messages', wrapAsync(this, 'get_messages'));
|
||||||
this.app.get('/message', wrapAsync(this, 'get_message'));
|
this.app.get('/message', wrapAsync(this, 'get_message'));
|
||||||
this.app.get('/attachment', wrapAsync(this, 'get_attachment'));
|
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.app.post('/sync', wrapAsync(this, 'post_sync'));
|
||||||
this.syncer.events.on('sync', (params) => this.syncer_sync(params));
|
this.syncer.events.on('sync', (params) => this.syncer_sync(params));
|
||||||
}
|
}
|
||||||
|
@ -120,18 +121,22 @@ class SyncerWeb
|
||||||
.filter(f => f[1]);
|
.filter(f => f[1]);
|
||||||
p['(f.account_id, f.name) NOT IN ('+folders.map(f => '(?, ?)').join(', ')+')'] =
|
p['(f.account_id, f.name) NOT IN ('+folders.map(f => '(?, ?)').join(', ')+')'] =
|
||||||
[].concat.apply([], folders);
|
[].concat.apply([], folders);
|
||||||
p['(flags @> array[\'in\'])'] = [];
|
p['f.kind NOT IN (?, ?, ?)'] = [ 'sent', 'drafts', 'trash' ];
|
||||||
}
|
}
|
||||||
else if (query.folderType == 'sent')
|
else if (query.folderType == 'sent')
|
||||||
{
|
{
|
||||||
// Все отправленные
|
// Все отправленные
|
||||||
p['(flags @> array[\'out\'])'] = [];
|
p['f.kind'] = 'sent';
|
||||||
}
|
}
|
||||||
else if (query.folderType == 'outbox')
|
else if (query.folderType == 'outbox')
|
||||||
{
|
{
|
||||||
// FIXME это "папка" для локально составленных сообщений, не сохранённых в IMAP
|
// 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)
|
let folders = Object.keys(this.syncer.accounts)
|
||||||
.map(id => [ id, this.syncer.accounts[id].settings.folders[query.folderType] ])
|
.map(id => [ id, this.syncer.accounts[id].settings.folders[query.folderType] ])
|
||||||
|
@ -297,6 +302,20 @@ class SyncerWeb
|
||||||
res.end();
|
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)
|
syncer_sync(params)
|
||||||
{
|
{
|
||||||
this.io.emit('sync', params);
|
this.io.emit('sync', params);
|
||||||
|
|
|
@ -56,7 +56,6 @@ create table messages (
|
||||||
-- FIXME bigint
|
-- FIXME bigint
|
||||||
time timestamptz not null,
|
time timestamptz not null,
|
||||||
size int not null,
|
size int not null,
|
||||||
-- FIXME jsonb
|
|
||||||
flags text[] not null,
|
flags text[] not null,
|
||||||
foreign key (folder_id) references folders (id) on delete cascade on update cascade
|
foreign key (folder_id) references folders (id) on delete cascade on update cascade
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* TODO:
|
||||||
* + скачивание вложений
|
* + скачивание вложений
|
||||||
|
* - сохранять заголовки в БД и возможно сохранять сообщения целиком ещё где-то
|
||||||
* - пометка прочитанным, просмотренным (seen)
|
* - пометка прочитанным, просмотренным (seen)
|
||||||
* - фоновая индексация всех текстов сообщений в ящике
|
* - фоновая индексация всех текстов сообщений в ящике
|
||||||
* - написание сообщений
|
* - написание сообщений
|
||||||
|
|
Loading…
Reference in New Issue