const gen = require('gen-thread'); const MailParser = require('mailparser').MailParser; const htmlawed = require('htmlawed'); const http = require('http'); const socket_io = require('socket.io'); const express = require('express'); const express_session = require('express-session'); const bodyparser = require('body-parser'); const multer = require('multer'); const css = require('css'); const MAX_FETCH = 100; module.exports = SyncerWeb; function SyncerWeb(syncer, pg, cfg) { this.syncer = syncer; this.pg = pg; this.cfg = cfg; this.app = express(); this.http = http.Server(this.app); this.io = socket_io(this.http); this.app.use(bodyparser.urlencoded({ extended: false })); this.app.use(express_session({ secret: this.cfg.sessionSecret || '1083581xm1l3s1l39k', resave: false, saveUninitialized: false })); this.app.get('/auth', this.get_auth); this.app.post('/auth', this.post_auth); this.app.get('/folders', genRequest(this.get_folders.bind(this))); this.app.get('/groups', genRequest(this.get_groups.bind(this))); this.app.get('/messages', genRequest(this.get_messages.bind(this))); this.app.get('/message', genRequest(this.get_message.bind(this))); this.app.post('/sync', genRequest(this.post_sync.bind(this))); this.syncer.events.on('sync', this.syncer_sync.bind(this)); } SyncerWeb.prototype.listen = function(port) { this.http.listen(port); } SyncerWeb.prototype.get_auth = function(req, res) { return res.type('html').send( '
'+ '
' ); } SyncerWeb.prototype.post_auth = function(req, res) { if (!req.body) return res.sendStatus(400); if (req.body.login == this.cfg.login && req.body.password == this.cfg.password) { req.session.auth = true; return res.send({ ok: true }); } return res.send({ ok: false }); } SyncerWeb.prototype.get_folders = function*(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) { return res.sendStatus(401); } var [ accounts ] = yield this.pg.select( 'id, name, email, settings->\'folders\' folderMap,'+ ' (select count(*) from messages m, folders f where m.folder_id=f.id and f.account_id=a.id and (flags @> array[\'pinned\',\'unread\']::varchar(255)[])) pinned_unread_count' ).from('accounts a').rows(gen.ef()); var [ folders ] = yield this.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 }); } function ymd(dt) { var m = dt.getMonth()+1; var d = dt.getDate(); return dt.getFullYear()+'-'+(m < 10 ? '0'+m : m)+'-'+(d < 10 ? '0'+d : d); } SyncerWeb.prototype.get_groups = function*(req, res) { if (this.cfg.login && (!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 intervals = []; var today, today_ts; today = new Date(ymd(new Date())); today_ts = today.getTime(); var week_start = today_ts - ((today.getDay()+6)%7)*86400000; var prev_week = ymd(new Date(week_start - 86400000*7)); for (var i = 1; i <= 12; i++) { var d = today.getFullYear()+'-'+(i < 10 ? '0' : '')+i+'-01'; if (d >= prev_week) break; intervals.push({ date: d, name: 'm'+i }); } intervals.push({ date: prev_week, name: 'pw' }); for (var i = week_start, d = 1; i < today_ts; i += 86400000, d++) { intervals.push({ date: ymd(new Date(i)), name: 'd'+d }); } intervals.push({ date: ymd(today), name: 't' }); for (var i = 0; i < intervals.length-1; i++) { intervals[i].date_end = intervals[i+1].date; } intervals[i].date_end = '100000-12-31'; // it's faster than (is null or <) var [ groups ] = yield this.pg .select('d.name, d.date, count(*) count') .from('messages m') .innerJoin(this.pg.sql.values(intervals).as('d').columns(), this.pg.sql('m.time >= d.date::date and m.time < d.date_end::date')) .where({ folder_id: folderId }) .groupBy('1, 2') .union( this.pg.select('date_part(\'year\', m.time)::text as name, date_part(\'year\', m.time)::text||\'-01-01\' date, count(*) count') .from('messages m').where({ folder_id: folderId }) .where(this.pg.sql.lt('m.time', intervals[0].date)) .groupBy('1, 2') ) .orderBy('date desc').rows(gen.ef()); return res.send({ groups: groups }); } SyncerWeb.prototype.get_messages = function*(req, res) { if (this.cfg.login && (!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; if (limit > MAX_FETCH) limit = MAX_FETCH; var offset = req.query.offset || 0; var [ msgs ] = yield this.pg.select('*').from('messages').where({ folder_id: folderId }) .orderBy('time desc').limit(limit).offset(offset).rows(gen.ef()); for (var i = 0; i < msgs.length; i++) { delete msgs[i].text_index; } return res.send({ messages: msgs }); } SyncerWeb.prototype.get_message = function*(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) return res.sendStatus(401); var msgId = req.query.msgId; var [ msg ] = yield this.pg.select('m.*, f.name folder_name, f.account_id') .from('messages m').join('folders f', this.pg.sql('f.id=m.folder_id')) .where({ 'm.id': msgId }).rows(gen.ef()); if (!msg.length) return res.send({ error: 'not-found' }); msg = msg[0]; delete msg.text_index; if (!msg.body_html && !msg.body_text) { var srv = yield* this.syncer.imap.getConnection(msg.account_id, msg.folder_name); var [ upd ] = yield* this.syncer.imap.runFetch( srv, msg.uid, { bodies: '' }, (messages, state) => this.getBody(messages, msg.folder_id) ); this.syncer.imap.releaseConnection(msg.account_id); return res.send({ msg: { ...msg, ...upd } }); } return res.send({ msg: msg }); } SyncerWeb.prototype.syncer_sync = function(params) { this.io.emit('sync', params); } SyncerWeb.prototype.post_sync = function*(req, res) { if (this.cfg.login && (!req.session || !req.session.auth)) return res.sendStatus(401); if (this.syncer.syncInProgress) return res.send({ error: 'already-running' }); gen.run(this.syncer.syncAll()); return res.send({ status: 'started' }); } function rewriteCss(ast) { var rules = ast.rules || ast.stylesheet && ast.stylesheet.rules; if (rules) { for (var i = 0; i < rules.length; i++) { if (rules[i].type == 'document') { // prune @document instructions (may spy on current URL) rules.splice(i--, 1); } else rewriteCss(rules[i]); } } else if (ast.type == 'rule') { for (var i = 0; i < ast.selectors.length; i++) { // FIXME: Do not hardcode css selector for frontend here // This will require generating unique substitution string, // so we may also generate 'blocked images' stubs when we do it. ast.selectors[i] = '.message-view .text '+ast.selectors[i]; } } } function sanitizeHtml(html) { let styles = ''; html = (html||'').replace(/]*>([\s\S]*?)<\/style\s*>/ig, function(m, m1) { styles += m1+'\n'; return ''; }); html = html.replace(/^[\s\S]*?]*>([\s\S]*)<\/body>[\s\S]*$/i, '$1'); html = html.replace(/^[\s\S]*?]*>([\s\S]*)<\/html>[\s\S]*$/i, '$1'); if (styles) { html = '\n'+html; styles = ''; } html = htmlawed.sanitize(html||'', { safe: 1, elements: '* +style', keep_bad: 0, comment: 1 }); html = html.replace(/]*>([\s\S]*)<\/style\s*>/ig, function(m, m1) { var ast = css.parse(m1, { silent: true }); rewriteCss(ast); return ''; }); return html; } SyncerWeb.prototype.getBody = function*(messages, boxId) { for (var i = 0; i < messages.length; i++) { let msg = messages[i]; let obj = yield* this.syncer.parseMsg(msg[0].headers); obj.html = sanitizeHtml(obj.html); let upd = { body_text: obj.text||'', body_html: obj.html }; upd.body_html_text = obj.html.replace(/]*>.*<\/style\s*>|<\/?[^>]*>/g, ''); yield this.pg.update('messages m', upd).where({ folder_id: boxId, uid: msg[0].uid }).run(gen.ef()); if (messages.length == 1) return [ upd ]; } } function genRequest(fn) { return (req, res) => gen.run(fn(req, res), null, e => res.status(500).send('Internal Error: '+e.stack)); }