2016-09-11 22:04:37 +03:00
const gen = require ( 'gen-thread' ) ;
const MailParser = require ( 'mailparser' ) . MailParser ;
const htmlawed = require ( 'htmlawed' ) ;
2016-10-03 16:55:09 +03:00
const http = require ( 'http' ) ;
const socket _io = require ( 'socket.io' ) ;
2016-09-11 22:04:37 +03:00
const express = require ( 'express' ) ;
const express _session = require ( 'express-session' ) ;
const bodyparser = require ( 'body-parser' ) ;
const multer = require ( 'multer' ) ;
2016-10-03 13:00:56 +03:00
const css = require ( 'css' ) ;
2016-10-07 14:39:07 +03:00
const MAX _FETCH = 100 ;
2016-09-11 22:04:37 +03:00
module . exports = SyncerWeb ;
function SyncerWeb ( syncer , pg , cfg )
{
this . syncer = syncer ;
this . pg = pg ;
this . cfg = cfg ;
this . app = express ( ) ;
2016-10-03 16:55:09 +03:00
this . http = http . Server ( this . app ) ;
this . io = socket _io ( this . http ) ;
2016-09-11 22:04:37 +03:00
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 ) ) ) ;
2016-10-07 02:06:31 +03:00
this . app . get ( '/groups' , genRequest ( this . get _groups . bind ( this ) ) ) ;
2016-09-11 22:04:37 +03:00
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 ) ) ) ;
2016-10-03 16:55:09 +03:00
this . syncer . events . on ( 'sync' , this . syncer _sync . bind ( this ) ) ;
}
SyncerWeb . prototype . listen = function ( port )
{
this . http . listen ( port ) ;
2016-09-11 22:04:37 +03:00
}
SyncerWeb . prototype . get _auth = function ( req , res )
{
2016-10-02 21:57:43 +03:00
return res . type ( 'html' ) . send (
'<form action="/auth" method="post"><input name="login" />' +
' <input name="password" type="password" /> <input type="submit" /></form>'
) ;
2016-09-11 22:04:37 +03:00
}
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 )
{
2016-10-02 21:57:43 +03:00
if ( this . cfg . login && ( ! req . session || ! req . session . auth ) )
{
2016-09-11 22:04:37 +03:00
return res . sendStatus ( 401 ) ;
2016-10-02 21:57:43 +03:00
}
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 ( ) ) ;
2016-09-11 22:04:37 +03:00
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 } ) ;
}
2016-10-07 02:06:31 +03:00
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 ) ;
}
2016-10-09 02:01:04 +03:00
SyncerWeb . prototype . msgSearchCond = function ( query )
{
2016-10-09 18:52:28 +03:00
var p = [ ] ;
2016-10-09 02:01:04 +03:00
if ( query . folderId )
2016-10-09 18:52:28 +03:00
p . push ( this . pg . sql . eq ( 'm.folder_id' , query . folderId ) ) ;
2016-10-09 02:01:04 +03:00
else if ( query . folderType == 'unread' )
2016-10-09 18:52:28 +03:00
p . push ( this . pg . sql ( '(flags @> array[\'unread\']::varchar(255)[])' ) ) ;
2016-10-09 02:01:04 +03:00
else if ( query . folderType == 'pinned' )
2016-10-09 18:52:28 +03:00
p . push ( this . pg . sql ( '(flags @> array[\'flagged\']::varchar(255)[])' ) ) ;
2016-10-09 02:01:04 +03:00
else if ( query . folderType == 'inbox' )
2016-10-09 18:52:28 +03:00
{
var folders = [ ] ;
for ( var id in this . syncer . accounts )
{
n = this . syncer . accounts [ id ] . settings . folders . spam ;
if ( n )
folders . push ( this . pg . sql . and ( { 'f.name' : n , 'f.account_id' : id } ) ) ;
}
p . push ( this . pg . sql . not ( this . pg . sql . or . apply ( this . pg . sql , folders ) ) ) ;
p . push ( this . pg . sql ( '(flags @> array[\'in\']::varchar(255)[])' ) ) ;
}
2016-10-09 17:48:47 +03:00
else if ( query . folderType == 'out' )
2016-10-09 18:52:28 +03:00
p . push ( this . pg . sql ( '(flags @> array[\'out\']::varchar(255)[])' ) ) ;
2016-10-09 02:01:04 +03:00
else if ( query . folderType == 'outbox' )
2016-10-09 17:48:47 +03:00
{
// TODO это какая-то хитрая метапапка, которая не живёт на IMAP'е ?
}
2016-10-09 02:01:04 +03:00
else if ( query . folderType == 'drafts' || query . folderType == 'spam' || query . folderType == 'trash' )
2016-10-09 17:48:47 +03:00
{
var folders = [ ] ;
var n ;
for ( var id in this . syncer . accounts )
{
n = this . syncer . accounts [ id ] . settings . folders [ query . folderType ] ;
if ( n )
folders . push ( this . pg . sql . and ( { 'f.name' : n , 'f.account_id' : id } ) ) ;
}
2016-10-09 18:52:28 +03:00
p . push ( this . pg . sql . or . apply ( this . pg . sql , folders ) ) ;
2016-10-09 17:48:47 +03:00
}
2016-10-09 18:52:28 +03:00
if ( typeof query . search == 'string' && query . search . trim ( ) )
p . push ( this . pg . sql ( 'messages_fulltext(m) @@ plainto_tsquery($1)' , query . search . trim ( ) ) ) ;
if ( query . accountId )
p . push ( this . pg . sql . and ( p , this . pg . sql . eq ( 'f.account_id' , query . accountId ) ) ) ;
return p . length ? this . pg . sql . and . apply ( this . pg . sql , p ) : null ;
2016-10-09 02:01:04 +03:00
}
2016-10-07 02:06:31 +03:00
SyncerWeb . prototype . get _groups = function * ( req , res )
{
if ( this . cfg . login && ( ! req . session || ! req . session . auth ) )
return res . sendStatus ( 401 ) ;
2016-10-09 02:01:04 +03:00
var cond = this . msgSearchCond ( req . query ) ;
if ( ! cond )
return res . status ( 500 ) . send ( 'Need message query parameters' ) ;
2016-10-07 02:06:31 +03:00
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 } ) ;
}
2016-10-09 18:52:28 +03:00
for ( var i = today . getFullYear ( ) - 1 ; i >= 1970 ; i -- )
{
intervals . unshift ( { date : i + '-01-01' , name : '' + i } ) ;
}
2016-10-07 02:06:31 +03:00
intervals . push ( { date : ymd ( today ) , name : 't' } ) ;
for ( var i = 0 ; i < intervals . length - 1 ; i ++ )
{
intervals [ i ] . date _end = intervals [ i + 1 ] . date ;
}
2016-10-09 18:52:28 +03:00
intervals [ intervals . length - 1 ] . date _end = '100000-12-31' ; // it's faster than (is null or <)
2016-10-07 02:06:31 +03:00
var [ groups ] = yield this . pg
2016-10-09 18:52:28 +03:00
. select ( 'd.name, d.date, (' +
this . pg . select ( 'count(*)' )
. from ( 'messages m' )
. innerJoin ( 'folders f' , this . pg . sql ( 'f.id=m.folder_id' ) )
. where ( cond )
. where ( this . pg . sql ( 'm.time >= d.date::date and m.time < d.date_end::date' ) )
+ ') count' )
. from ( this . pg . sql . values ( intervals ) . as ( 'd' ) . columns ( ) )
2016-10-07 02:06:31 +03:00
. orderBy ( 'date desc' ) . rows ( gen . ef ( ) ) ;
2016-10-09 18:52:28 +03:00
groups = groups . filter ( g => g . count > 0 ) ;
2016-10-07 02:06:31 +03:00
return res . send ( { groups : groups } ) ;
}
2016-09-11 22:04:37 +03:00
SyncerWeb . prototype . get _messages = function * ( req , res )
{
2016-10-02 21:57:43 +03:00
if ( this . cfg . login && ( ! req . session || ! req . session . auth ) )
2016-09-11 22:04:37 +03:00
return res . sendStatus ( 401 ) ;
2016-10-09 02:01:04 +03:00
var cond = this . msgSearchCond ( req . query ) ;
if ( ! cond )
return res . status ( 500 ) . send ( 'Need message query parameters' ) ;
2016-09-11 22:04:37 +03:00
var limit = req . query . limit || 50 ;
2016-10-07 14:39:07 +03:00
if ( limit > MAX _FETCH )
limit = MAX _FETCH ;
2016-09-11 22:04:37 +03:00
var offset = req . query . offset || 0 ;
2016-10-09 02:01:04 +03:00
var [ msgs ] = yield this . pg . select ( 'm.*' ) . from ( 'messages m' )
. innerJoin ( 'folders f' , this . pg . sql ( 'f.id=m.folder_id' ) )
. where ( cond ) . orderBy ( 'time desc' ) . limit ( limit ) . offset ( offset )
. rows ( gen . ef ( ) ) ;
2016-10-02 21:57:43 +03:00
for ( var i = 0 ; i < msgs . length ; i ++ )
{
delete msgs [ i ] . text _index ;
}
2016-09-11 22:04:37 +03:00
return res . send ( { messages : msgs } ) ;
}
SyncerWeb . prototype . get _message = function * ( req , res )
{
2016-10-02 21:57:43 +03:00
if ( this . cfg . login && ( ! req . session || ! req . session . auth ) )
2016-09-11 22:04:37 +03:00
return res . sendStatus ( 401 ) ;
var msgId = req . query . msgId ;
2016-10-09 17:48:47 +03:00
console . log ( 'fetch message ' + msgId ) ;
2016-09-11 22:04:37 +03:00
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' ) )
2016-10-02 21:57:43 +03:00
. where ( { 'm.id' : msgId } ) . rows ( gen . ef ( ) ) ;
if ( ! msg . length )
2016-09-11 22:04:37 +03:00
return res . send ( { error : 'not-found' } ) ;
2016-10-02 21:57:43 +03:00
msg = msg [ 0 ] ;
2016-09-11 22:04:37 +03:00
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 : '' } ,
2016-10-05 13:50:33 +03:00
( messages , state ) => this . getBody ( messages , msg . folder _id )
2016-09-11 22:04:37 +03:00
) ;
this . syncer . imap . releaseConnection ( msg . account _id ) ;
return res . send ( { msg : { ... msg , ... upd } } ) ;
}
return res . send ( { msg : msg } ) ;
}
2016-10-03 16:55:09 +03:00
SyncerWeb . prototype . syncer _sync = function ( params )
{
this . io . emit ( 'sync' , params ) ;
}
2016-09-11 22:04:37 +03:00
SyncerWeb . prototype . post _sync = function * ( req , res )
{
2016-10-02 21:57:43 +03:00
if ( this . cfg . login && ( ! req . session || ! req . session . auth ) )
2016-09-11 22:04:37 +03:00
return res . sendStatus ( 401 ) ;
2016-10-03 16:55:09 +03:00
if ( this . syncer . syncInProgress )
2016-09-11 22:04:37 +03:00
return res . send ( { error : 'already-running' } ) ;
2016-10-03 16:55:09 +03:00
gen . run ( this . syncer . syncAll ( ) ) ;
2016-09-11 22:04:37 +03:00
return res . send ( { status : 'started' } ) ;
}
2016-10-03 13:00:56 +03:00
function rewriteCss ( ast )
{
var rules = ast . rules || ast . stylesheet && ast . stylesheet . rules ;
2016-10-09 19:12:00 +03:00
if ( ast . stylesheet && ast . stylesheet . parsingErrors )
{
delete ast . stylesheet . parsingErrors ;
}
2016-10-03 13:00:56 +03:00
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 ) ;
}
2016-10-09 19:12:00 +03:00
else if ( rules [ i ] . type == 'rule' && ( ! rules [ i ] . selectors || ! rules [ i ] . declarations ) )
rules . splice ( i -- , 1 ) ;
2016-10-03 13:00:56 +03:00
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 ] ;
}
}
}
2016-10-05 01:46:35 +03:00
function sanitizeHtml ( html )
{
let styles = '' ;
html = ( html || '' ) . replace ( /<style[^<>]*>([\s\S]*?)<\/style\s*>/ig , function ( m , m1 )
{
styles += m1 + '\n' ;
return '' ;
} ) ;
html = html . replace ( /^[\s\S]*?<body[^<>]*>([\s\S]*)<\/body>[\s\S]*$/i , '$1' ) ;
html = html . replace ( /^[\s\S]*?<html[^<>]*>([\s\S]*)<\/html>[\s\S]*$/i , '$1' ) ;
if ( styles )
{
html = '<style>\n' + styles + '</style>\n' + html ;
styles = '' ;
}
html = htmlawed . sanitize ( html || '' , { safe : 1 , elements : '* +style' , keep _bad : 0 , comment : 1 } ) ;
html = html . replace ( /<style[^>]*>([\s\S]*)<\/style\s*>/ig , function ( m , m1 )
{
var ast = css . parse ( m1 , { silent : true } ) ;
rewriteCss ( ast ) ;
return '<style>' + css . stringify ( ast ) + '</style>' ;
} ) ;
return html ;
}
2016-10-05 13:50:33 +03:00
SyncerWeb . prototype . getBody = function * ( messages , boxId )
2016-09-11 22:04:37 +03:00
{
for ( var i = 0 ; i < messages . length ; i ++ )
{
let msg = messages [ i ] ;
2016-10-05 13:50:33 +03:00
let obj = yield * this . syncer . parseMsg ( msg [ 0 ] . headers ) ;
2016-10-05 01:46:35 +03:00
obj . html = sanitizeHtml ( obj . html ) ;
2016-09-11 22:04:37 +03:00
let upd = { body _text : obj . text || '' , body _html : obj . html } ;
upd . body _html _text = obj . html . replace ( /<style[^>]*>.*<\/style\s*>|<\/?[^>]*>/g , '' ) ;
2016-10-05 13:50:33 +03:00
yield this . pg . update ( 'messages m' , upd ) . where ( { folder _id : boxId , uid : msg [ 0 ] . uid } ) . run ( gen . ef ( ) ) ;
2016-09-11 22:04:37 +03:00
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 ) ) ;
}