begin http server
parent
3c491d0ba8
commit
875dee61c3
30
db.sql
30
db.sql
|
@ -41,8 +41,12 @@ create table messages (
|
||||||
cc_list text not null,
|
cc_list text not null,
|
||||||
bcc_list text not null,
|
bcc_list text not null,
|
||||||
headers text not null,
|
headers text not null,
|
||||||
body text not null,
|
body_html text not null,
|
||||||
|
body_text text not null,
|
||||||
|
body_html_text text not null,
|
||||||
|
text_index tsvector not null,
|
||||||
time timestamptz not null,
|
time timestamptz not null,
|
||||||
|
size unsigned not null,
|
||||||
flags varchar(255)[] not null,
|
flags varchar(255)[] not null,
|
||||||
vertag int not null default 0,
|
vertag int not null default 0,
|
||||||
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
|
||||||
|
@ -53,6 +57,29 @@ create index messages_messageid on messages (messageid);
|
||||||
create index messages_refs on messages using gin (refs);
|
create index messages_refs on messages using gin (refs);
|
||||||
create index messages_vertag on messages (folder_id, vertag);
|
create index messages_vertag on messages (folder_id, vertag);
|
||||||
create index messages_time on messages (folder_id, time);
|
create index messages_time on messages (folder_id, time);
|
||||||
|
create index messages_text on messages using gin (text_index);
|
||||||
|
create or replace function fn_messages_text_index() returns trigger
|
||||||
|
security definer language plpgsql as $$
|
||||||
|
begin
|
||||||
|
NEW.text_index = (
|
||||||
|
setweight(to_tsvector('russian', regexp_replace(NEW.from_name || ' ' || NEW.from_email || ' ' ||
|
||||||
|
NEW.replyto_name || ' ' || NEW.replyto_email || ' ' ||
|
||||||
|
NEW.to_list || ' ' || NEW.cc_list || ' ' || NEW.bcc_list || ' ' || NEW.subject, '\W+', ' ', 'g')), 'A') ||
|
||||||
|
setweight(to_tsvector('russian', NEW.body_html_text || ' ' || NEW.body_text), 'B')
|
||||||
|
);
|
||||||
|
return NEW;
|
||||||
|
end
|
||||||
|
$$;
|
||||||
|
create trigger messages_text_index before insert or update on messages
|
||||||
|
for each row execute procedure fn_messages_text_index();
|
||||||
|
|
||||||
|
create table attachments (
|
||||||
|
id serial not null primary key,
|
||||||
|
msg_id int not null,
|
||||||
|
ctype varchar(255) not null,
|
||||||
|
size unsigned not null,
|
||||||
|
foreign key (msg_id) references messages (id) on delete cascade on update cascade
|
||||||
|
);
|
||||||
|
|
||||||
create table threads (
|
create table threads (
|
||||||
id serial not null primary key,
|
id serial not null primary key,
|
||||||
|
@ -60,6 +87,7 @@ create table threads (
|
||||||
msg_count int not null default 1,
|
msg_count int not null default 1,
|
||||||
foreign key (first_msg) references messages (id) on delete restrict on update cascade
|
foreign key (first_msg) references messages (id) on delete restrict on update cascade
|
||||||
);
|
);
|
||||||
|
create index threads_first_msg on threads (first_msg);
|
||||||
|
|
||||||
alter table messages add foreign key (thread_id) references threads (id) on delete restrict on update cascade;
|
alter table messages add foreign key (thread_id) references threads (id) on delete restrict on update cascade;
|
||||||
|
|
||||||
|
|
324
operetta.js
324
operetta.js
|
@ -1,40 +1,217 @@
|
||||||
|
// TODO: Получать, парсить и хранить тела писем (и, вероятно, вложения) + индексировать тексты
|
||||||
|
// TODO: Группировка писем
|
||||||
// TODO: Висеть в виде демона и сразу получать новые письма (IDLE)
|
// TODO: Висеть в виде демона и сразу получать новые письма (IDLE)
|
||||||
// TODO: Сделать веб-сервер для обновления view
|
// TODO: Сделать веб-сервер
|
||||||
// TODO: Сделать подписки на новые сообщения по вебсокетам
|
// TODO: Сделать подписки на новые сообщения по вебсокетам
|
||||||
|
// TODO: Чего я ещё хотел - интеграцию с maillog'ом и серверным спамфильтром
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Нужные методы API:
|
||||||
|
* - список аккаунтов и папок
|
||||||
|
* - список сообщений в папке
|
||||||
|
* - содержимое сообщения
|
||||||
|
* - поиск по тексту
|
||||||
|
* - список тредов в папке, с сообщениями
|
||||||
|
* - проверить почту
|
||||||
|
* - пометить прочтённым
|
||||||
|
* - переместить
|
||||||
|
* - удалить
|
||||||
|
* - подсказка адресов To
|
||||||
|
* - отправить сообщение
|
||||||
|
*
|
||||||
|
* В определённом плане получается тупость - получается, что дублируешь
|
||||||
|
* функционал самого почтового сервера. Но шо ж с ним поделаешь, если он "ни ф силах"...
|
||||||
|
* Ведь по сути-то, MTA от такой штуки нужен только 1 метод: "добавить сообщение в папку".
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('heapdump');
|
||||||
|
|
||||||
|
const gen = require("gen-thread");
|
||||||
|
const Imap = require('imap');
|
||||||
|
const iconv = require('iconv-lite');
|
||||||
|
const MailParser = require('mailparser').MailParser;
|
||||||
|
const bricks = require('pg-bricks');
|
||||||
|
const htmlawed = require('htmlawed');
|
||||||
|
|
||||||
|
const express = require('express');
|
||||||
|
const express_session = require('express-session');
|
||||||
|
const bodyparser = require('body-parser');
|
||||||
|
const multer = require('multer');
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||||
var cfg = require('./cfg.json');
|
var cfg = require('./cfg.json');
|
||||||
|
|
||||||
require('heapdump');
|
var pg = bricks.configure(cfg.pg);
|
||||||
var gen = require('gen-thread');
|
|
||||||
var Imap = require('imap');
|
|
||||||
var inspect = require('util').inspect;
|
|
||||||
var iconv = require('iconv-lite');
|
|
||||||
|
|
||||||
var bricks = require('pg-bricks');
|
|
||||||
var pg = bricks.configure('postgresql://'+cfg.pg.user+':'+cfg.pg.password+'@'+(cfg.pg.host||'')+':'+cfg.pg.port+'/'+cfg.pg.database);
|
|
||||||
|
|
||||||
function splitEmails(s)
|
|
||||||
{
|
|
||||||
var re = /^[\s,]*(?:(?:["'](.*?)["']|([^<]+))\s*<([^>]+)>|<?([^<>]+)>?)/; // '
|
|
||||||
var m, r = [];
|
|
||||||
while (m = re.exec(s))
|
|
||||||
{
|
|
||||||
s = s.substr(m[0].length);
|
|
||||||
r.push({ name: (m[1]||m[2]||'').trim(), email: (m[3]||m[4]||'').trim() });
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toPgArray(a)
|
|
||||||
{
|
|
||||||
a = JSON.stringify(a);
|
|
||||||
return '{'+a.substring(1, a.length-1)+'}';
|
|
||||||
}
|
|
||||||
|
|
||||||
var Syncer = {
|
var Syncer = {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Syncer.app = express();
|
||||||
|
|
||||||
|
Syncer.app.use(bodyparser.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
Syncer.app.use(express_session({
|
||||||
|
secret: cfg.sessionSecret || '1083581xm1l3s1l39k',
|
||||||
|
resave: false,
|
||||||
|
saveUninitialized: false
|
||||||
|
}));
|
||||||
|
|
||||||
|
Syncer.app.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>');
|
||||||
|
});
|
||||||
|
|
||||||
|
Syncer.app.post('/auth', function(req, res)
|
||||||
|
{
|
||||||
|
if (!req.body)
|
||||||
|
return res.sendStatus(400);
|
||||||
|
if (req.body.login == cfg.login && req.body.password == cfg.password)
|
||||||
|
{
|
||||||
|
req.session.auth = true;
|
||||||
|
return res.send({ ok: true });
|
||||||
|
}
|
||||||
|
return res.send({ ok: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
Syncer.app.get('/folders', genRequest(function*(req, res)
|
||||||
|
{
|
||||||
|
var self = Syncer;
|
||||||
|
if (!req.session || !req.session.auth)
|
||||||
|
return res.sendStatus(401);
|
||||||
|
var [ accounts ] = yield pg.select('id, name, email').from('accounts').rows(gen.ef());
|
||||||
|
var [ folders ] = yield pg.select(
|
||||||
|
'id, account_id, name,'+
|
||||||
|
' (select count(*) from messages m where m.folder_id=f.id) total_count,'+
|
||||||
|
' (select count(*) from messages m where m.folder_id=f.id and (flags @> array[\'unread\']::varchar(255)[])) unread_count'
|
||||||
|
).from('folders f').orderBy('account_id, name').rows(gen.ef());
|
||||||
|
var fh = {};
|
||||||
|
for (let i = 0; i < folders.length; i++)
|
||||||
|
{
|
||||||
|
fh[folders[i].account_id] = fh[folders[i].account_id] || [];
|
||||||
|
fh[folders[i].account_id].push(folders[i]);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < accounts.length; i++)
|
||||||
|
{
|
||||||
|
accounts[i].folders = fh[accounts[i].id] || [];
|
||||||
|
}
|
||||||
|
return res.send({ accounts: accounts });
|
||||||
|
}));
|
||||||
|
|
||||||
|
Syncer.app.get('/messages', genRequest(function*(req, res)
|
||||||
|
{
|
||||||
|
var self = Syncer;
|
||||||
|
if (!req.session || !req.session.auth)
|
||||||
|
return res.sendStatus(401);
|
||||||
|
var folderId = req.query.folderId;
|
||||||
|
if (!folderId)
|
||||||
|
return res.status(500).send('Need `folderId` query parameter');
|
||||||
|
var limit = req.query.limit || 50;
|
||||||
|
var offset = req.query.offset || 0;
|
||||||
|
var [ msgs ] = yield pg.select('*').from('messages').where({ folder_id: folderId })
|
||||||
|
.limit(limit).offset(offset).rows(gen.ef());
|
||||||
|
return res.send({ messages: msgs });
|
||||||
|
}));
|
||||||
|
|
||||||
|
Syncer.app.get('/message', genRequest(function*(req, res)
|
||||||
|
{
|
||||||
|
var self = Syncer;
|
||||||
|
if (!req.session || !req.session.auth)
|
||||||
|
return res.sendStatus(401);
|
||||||
|
var msgId = req.query.msgId;
|
||||||
|
var [ msg ] = yield pg.select('m.*, f.name folder_name, f.account_id')
|
||||||
|
.from('messages m').join('folders f', 'f.id=m.folder_id')
|
||||||
|
.where({ 'm.id': msgId }).row(gen.ef());
|
||||||
|
if (!msg)
|
||||||
|
return res.send({ error: 'not-found' });
|
||||||
|
if (!msg.body_html && !msg.body_text)
|
||||||
|
{
|
||||||
|
var srv = yield* self.getConnection(msg.account_id, boxName);
|
||||||
|
var [ upd ] = yield* self.runFetch(msg.uid, { bodies: '' }, msg.folder_id, 'getBody');
|
||||||
|
self.releaseConnection(accountId);
|
||||||
|
return res.send({ msg: { ...msg, ...upd } });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
Syncer.getBody = function*(messages, boxId)
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
var p = new MailParser({ streamAttachments: false, defaultCharset: 'windows-1251' });
|
||||||
|
for (var i = 0; i < messages.length; i++)
|
||||||
|
{
|
||||||
|
let msg = messages[i];
|
||||||
|
p.on('end', gen.cb());
|
||||||
|
p.write(msg[0].headers);
|
||||||
|
let obj = yield p.end();
|
||||||
|
delete msg[0].headers;
|
||||||
|
obj.html = htmlawed.sanitize(obj.html, { safe: 1, elements: '* +style' });
|
||||||
|
let upd = { body_text: obj.text, body_html: obj.html };
|
||||||
|
upd.body_html_text = obj.html.replace(/<style[^>]*>.*<\/style\s*>|<\/?[^>]*>/g, '');
|
||||||
|
yield pg.update('messages m', upd).where({ folder_id: boxId, uid: msg[0].uid }).run(gen.ef());
|
||||||
|
if (messages.length == 1)
|
||||||
|
return [ upd ];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Syncer.getConnection = function*(accountId, boxName)
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
if (self.connections[accountId])
|
||||||
|
{
|
||||||
|
if (self.busy[accountId])
|
||||||
|
yield self.queue[accountId].push(gen.cb());
|
||||||
|
if (boxName && self.selected[accountId] != boxName)
|
||||||
|
{
|
||||||
|
yield srv.openBox(boxName, true, gen.ef());
|
||||||
|
self.selected[accountId] = boxName;
|
||||||
|
}
|
||||||
|
self.busy[accountId] = true;
|
||||||
|
return self.connections[accountId];
|
||||||
|
}
|
||||||
|
|
||||||
|
var srv = new Imap(self.accounts[accountId].imap);
|
||||||
|
|
||||||
|
srv.once('ready', gen.cb());
|
||||||
|
// FIXME handle connection errors
|
||||||
|
yield srv.connect();
|
||||||
|
yield srv._enqueue('ENABLE QRESYNC', gen.cb());
|
||||||
|
|
||||||
|
// Monkey-patch node-imap to support VANISHED responses
|
||||||
|
var oldUT = srv._parser._resUntagged;
|
||||||
|
srv._parser._resUntagged = function()
|
||||||
|
{
|
||||||
|
var m;
|
||||||
|
if (m = /^\* VANISHED( \(EARLIER\))? ([\d:,]+)/.exec(this._buffer))
|
||||||
|
self.vanished.push(m[2].split(/,/).map(s => s.split(':')));
|
||||||
|
oldUT.apply(this);
|
||||||
|
};
|
||||||
|
|
||||||
|
srv.on('close', function()
|
||||||
|
{
|
||||||
|
delete self.connections[accountId];
|
||||||
|
if (self.srv == srv)
|
||||||
|
delete self.srv;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (boxName)
|
||||||
|
{
|
||||||
|
yield srv.openBox(boxName, true, gen.ef());
|
||||||
|
self.selected[accountId] = boxName;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.connections[accountId] = srv;
|
||||||
|
self.busy[accountId] = true;
|
||||||
|
self.queue[accountId] = [];
|
||||||
|
return srv;
|
||||||
|
}
|
||||||
|
|
||||||
|
Syncer.releaseConnection = function(accountId)
|
||||||
|
{
|
||||||
|
var self = this;
|
||||||
|
self.busy[accountId] = false;
|
||||||
|
if (self.queue[accountId].length)
|
||||||
|
(self.queue[accountId].shift())();
|
||||||
|
}
|
||||||
|
|
||||||
Syncer.sync = function*(account)
|
Syncer.sync = function*(account)
|
||||||
{
|
{
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -95,8 +272,6 @@ Syncer.sync = function*(account)
|
||||||
.where({ folder_id: row.id }).val(gen.ef());
|
.where({ folder_id: row.id }).val(gen.ef());
|
||||||
self.versionTag = self.versionTag || 0;
|
self.versionTag = self.versionTag || 0;
|
||||||
}
|
}
|
||||||
yield pg.update('folders', { uidvalidity: box.uidvalidity, unread_count: box.messages.new, highestmodseq: box.highestmodseq||0 })
|
|
||||||
.where({ id: row.id }).run(gen.ef());
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -104,9 +279,9 @@ Syncer.sync = function*(account)
|
||||||
name: box.name,
|
name: box.name,
|
||||||
uidvalidity: box.uidvalidity,
|
uidvalidity: box.uidvalidity,
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
unread_count: box.messages.new,
|
highestmodseq: 0,
|
||||||
highestmodseq: box.highestmodseq||0,
|
//unread_count: box.messages.new,
|
||||||
//total_count: box.messages.count
|
//total_count: box.messages.total,
|
||||||
}).returning('id').row(gen.ef());
|
}).returning('id').row(gen.ef());
|
||||||
boxId = row.id;
|
boxId = row.id;
|
||||||
}
|
}
|
||||||
|
@ -131,7 +306,7 @@ Syncer.sync = function*(account)
|
||||||
}
|
}
|
||||||
if (lst.length)
|
if (lst.length)
|
||||||
dia.push('uid IN ('+lst.join(',')+')');
|
dia.push('uid IN ('+lst.join(',')+')');
|
||||||
yield pg.delete('messages').where({ folder_id: boxId }).where(pg.sql('('+dia.join(' OR ')+')')).run(gen.ef());
|
yield* self.deleteMessages(pg.sql.and({ folder_id: boxId }, pg.sql('('+dia.join(' OR ')+')')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (maxUid)
|
else if (maxUid)
|
||||||
|
@ -150,22 +325,7 @@ Syncer.sync = function*(account)
|
||||||
process.stderr.write('\n');
|
process.stderr.write('\n');
|
||||||
|
|
||||||
// delete messages removed from IMAP server
|
// delete messages removed from IMAP server
|
||||||
yield pg.update('threads', { first_msg: null })
|
yield* self.deleteMessages(pg.sql.and({ folder_id: boxId }, pg.sql('uid is not null'), pg.sql.lt('vertag', self.versionTag)));
|
||||||
.where(pg.sql('first_msg IN ('+
|
|
||||||
pg.select('id').from('messages')
|
|
||||||
.where({ folder_id: boxId })
|
|
||||||
.where(pg.sql('uid is not null'))
|
|
||||||
.where(pg.sql.lt('vertag', self.versionTag))
|
|
||||||
+')')).run(gen.ef());
|
|
||||||
yield pg.delete('messages')
|
|
||||||
.where({ folder_id: boxId })
|
|
||||||
.where(pg.sql('uid is not null'))
|
|
||||||
.where(pg.sql.lt('vertag', self.versionTag)).run(gen.ef());
|
|
||||||
yield pg.update('threads',
|
|
||||||
{ first_msg: pg.sql('('+
|
|
||||||
pg.select('MIN(id)').from('messages').where({ thread_id: pg.sql('threads.id') })+')'
|
|
||||||
) }).where(pg.sql('first_msg IS NULL')).run(gen.ef());
|
|
||||||
yield pg.delete('threads').where(pg.sql('first_msg IS NULL')).run(gen.ef());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch new messages
|
// fetch new messages
|
||||||
|
@ -175,12 +335,32 @@ Syncer.sync = function*(account)
|
||||||
bodies: 'HEADER'
|
bodies: 'HEADER'
|
||||||
}, boxId, 'saveMessages');
|
}, boxId, 'saveMessages');
|
||||||
|
|
||||||
|
yield pg.update('folders', {
|
||||||
|
uidvalidity: box.uidvalidity,
|
||||||
|
//unread_count: box.messages.new,
|
||||||
|
highestmodseq: box.highestmodseq||0
|
||||||
|
}).where({ id: row.id }).run(gen.ef());
|
||||||
|
|
||||||
yield srv.closeBox(gen.cb());
|
yield srv.closeBox(gen.cb());
|
||||||
}
|
}
|
||||||
srv.end();
|
srv.end();
|
||||||
self.srv = null;
|
self.srv = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Syncer.deleteMessages = function*(where)
|
||||||
|
{
|
||||||
|
console.log(where+'');
|
||||||
|
yield pg.update('threads', { first_msg: null })
|
||||||
|
.where(pg.sql('first_msg IN ('+pg.select('id').from('messages').where(where)+')'))
|
||||||
|
.run(gen.ef());
|
||||||
|
yield pg.delete('messages').where(where).run(gen.ef());
|
||||||
|
yield pg.update('threads',
|
||||||
|
{ first_msg: pg.sql('('+
|
||||||
|
pg.select('id').from('messages').where({ thread_id: pg.sql('threads.id') }).orderBy('time').limit(1)
|
||||||
|
+')') }).where(pg.sql('first_msg IS NULL')).run(gen.ef());
|
||||||
|
yield pg.delete('threads').where(pg.sql('first_msg IS NULL')).run(gen.ef());
|
||||||
|
}
|
||||||
|
|
||||||
Syncer.runFetch = function*(what, params, boxId, processor)
|
Syncer.runFetch = function*(what, params, boxId, processor)
|
||||||
{
|
{
|
||||||
var self = this;
|
var self = this;
|
||||||
|
@ -190,6 +370,7 @@ Syncer.runFetch = function*(what, params, boxId, processor)
|
||||||
self.paused = false;
|
self.paused = false;
|
||||||
self.synced = 0;
|
self.synced = 0;
|
||||||
self.pending = [];
|
self.pending = [];
|
||||||
|
self.results = [];
|
||||||
|
|
||||||
var cb, wait;
|
var cb, wait;
|
||||||
f.on('message', function(msg, seqnum)
|
f.on('message', function(msg, seqnum)
|
||||||
|
@ -204,9 +385,20 @@ Syncer.runFetch = function*(what, params, boxId, processor)
|
||||||
if (self.parsed <= 0)
|
if (self.parsed <= 0)
|
||||||
cb();
|
cb();
|
||||||
else if (self.pending.length > 0)
|
else if (self.pending.length > 0)
|
||||||
gen.run(self[processor](self.pending, boxId), saveLast, function(e) { saveLast(); throw e; });
|
{
|
||||||
|
var result = gen.run(self[processor](self.pending, boxId), saveLast, function(e) { saveLast(); throw e; });
|
||||||
|
if (result)
|
||||||
|
self.results = self.results.concat(result);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (self.results.length > 0)
|
||||||
|
{
|
||||||
|
let r = self.results;
|
||||||
|
delete self.results;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
function saveLast()
|
function saveLast()
|
||||||
{
|
{
|
||||||
self.parsed -= self.pending.length;
|
self.parsed -= self.pending.length;
|
||||||
|
@ -240,9 +432,12 @@ Syncer.onMessage = function*(msg, seqnum, boxId, processor)
|
||||||
var m = self.pending;
|
var m = self.pending;
|
||||||
self.pending = [];
|
self.pending = [];
|
||||||
var err;
|
var err;
|
||||||
|
var result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
yield gen.run(self[processor](m, boxId), gen.cb());
|
result = yield gen.run(self[processor](m, boxId), gen.cb());
|
||||||
|
if (result)
|
||||||
|
self.results = self.results.concat(result);
|
||||||
}
|
}
|
||||||
catch (e)
|
catch (e)
|
||||||
{
|
{
|
||||||
|
@ -276,7 +471,6 @@ Syncer.parseMessage = function*(msg, seqnum, boxId)
|
||||||
});
|
});
|
||||||
stream.once('end', function()
|
stream.once('end', function()
|
||||||
{
|
{
|
||||||
msgrow.body = '';
|
|
||||||
var b = buffer.toString('utf8');
|
var b = buffer.toString('utf8');
|
||||||
if (b.indexOf('<27>') >= 0)
|
if (b.indexOf('<27>') >= 0)
|
||||||
{
|
{
|
||||||
|
@ -436,9 +630,33 @@ Syncer.addMessage = function*(msgrow, attrs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Syncer.app.listen(8057);
|
||||||
|
|
||||||
gen.run(function*()
|
gen.run(function*()
|
||||||
{
|
{
|
||||||
for (var i = 0; i < cfg.accounts.length; i++)
|
for (var i = 0; i < cfg.accounts.length; i++)
|
||||||
yield* Syncer.sync(cfg.accounts[i]);
|
yield* Syncer.sync(cfg.accounts[i]);
|
||||||
process.exit();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function genRequest(fn)
|
||||||
|
{
|
||||||
|
return (req, res) => gen.run(fn(req, res), null, e => res.status(500).send('Internal Error: '+e));
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitEmails(s)
|
||||||
|
{
|
||||||
|
var re = /^[\s,]*(?:(?:["'](.*?)["']|([^<]+))\s*<([^>]+)>|<?([^<>]+)>?)/; // '
|
||||||
|
var m, r = [];
|
||||||
|
while (m = re.exec(s))
|
||||||
|
{
|
||||||
|
s = s.substr(m[0].length);
|
||||||
|
r.push({ name: (m[1]||m[2]||'').trim(), email: (m[3]||m[4]||'').trim() });
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPgArray(a)
|
||||||
|
{
|
||||||
|
a = JSON.stringify(a);
|
||||||
|
return '{'+a.substring(1, a.length-1)+'}';
|
||||||
|
}
|
||||||
|
|
12
package.json
12
package.json
|
@ -8,12 +8,22 @@
|
||||||
"description": "Operetta webmail backend",
|
"description": "Operetta webmail backend",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"gen-thread": "latest",
|
"gen-thread": "latest",
|
||||||
|
"htmlawed": "latest",
|
||||||
|
"body-parser": "latest",
|
||||||
|
"express": "latest",
|
||||||
|
"express-session": "latest",
|
||||||
"iconv-lite": "latest",
|
"iconv-lite": "latest",
|
||||||
"imap": "latest",
|
"imap": "latest",
|
||||||
"mailparser": "latest",
|
"mailparser": "latest",
|
||||||
|
"multer": "latest",
|
||||||
"nodemailer": "latest",
|
"nodemailer": "latest",
|
||||||
"pg": "latest",
|
"pg": "latest",
|
||||||
"pg-bricks": "latest"
|
"pg-bricks": "latest",
|
||||||
|
"sql-bricks": "latest",
|
||||||
|
"socket.io": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"sql-bricks": ">=1.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-cli": "latest",
|
"babel-cli": "latest",
|
||||||
|
|
Loading…
Reference in New Issue