/ * *
* Roundcube Webmail Client Script
*
* This file is part of the Roundcube Webmail client
*
* @ licstart The following is the entire license notice for the
* JavaScript code in this file .
*
* Copyright ( C ) The Roundcube Dev Team
* Copyright ( C ) Kolab Systems AG
*
* The JavaScript code in this page is free software : you can
* redistribute it and / or modify it under the terms of the GNU
* General Public License ( GNU GPL ) as published by the Free Software
* Foundation , either version 3 of the License , or ( at your option )
* any later version . The code is distributed WITHOUT ANY WARRANTY ;
* without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE . See the GNU GPL for more details .
*
* As additional permission under GNU GPL version 3 section 7 , you
* may distribute non - source ( e . g . , minimized or compacted ) forms of
* that code without the copy of the GNU GPL normally required by
* section 4 , provided you include this license notice and a URL
* through which recipients can access the Corresponding Source .
*
* @ licend The above is the entire license notice
* for the JavaScript code in this file .
*
* @ author Thomas Bruederli < roundcube @ gmail . com >
* @ author Aleksander 'A.L.E.C' Machniak < alec @ alec . pl >
* @ author Charles McNulty < charles @ charlesmcnulty . com >
*
* @ requires jquery . js , common . js , list . js
* /
function rcube _webmail ( )
{
this . labels = { } ;
this . buttons = { } ;
this . buttons _sel = { } ;
this . gui _objects = { } ;
this . gui _containers = { } ;
this . commands = { } ;
this . command _handlers = { } ;
this . onloads = [ ] ;
this . messages = { } ;
this . group2expand = { } ;
this . http _request _jobs = { } ;
this . menu _stack = [ ] ;
this . menu _buttons = { } ;
this . entity _selectors = [ ] ;
this . image _style = { } ;
this . uploads = { } ;
// webmail client settings
this . dblclick _time = 500 ;
this . message _time = 5000 ;
this . preview _delay _select = 400 ;
this . preview _delay _click = 60 ;
this . identifier _expr = /[^0-9a-z_-]/gi ;
// environment defaults
this . env = {
request _timeout : 180 , // seconds
draft _autosave : 0 , // seconds
comm _path : './' ,
recipients _separator : ',' , // @deprecated
recipients _delimiter : ', ' , // @deprecated
popup _width : 1150 ,
popup _width _small : 900 ,
thread _padding : '15px'
} ;
// create protected reference to myself
this . ref = 'rcmail' ;
var ref = this ;
// set jQuery ajax options
$ . ajaxSetup ( {
cache : false ,
timeout : this . env . request _timeout * 1000 ,
error : function ( request , status , err ) { ref . http _error ( request , status , err ) ; } ,
beforeSend : function ( xmlhttp ) { xmlhttp . setRequestHeader ( 'X-Roundcube-Request' , ref . env . request _token ) ; }
} ) ;
// unload fix
$ ( window ) . on ( 'beforeunload' , function ( ) { ref . unload = true ; } ) ;
// set environment variable(s)
this . set _env = function ( p , value )
{
if ( p != null && typeof p === 'object' && ! value )
for ( var n in p )
this . env [ n ] = p [ n ] ;
else
this . env [ p ] = value ;
} ;
// add a localized label to the client environment
this . add _label = function ( p , value )
{
if ( typeof p == 'string' )
this . labels [ p ] = value ;
else if ( typeof p == 'object' )
$ . extend ( this . labels , p ) ;
} ;
// add a button to the button list
this . register _button = function ( command , id , type , act , sel , over )
{
var button _prop = { id : id , type : type } ;
if ( act ) button _prop . act = act ;
if ( sel ) button _prop . sel = sel ;
if ( over ) button _prop . over = over ;
if ( ! this . buttons [ command ] )
this . buttons [ command ] = [ ] ;
this . buttons [ command ] . push ( button _prop ) ;
if ( this . loaded ) {
this . init _button ( command , button _prop ) ;
this . set _button ( command , ( this . commands [ command ] ? 'act' : 'pas' ) ) ;
}
} ;
// register a button with popup menu, to set its state according to the state of all commands in the menu
this . register _menu _button = function ( button , menu _id )
{
if ( this . menu _buttons [ menu _id ] ) {
this . menu _buttons [ menu _id ] [ 0 ] . push ( button ) ;
}
else {
var commands = [ ] ;
$ ( '#' + menu _id ) . find ( 'a' ) . each ( function ( ) {
var command , link = $ ( this ) , onclick = link . attr ( 'onclick' ) ;
if ( onclick && String ( onclick ) . match ( /rcmail\.command\(\'([^']+)/ ) )
command = RegExp . $1 ;
else
command = function ( ) { return link . is ( '.active' ) ; } ;
commands . push ( command ) ;
} ) ;
if ( commands . length )
this . menu _buttons [ menu _id ] = [ [ button ] , commands ] ;
}
this . set _menu _buttons ( ) ;
} ;
// set state of a menu button according to state of all menu actions
this . set _menu _buttons = function ( )
{
// Use timeouts to not block and set menu button states only once
clearTimeout ( this . menu _buttons _timeout ) ;
this . menu _buttons _timeout = setTimeout ( function ( ) {
$ . each ( ref . menu _buttons , function ( ) {
var disabled = true ;
$ . each ( this [ 1 ] , function ( ) {
var is _func = typeof ( this ) == 'function' ;
if ( ( is _func && this ( ) ) || ( ! is _func && ref . commands [ this ] ) ) {
return disabled = false ;
}
} ) ;
$ ( this [ 0 ] ) . add ( $ ( this [ 0 ] ) . parent ( '.dropbutton' ) )
. addClass ( disabled ? 'disabled' : 'active' )
. removeClass ( disabled ? 'active' : 'disabled' ) ;
} ) ;
} , 50 ) ;
} ;
// register a specific gui object
this . gui _object = function ( name , id )
{
this . gui _objects [ name ] = this . loaded ? rcube _find _object ( id ) : id ;
} ;
// register a container object
this . gui _container = function ( name , id )
{
this . gui _containers [ name ] = id ;
} ;
// add a GUI element (html node) to a specified container
this . add _element = function ( elm , container )
{
if ( this . gui _containers [ container ] && this . gui _containers [ container ] . jquery )
this . gui _containers [ container ] . append ( elm ) ;
} ;
// register an external handler for a certain command
this . register _command = function ( command , callback , enable )
{
this . command _handlers [ command ] = callback ;
if ( enable )
this . enable _command ( command , true ) ;
} ;
// execute the given script on load
this . add _onload = function ( f )
{
this . onloads . push ( f ) ;
} ;
// initialize webmail client
this . init = function ( )
{
var n ;
this . task = this . env . task ;
// check browser capabilities (never use version checks here)
if ( this . env . server _error != 409 && ( ! bw . dom || ! bw . xmlhttp _test ( ) ) ) {
this . goto _url ( 'error' , '_code=0x199' ) ;
return ;
}
if ( ! this . env . blankpage )
this . env . blankpage = 'about:blank' ;
// find all registered gui containers
for ( n in this . gui _containers )
this . gui _containers [ n ] = $ ( '#' + this . gui _containers [ n ] ) ;
// find all registered gui objects
for ( n in this . gui _objects )
this . gui _objects [ n ] = rcube _find _object ( this . gui _objects [ n ] ) ;
// init registered buttons
this . init _buttons ( ) ;
// tell parent window that this frame is loaded
if ( this . is _framed ( ) ) {
parent . rcmail . unlock _frame ( ) ;
}
// enable general commands
this . enable _command ( 'close' , 'logout' , 'mail' , 'addressbook' , 'settings' , 'save-pref' ,
'compose' , 'undo' , 'about' , 'switch-task' , 'menu-open' , 'menu-close' , 'menu-save' , true ) ;
// set active task button
this . set _button ( this . task , 'sel' ) ;
if ( this . env . permaurl )
this . enable _command ( 'permaurl' , 'extwin' , true ) ;
switch ( this . task ) {
case 'mail' :
// enable mail commands
this . enable _command ( 'list' , 'checkmail' , 'add-contact' , 'search' , 'reset-search' , 'collapse-folder' , 'import-messages' , true ) ;
if ( this . gui _objects . messagelist ) {
this . env . widescreen _list _template = [
{ className : 'threads' , cells : [ 'threads' ] } ,
{ className : 'subject' , cells : [ 'fromto' , 'date' , 'status' , 'subject' ] } ,
{ className : 'flags' , cells : [ 'flag' , 'attachment' ] }
] ;
this . message _list = new rcube _list _widget ( this . gui _objects . messagelist , {
multiselect : true , multiexpand : true , draggable : true , keyboard : true ,
column _movable : this . env . col _movable , dblclick _time : this . dblclick _time
} ) ;
this . message _list
. addEventListener ( 'initrow' , function ( o ) { ref . init _message _row ( o ) ; } )
. addEventListener ( 'dblclick' , function ( o ) { ref . msglist _dbl _click ( o ) ; } )
. addEventListener ( 'keypress' , function ( o ) { ref . msglist _keypress ( o ) ; } )
. addEventListener ( 'select' , function ( o ) { ref . msglist _select ( o ) ; } )
. addEventListener ( 'dragstart' , function ( o ) { ref . drag _start ( o ) ; } )
. addEventListener ( 'dragmove' , function ( e ) { ref . drag _move ( e ) ; } )
. addEventListener ( 'dragend' , function ( e ) { ref . drag _end ( e ) ; } )
. addEventListener ( 'expandcollapse' , function ( o ) { ref . msglist _expand ( o ) ; } )
. addEventListener ( 'column_replace' , function ( o ) { ref . msglist _set _coltypes ( o ) ; } )
. init ( ) ;
// TODO: this should go into the list-widget code
$ ( this . message _list . thead ) . on ( 'click' , 'a.sortcol' , function ( e ) {
return ref . command ( 'sort' , $ ( this ) . attr ( 'rel' ) , this ) ;
} ) ;
this . enable _command ( 'toggle_status' , 'toggle_flag' , 'sort' , true ) ;
this . enable _command ( 'set-listmode' , this . env . threads && ! this . is _multifolder _listing ( ) ) ;
// load messages
var searchfilter = $ ( this . gui _objects . search _filter ) . val ( ) ;
if ( searchfilter && searchfilter != 'ALL' )
this . filter _mailbox ( searchfilter ) ;
else
this . command ( 'list' ) ;
$ ( this . gui _objects . qsearchbox ) . val ( this . env . search _text ) . focusin ( function ( ) { ref . message _list . blur ( ) ; } ) ;
}
this . set _button _titles ( ) ;
this . env . message _commands = [ 'show' , 'reply' , 'reply-all' , 'reply-list' ,
'move' , 'copy' , 'delete' , 'open' , 'mark' , 'edit' , 'viewsource' , 'bounce' ,
'print' , 'load-attachment' , 'download-attachment' , 'show-headers' , 'hide-headers' , 'download' ,
'forward' , 'forward-inline' , 'forward-attachment' , 'change-format' ] ;
if ( this . env . action == 'show' || this . env . action == 'preview' ) {
this . enable _command ( this . env . message _commands , this . env . uid ) ;
this . enable _command ( 'reply-list' , this . env . list _post ) ;
if ( this . env . action == 'show' ) {
this . http _request ( 'pagenav' , { _uid : this . env . uid , _mbox : this . env . mailbox , _search : this . env . search _request } ,
this . display _message ( '' , 'loading' ) ) ;
}
if ( this . env . mail _read _time > 0 )
setTimeout ( function ( ) {
ref . http _post ( 'mark' , { _uid : ref . env . uid , _flag : 'read' , _mbox : ref . env . mailbox , _quiet : 1 } ) ;
} , this . env . mail _read _time * 1000 ) ;
if ( this . env . blockedobjects ) {
$ ( this . gui _objects . remoteobjectsmsg ) . show ( ) ;
this . enable _command ( 'load-remote' , true ) ;
}
// make preview/message frame visible
if ( this . env . action == 'preview' && this . is _framed ( ) ) {
this . enable _command ( 'compose' , 'add-contact' , false ) ;
parent . rcmail . show _contentframe ( true ) ;
}
if ( $ . inArray ( 'flagged' , this . env . message _flags ) >= 0 ) {
$ ( document . body ) . addClass ( 'status-flagged' ) ;
}
// initialize drag-n-drop on attachments, so they can e.g.
// be dropped into mail compose attachments in another window
if ( this . gui _objects . attachments )
$ ( 'li > a' , this . gui _objects . attachments ) . not ( '.drop' ) . on ( 'dragstart' , function ( e ) {
var n , href = this . href , dt = e . originalEvent . dataTransfer ;
if ( dt ) {
// inject username to the uri
href = href . replace ( /^https?:\/\// , function ( m ) { return m + urlencode ( ref . env . username ) + '@' } ) ;
// cleanup the node to get filename without the size test
n = $ ( this ) . clone ( ) ;
n . children ( ) . remove ( ) ;
dt . setData ( 'roundcube-uri' , href ) ;
dt . setData ( 'roundcube-name' , n . text ( ) . trim ( ) ) ;
}
} ) ;
}
else if ( this . env . action == 'compose' ) {
this . env . address _group _stack = [ ] ;
this . env . compose _commands = [ 'send-attachment' , 'remove-attachment' , 'send' , 'cancel' ,
'toggle-editor' , 'list-addresses' , 'pushgroup' , 'search' , 'reset-search' , 'extwin' ,
'insert-response' , 'save-response' , 'menu-open' , 'menu-close' , 'load-attachment' ,
'download-attachment' , 'open-attachment' , 'rename-attachment' ] ;
if ( this . env . drafts _mailbox )
this . env . compose _commands . push ( 'savedraft' )
this . enable _command ( this . env . compose _commands , true ) ;
// add more commands (not enabled)
$ . merge ( this . env . compose _commands , [ 'add-recipient' , 'firstpage' , 'previouspage' , 'nextpage' , 'lastpage' ] ) ;
if ( window . googie ) {
this . env . editor _config . spellchecker = googie ;
this . env . editor _config . spellcheck _observer = function ( s ) { ref . spellcheck _state ( ) ; } ;
this . env . compose _commands . push ( 'spellcheck' )
this . enable _command ( 'spellcheck' , true ) ;
}
// initialize HTML editor
this . editor _init ( this . env . editor _config , this . env . composebody ) ;
// init canned response functions
if ( this . gui _objects . responseslist ) {
$ ( 'a.insertresponse' , this . gui _objects . responseslist )
. attr ( 'unselectable' , 'on' )
. mousedown ( function ( e ) { return rcube _event . cancel ( e ) ; } )
. on ( 'mouseup keypress' , function ( e ) {
if ( e . type == 'mouseup' || rcube _event . get _keycode ( e ) == 13 ) {
ref . command ( 'insert-response' , $ ( this ) . attr ( 'rel' ) ) ;
$ ( document . body ) . trigger ( 'mouseup' ) ; // hides the menu
return rcube _event . cancel ( e ) ;
}
} ) ;
// avoid textarea loosing focus when hitting the save-response button/link
$ . each ( this . buttons [ 'save-response' ] || [ ] , function ( i , v ) {
$ ( '#' + v . id ) . mousedown ( function ( e ) { return rcube _event . cancel ( e ) ; } )
} ) ;
}
// init message compose form
this . init _messageform ( ) ;
}
else if ( this . env . action == 'bounce' ) {
this . init _messageform _inputs ( ) ;
this . env . compose _commands = [ ] ;
}
else if ( this . env . action == 'get' ) {
this . enable _command ( 'download' , true ) ;
this . enable _command ( 'image-scale' , 'image-rotate' , ! ! /^image\// . test ( this . env . mimetype ) ) ;
// Mozilla's PDF.js viewer does not allow printing from host page (#5125)
// to minimize user confusion we disable the Print button on Firefox < 75
this . enable _command ( 'print' , this . env . mimetype != 'application/pdf' || ! bw . mz || bw . vendver >= 75 ) ;
if ( this . env . is _message ) {
this . enable _command ( 'reply' , 'reply-all' , 'edit' , 'viewsource' ,
'forward' , 'forward-inline' , 'forward-attachment' , 'bounce' , true ) ;
if ( this . env . list _post )
this . enable _command ( 'reply-list' , true ) ;
}
// center and scale the image in preview frame
// TODO: Find a better way. Onload is late, also we could use embed.css
if ( this . env . mimetype . startsWith ( 'image/' ) )
$ ( this . gui _objects . messagepartframe ) . on ( 'load' , function ( ) {
var contents = $ ( this ) . contents ( ) ;
// do not apply styles to an error page (with no image)
if ( contents . find ( 'img' ) . length )
contents . find ( 'head' ) . append (
'<style type="text/css">'
+ 'img { max-width:100%; max-height:100%; } ' // scale
+ 'body { display:flex; align-items:center; justify-content:center; height:100%; margin:0; }' // align
+ '</style>'
) ;
} ) ;
}
// show printing dialog
else if ( this . env . action == 'print' && this . env . uid
&& ! this . env . is _pgp _content && ! this . env . pgp _mime _part
) {
this . print _dialog ( ) ;
}
// get unread count for each mailbox
if ( this . gui _objects . mailboxlist ) {
this . env . unread _counts = { } ;
this . gui _objects . folderlist = this . gui _objects . mailboxlist ;
this . http _request ( 'getunread' , { _page : this . env . current _page } ) ;
}
// init address book widget
if ( this . gui _objects . contactslist ) {
this . contact _list = new rcube _list _widget ( this . gui _objects . contactslist ,
{ multiselect : true , draggable : false , keyboard : true } ) ;
this . contact _list
. addEventListener ( 'initrow' , function ( o ) { ref . triggerEvent ( 'insertrow' , { cid : o . uid , row : o } ) ; } )
. addEventListener ( 'select' , function ( o ) { ref . compose _recipient _select ( o ) ; } )
. addEventListener ( 'dblclick' , function ( o ) { ref . compose _add _recipient ( ) ; } )
. addEventListener ( 'keypress' , function ( o ) {
if ( o . key _pressed == o . ENTER _KEY ) {
if ( ! ref . compose _add _recipient ( ) ) {
// execute link action on <enter> if not a recipient entry
if ( o . last _selected && String ( o . last _selected ) . charAt ( 0 ) == 'G' ) {
$ ( o . rows [ o . last _selected ] . obj ) . find ( 'a' ) . first ( ) . click ( ) ;
}
}
}
} )
. init ( ) ;
// remember last focused address field
$ ( '#_to,#_cc,#_bcc' ) . focus ( function ( ) { ref . env . focused _field = this ; } ) ;
}
if ( this . gui _objects . addressbookslist ) {
this . gui _objects . folderlist = this . gui _objects . addressbookslist ;
this . enable _command ( 'list-addresses' , true ) ;
}
// ask user to send MDN
if ( this . env . mdn _request && this . env . uid ) {
var postact = 'sendmdn' ,
postdata = { _uid : this . env . uid , _mbox : this . env . mailbox } ;
if ( ! confirm ( this . get _label ( 'mdnrequest' ) ) ) {
postdata . _flag = 'mdnsent' ;
postact = 'mark' ;
}
this . http _post ( postact , postdata ) ;
}
this . check _mailvelope ( this . env . action ) ;
// detect browser capabilities
if ( ! this . is _framed ( ) && ! this . env . extwin )
this . browser _capabilities _check ( ) ;
break ;
case 'addressbook' :
this . env . address _group _stack = [ ] ;
if ( this . gui _objects . folderlist )
this . env . contactfolders = $ . extend ( $ . extend ( { } , this . env . address _sources ) , this . env . contactgroups ) ;
this . enable _command ( 'add' , 'import' , this . env . writable _source ) ;
this . enable _command ( 'list' , 'listgroup' , 'pushgroup' , 'popgroup' , 'listsearch' , 'search' , 'reset-search' , 'advanced-search' , true ) ;
if ( this . gui _objects . contactslist ) {
this . contact _list = new rcube _list _widget ( this . gui _objects . contactslist ,
{ multiselect : true , draggable : this . gui _objects . folderlist ? true : false , keyboard : true } ) ;
this . contact _list
. addEventListener ( 'initrow' , function ( o ) { ref . triggerEvent ( 'insertrow' , { cid : o . uid , row : o } ) ; } )
. addEventListener ( 'keypress' , function ( o ) { ref . list _keypress ( o ) ; } )
. addEventListener ( 'select' , function ( o ) { ref . contactlist _select ( o ) ; } )
. addEventListener ( 'dragstart' , function ( o ) { ref . drag _start ( o ) ; } )
. addEventListener ( 'dragmove' , function ( e ) { ref . drag _move ( e ) ; } )
. addEventListener ( 'dragend' , function ( e ) { ref . drag _end ( e ) ; } )
. init ( ) ;
$ ( this . gui _objects . qsearchbox ) . focusin ( function ( ) { ref . contact _list . blur ( ) ; } ) ;
this . update _group _commands ( ) ;
this . command ( 'list' ) ;
}
if ( this . gui _objects . savedsearchlist ) {
this . savedsearchlist = new rcube _treelist _widget ( this . gui _objects . savedsearchlist , {
id _prefix : 'rcmli' ,
id _encode : this . html _identifier _encode ,
id _decode : this . html _identifier _decode
} ) ;
this . savedsearchlist . addEventListener ( 'select' , function ( node ) {
ref . triggerEvent ( 'selectfolder' , { folder : node . id , prefix : 'rcmli' } ) ; } ) ;
}
this . set _page _buttons ( ) ;
if ( this . env . cid ) {
this . enable _command ( 'show' , 'edit' , 'qrcode' , true ) ;
// register handlers for group assignment via checkboxes
if ( this . gui _objects . editform ) {
$ ( 'input.groupmember' ) . change ( function ( ) {
ref . group _member _change ( this . checked ? 'add' : 'del' , ref . env . cid , ref . env . source , this . value ) ;
} ) ;
}
}
if ( this . gui _objects . editform ) {
this . enable _command ( 'save' , true ) ;
if ( this . env . action == 'add' || this . env . action == 'edit' || this . env . action == 'search' )
this . init _contact _form ( ) ;
}
else if ( this . env . action == 'print' ) {
this . print _dialog ( ) ;
}
break ;
case 'settings' :
this . enable _command ( 'show' , 'save' , true ) ;
if ( this . env . action == 'identities' ) {
this . enable _command ( 'add' , this . env . identities _level < 2 ) ;
}
else if ( this . env . action == 'edit-identity' || this . env . action == 'add-identity' ) {
this . enable _command ( 'save' , 'edit' , 'toggle-editor' , true ) ;
this . enable _command ( 'delete' , this . env . identities _level < 2 ) ;
// initialize HTML editor
this . editor _init ( this . env . editor _config , 'rcmfd_signature' ) ;
this . check _mailvelope ( this . env . action ) ;
}
else if ( this . env . action == 'folders' ) {
this . enable _command ( 'subscribe' , 'unsubscribe' , 'create-folder' , 'rename-folder' , true ) ;
}
else if ( this . env . action == 'edit-folder' && this . gui _objects . editform ) {
this . enable _command ( 'save' , 'folder-size' , true ) ;
parent . rcmail . env . exists = this . env . messagecount ;
parent . rcmail . enable _command ( 'purge' , this . env . messagecount ) ;
}
else if ( this . env . action == 'responses' ) {
this . enable _command ( 'add' , true ) ;
}
if ( this . gui _objects . identitieslist ) {
this . identity _list = new rcube _list _widget ( this . gui _objects . identitieslist ,
{ multiselect : false , draggable : false , keyboard : true } ) ;
this . identity _list
. addEventListener ( 'select' , function ( o ) { ref . identity _select ( o ) ; } )
. addEventListener ( 'keypress' , function ( o ) { ref . list _keypress ( o ) ; } )
. init ( )
. focus ( ) ;
}
else if ( this . gui _objects . sectionslist ) {
this . sections _list = new rcube _list _widget ( this . gui _objects . sectionslist , { multiselect : false , draggable : false , keyboard : true } ) ;
this . sections _list
. addEventListener ( 'select' , function ( o ) { ref . section _select ( o ) ; } )
. init ( )
. focus ( ) ;
}
else if ( this . gui _objects . subscriptionlist ) {
this . init _subscription _list ( ) ;
}
else if ( this . gui _objects . responseslist ) {
this . responses _list = new rcube _list _widget ( this . gui _objects . responseslist , { multiselect : false , draggable : false , keyboard : true } ) ;
this . responses _list
. addEventListener ( 'select' , function ( o ) { ref . response _select ( o ) ; } )
. addEventListener ( 'keypress' , function ( o ) { ref . list _keypress ( o ) ; } )
. init ( )
. focus ( ) ;
}
break ;
case 'login' :
var tz , tz _name ,
input _user = $ ( '#rcmloginuser' ) ,
input _tz = $ ( '#rcmlogintz' ) ;
if ( input _user . val ( ) == '' )
input _user . focus ( ) ;
else
$ ( '#rcmloginpwd' ) . focus ( ) ;
// detect client timezone
if ( window . jstz && ( tz = jstz . determine ( ) ) )
tz _name = tz . name ( ) ;
input _tz . val ( tz _name ? tz _name : ( new Date ( ) . getStdTimezoneOffset ( ) / - 60 ) ) ;
// display 'loading' message on form submit, lock submit button
$ ( 'form' ) . submit ( function ( ) {
$ ( '[type=submit]' , this ) . prop ( 'disabled' , true ) ;
ref . clear _messages ( ) ;
ref . display _message ( '' , 'loading' ) ;
} ) ;
break ;
}
// select first input field in an edit form
if ( this . gui _objects . editform )
$ ( "input,select,textarea" , this . gui _objects . editform )
. not ( ':hidden' ) . not ( ':disabled' ) . first ( ) . select ( ) . focus ( ) ;
// prevent from form submit with Enter key in file input fields
if ( bw . ie )
$ ( 'input[type=file]' ) . keydown ( function ( e ) { if ( e . keyCode == '13' ) e . preventDefault ( ) ; } ) ;
// flag object as complete
this . loaded = true ;
this . env . lastrefresh = new Date ( ) ;
// show message
if ( this . pending _message )
this . display _message . apply ( this , this . pending _message ) ;
// init treelist widget
if ( this . gui _objects . folderlist && window . rcube _treelist _widget
// some plugins may load rcube_treelist_widget and there's one case
// when this will cause problems - addressbook widget in compose,
// which already has been initialized using rcube_list_widget
&& this . gui _objects . folderlist != this . gui _objects . addressbookslist
) {
this . treelist = new rcube _treelist _widget ( this . gui _objects . folderlist , {
selectable : true ,
id _prefix : 'rcmli' ,
parent _focus : true ,
id _encode : this . html _identifier _encode ,
id _decode : this . html _identifier _decode ,
check _droptarget : function ( node ) { return ! node . virtual && ref . check _droptarget ( node . id ) }
} ) ;
this . treelist
. addEventListener ( 'collapse' , function ( node ) { ref . folder _collapsed ( node ) } )
. addEventListener ( 'expand' , function ( node ) { ref . folder _collapsed ( node ) } )
. addEventListener ( 'beforeselect' , function ( node ) { return ! ref . busy ; } )
. addEventListener ( 'select' , function ( node ) {
ref . triggerEvent ( 'selectfolder' , { folder : node . id , prefix : 'rcmli' } ) ;
ref . mark _all _read _state ( ) ;
} ) ;
}
// activate html5 file drop feature (if browser supports it and if configured)
if ( this . gui _objects . filedrop && this . env . filedrop && window . FormData ) {
$ ( document . body ) . on ( 'dragover dragleave drop' , function ( e ) { return ref . document _drag _hover ( e , e . type == 'dragover' ) ; } ) ;
$ ( this . gui _objects . filedrop ) . addClass ( 'droptarget' )
. on ( 'dragover dragleave' , function ( e ) { return ref . file _drag _hover ( e , e . type == 'dragover' ) ; } )
. get ( 0 ) . addEventListener ( 'drop' , function ( e ) { return ref . file _dropped ( e ) ; } , false ) ;
}
// catch document (and iframe) mouse clicks
var body _mouseup = function ( e ) { return ref . doc _mouse _up ( e ) ; } ;
$ ( document . body )
. mouseup ( body _mouseup )
. keydown ( function ( e ) { return ref . doc _keypress ( e ) ; } ) ;
rcube _webmail . set _iframe _events ( { mouseup : body _mouseup } ) ;
// trigger init event hook
this . triggerEvent ( 'init' , { task : this . task , action : this . env . action } ) ;
// execute all foreign onload scripts
// @deprecated
for ( n in this . onloads ) {
if ( typeof this . onloads [ n ] === 'string' )
eval ( this . onloads [ n ] ) ;
else if ( typeof this . onloads [ n ] === 'function' )
this . onloads [ n ] ( ) ;
}
// register menu buttons
$ ( '[data-popup]' ) . each ( function ( ) { ref . register _menu _button ( this , $ ( this ) . data ( 'popup' ) ) ; } ) ;
// start keep-alive and refresh intervals
this . start _refresh ( ) ;
this . start _keepalive ( ) ;
} ;
this . log = function ( msg )
{
if ( this . env . devel _mode && window . console && console . log )
console . log ( msg ) ;
} ;
/*********************************************************/
/********* client command interface *********/
/*********************************************************/
// execute a specific command on the web client
this . command = function ( command , props , obj , event , allow _disabled )
{
var ret ;
if ( obj && obj . blur && ! ( event && rcube _event . is _keyboard ( event ) ) )
obj . blur ( ) ;
// do nothing if interface is locked by another command
// with exception for searching reset and menu
if ( this . busy && ! ( command == 'reset-search' && this . last _command == 'search' ) && ! command . match ( /^menu-/ ) )
return false ;
// let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
if ( ( obj && obj . href && String ( obj . href ) . indexOf ( '#' ) < 0 ) && rcube _event . get _modifier ( event ) ) {
return true ;
}
// command not supported or allowed
if ( ! allow _disabled && ! this . commands [ command ] ) {
// pass command to parent window
if ( this . is _framed ( ) )
parent . rcmail . command ( command , props ) ;
return false ;
}
// check input before leaving compose step
if ( this . task == 'mail' && this . env . action == 'compose' && ! this . env . server _error && command != 'save-pref'
&& $ . inArray ( command , this . env . compose _commands ) < 0 && ! this . compose _skip _unsavedcheck
) {
if ( ! this . env . is _sent && this . cmp _hash != this . compose _field _hash ( ) ) {
this . confirm _dialog ( this . get _label ( 'notsentwarning' ) , 'discard' , function ( ) {
// remove copy from local storage if compose screen is left intentionally
ref . remove _compose _data ( ref . env . compose _id ) ;
ref . compose _skip _unsavedcheck = true ;
ref . command ( command , props , obj , event ) ;
} ) ;
return false ;
}
}
this . last _command = command ;
this . command _aborted = false ;
// trigger plugin hooks
this . triggerEvent ( 'actionbefore' , { props : props , action : command , originalEvent : event } ) ;
if ( ( ret = this . triggerEvent ( 'before' + command , props || event ) ) !== undefined ) {
// abort if one of the handlers returned false
if ( ret === false )
return false ;
props = ret ;
}
// process external commands
if ( typeof this . command _handlers [ command ] === 'function' ) {
ret = this . command _handlers [ command ] ( props , obj , event ) ;
}
else if ( typeof this . command _handlers [ command ] === 'string' ) {
ret = window [ this . command _handlers [ command ] ] ( props , obj , event ) ;
}
// process internal commands
else {
ret = this . command _handler ( command , props , obj , event ) ;
}
if ( ! this . command _aborted && this . triggerEvent ( 'after' + command , props ) === false )
ret = false ;
this . triggerEvent ( 'actionafter' , { props : props , action : command , aborted : this . command _aborted , ret : ret , originalEvent : event } ) ;
if ( ret === false )
return false ;
if ( ( obj && ret !== true ) || this . command _aborted === true )
return false ;
return true ;
} ;
// execute a specific known command
this . command _handler = function ( command , props , obj , event )
{
var uid , cid , url , flag ;
// process internal command
switch ( command ) {
// commands to switch task
case 'logout' :
case 'mail' :
case 'addressbook' :
case 'settings' :
this . switch _task ( command ) ;
break ;
case 'about' :
this . redirect ( '?_task=settings&_action=about' , false ) ;
break ;
case 'permaurl' :
if ( obj && obj . href && obj . target )
return true ;
if ( this . env . permaurl )
parent . location . href = this . env . permaurl ;
break ;
case 'extwin' :
if ( this . env . action == 'compose' ) {
var form = this . gui _objects . messageform ,
win = this . open _window ( '' ) ;
if ( win ) {
this . save _compose _form _local ( ) ;
this . compose _skip _unsavedcheck = true ;
$ ( "[name='_action']" , form ) . val ( 'compose' ) ;
form . action = this . url ( 'mail/compose' , { _id : this . env . compose _id , _extwin : 1 } ) ;
form . target = win . name ;
form . submit ( ) ;
}
}
else {
this . open _window ( this . env . permaurl , true ) ;
}
break ;
case 'change-format' :
url = this . env . permaurl + '&_format=' + props ;
if ( this . env . action == 'preview' )
url = url . replace ( /_action=show/ , '_action=preview' ) + '&_framed=1' ;
if ( this . env . extwin )
url += '&_extwin=1' ;
location . href = url ;
break ;
case 'menu-open' :
if ( props && props . menu == 'attachmentmenu' ) {
var mimetype = this . env . attachments [ props . id ] ;
if ( mimetype && mimetype . mimetype ) // in compose format is different
mimetype = mimetype . mimetype ;
this . enable _command ( 'open-attachment' , mimetype && this . env . mimetypes && $ . inArray ( mimetype , this . env . mimetypes ) >= 0 ) ;
}
this . show _menu ( props , props . show || undefined , event ) ;
break ;
case 'menu-close' :
this . hide _menu ( props , event ) ;
break ;
case 'menu-save' :
this . triggerEvent ( command , { props : props , originalEvent : event } ) ;
return false ;
case 'open' :
if ( uid = this . get _single _uid ( ) ) {
obj . href = this . url ( 'show' , this . params _from _uid ( uid , { _extwin : 1 } ) ) ;
return true ;
}
break ;
case 'close' :
if ( this . env . extwin )
window . close ( ) ;
break ;
case 'list' :
if ( props && props != '' ) {
this . reset _qsearch ( true ) ;
}
if ( this . env . action == 'compose' && this . env . extwin ) {
window . close ( ) ;
}
else if ( this . task == 'mail' ) {
this . list _mailbox ( props ) ;
this . set _button _titles ( ) ;
}
else if ( this . task == 'addressbook' )
this . list _contacts ( props ) ;
break ;
case 'set-listmode' :
this . set _list _options ( null , undefined , undefined , props == 'threads' ? 1 : 0 ) ;
break ;
case 'sort' :
var sort _order = this . env . sort _order ,
sort _col = ! this . env . disabled _sort _col ? props : this . env . sort _col ;
if ( ! this . env . disabled _sort _order )
sort _order = this . env . sort _col == sort _col && sort _order == 'ASC' ? 'DESC' : 'ASC' ;
// set table header and update env
this . set _list _sorting ( sort _col , sort _order ) ;
// reload message list
this . list _mailbox ( '' , '' , sort _col + '_' + sort _order ) ;
break ;
case 'nextpage' :
this . list _page ( 'next' ) ;
break ;
case 'lastpage' :
this . list _page ( 'last' ) ;
break ;
case 'previouspage' :
this . list _page ( 'prev' ) ;
break ;
case 'firstpage' :
this . list _page ( 'first' ) ;
break ;
case 'expunge' :
if ( this . env . exists )
this . expunge _mailbox ( this . env . mailbox ) ;
break ;
case 'purge' :
case 'empty-mailbox' :
if ( this . env . exists )
this . purge _mailbox ( this . env . mailbox ) ;
break ;
// common commands used in multiple tasks
case 'show' :
if ( this . task == 'mail' ) {
uid = this . get _single _uid ( ) ;
if ( uid && ( ! this . env . uid || uid != this . env . uid ) ) {
var mbox = this . get _message _mailbox ( uid ) ;
if ( mbox == this . env . drafts _mailbox )
this . open _compose _step ( { _draft _uid : uid , _mbox : mbox } ) ;
else
this . show _message ( uid ) ;
}
}
else if ( this . task == 'addressbook' ) {
cid = props ? props : this . get _single _cid ( ) ;
if ( cid && ! ( this . env . action == 'show' && cid == this . env . cid ) )
this . load _contact ( cid , 'show' ) ;
}
else if ( this . task == 'settings' ) {
this . goto _url ( 'settings/' + props , { _framed : 0 } ) ;
}
break ;
case 'add' :
if ( this . task == 'addressbook' )
this . load _contact ( 0 , 'add' ) ;
else if ( this . task == 'settings' && this . env . action == 'responses' )
this . load _response ( 0 , 'add-response' ) ;
else if ( this . task == 'settings' )
this . load _identity ( 0 , 'add-identity' ) ;
break ;
case 'edit' :
if ( this . task == 'addressbook' && ( cid = this . get _single _cid ( ) ) )
this . load _contact ( cid , 'edit' ) ;
else if ( this . task == 'mail' && ( uid = this . get _single _uid ( ) ) ) {
url = { _mbox : this . get _message _mailbox ( uid ) } ;
url [ url . _mbox == this . env . drafts _mailbox && props != 'new' ? '_draft_uid' : '_uid' ] = uid ;
this . open _compose _step ( url ) ;
}
break ;
case 'save' :
var input , form = this . gui _objects . editform ;
if ( form ) {
// user prefs
if ( ( input = $ ( "[name='_pagesize']" , form ) ) && input . length && isNaN ( parseInt ( input . val ( ) ) ) ) {
this . alert _dialog ( this . get _label ( 'nopagesizewarning' ) , function ( ) {
input . focus ( ) ;
} ) ;
break ;
}
// contacts/identities
else {
// reload form
if ( props == 'reload' ) {
form . action += '&_reload=1' ;
}
else if ( this . task == 'settings' && ( this . env . identities _level % 2 ) == 0 &&
( input = $ ( "[name='_email']" , form ) ) && input . length && ! rcube _check _email ( input . val ( ) )
) {
this . alert _dialog ( this . get _label ( 'noemailwarning' ) , function ( ) {
input . focus ( ) ;
} ) ;
break ;
}
}
// add selected source (on the list)
if ( parent . rcmail && parent . rcmail . env . source )
form . action = this . add _url ( form . action , '_orig_source' , parent . rcmail . env . source ) ;
form . submit ( ) ;
}
break ;
case 'delete' :
// mail task
if ( this . task == 'mail' )
this . delete _messages ( event ) ;
// addressbook task
else if ( this . task == 'addressbook' )
this . delete _contacts ( ) ;
// settings: canned response
else if ( this . task == 'settings' && this . env . action == 'responses' )
this . delete _response ( ) ;
// settings: user identities
else if ( this . task == 'settings' )
this . delete _identity ( ) ;
break ;
// mail task commands
case 'move' :
case 'moveto' : // deprecated
if ( this . task == 'mail' )
this . move _messages ( props , event ) ;
else if ( this . task == 'addressbook' )
this . move _contacts ( props , event ) ;
break ;
case 'copy' :
if ( this . task == 'mail' )
this . copy _messages ( props , event ) ;
else if ( this . task == 'addressbook' )
this . copy _contacts ( props , event ) ;
break ;
case 'mark' :
if ( props )
this . mark _message ( props ) ;
break ;
case 'toggle_status' :
case 'toggle_flag' :
flag = command == 'toggle_flag' ? 'flagged' : 'read' ;
if ( uid = props ) {
// toggle flagged/unflagged
if ( flag == 'flagged' ) {
if ( this . message _list . rows [ uid ] . flagged )
flag = 'unflagged' ;
}
// toggle read/unread
else if ( this . message _list . rows [ uid ] . deleted )
flag = 'undelete' ;
else if ( ! this . message _list . rows [ uid ] . unread )
flag = 'unread' ;
this . mark _message ( flag , uid ) ;
}
break ;
case 'add-contact' :
this . add _contact ( props ) ;
break ;
case 'load-remote' :
if ( this . env . uid ) {
if ( props && this . env . sender ) {
this . add _contact ( this . env . sender , true ) ;
break ;
}
this . show _message ( this . env . uid , true , this . env . action == 'preview' ) ;
}
break ;
case 'load-attachment' :
case 'open-attachment' :
case 'download-attachment' :
var params , mimetype = this . env . attachments [ props ] ;
if ( this . env . action == 'compose' ) {
params = { _file : props , _id : this . env . compose _id } ;
mimetype = mimetype ? mimetype . mimetype : '' ;
}
else {
params = { _mbox : this . env . mailbox , _uid : this . env . uid , _part : props } ;
}
// open attachment in frame if it's of a supported mimetype
if ( command != 'download-attachment' && mimetype && this . env . mimetypes && $ . inArray ( mimetype , this . env . mimetypes ) >= 0 ) {
// Note: We disable _framed for proper X-Frame-Options:deny support (#6688)
if ( this . open _window ( this . url ( 'get' , $ . extend ( { _frame : 1 , _framed : 0 } , params ) ) ) )
break ;
}
params . _download = 1 ;
// prevent from page unload warning in compose
this . compose _skip _unsavedcheck = 1 ;
this . goto _url ( 'get' , params , false , true ) ;
this . compose _skip _unsavedcheck = 0 ;
break ;
case 'select-all' :
this . select _all _mode = props ? false : true ;
this . dummy _select = true ; // prevent msg opening if there's only one msg on the list
var list = this [ this . task == 'addressbook' ? 'contact_list' : 'message_list' ] ;
if ( props == 'invert' )
list . invert _selection ( ) ;
else
list . select _all ( props == 'page' ? '' : props ) ;
this . dummy _select = null ;
break ;
case 'select-none' :
this . select _all _mode = false ;
this [ this . task == 'addressbook' ? 'contact_list' : 'message_list' ] . clear _selection ( ) ;
break ;
case 'expand-all' :
this . env . autoexpand _threads = 1 ;
this . message _list . expand _all ( ) ;
break ;
case 'expand-unread' :
this . env . autoexpand _threads = 2 ;
this . message _list . collapse _all ( ) ;
this . expand _unread ( ) ;
break ;
case 'collapse-all' :
this . env . autoexpand _threads = 0 ;
this . message _list . collapse _all ( ) ;
break ;
case 'nextmessage' :
if ( this . env . next _uid )
this . show _message ( this . env . next _uid , false , this . env . action == 'preview' ) ;
break ;
case 'lastmessage' :
if ( this . env . last _uid )
this . show _message ( this . env . last _uid ) ;
break ;
case 'previousmessage' :
if ( this . env . prev _uid )
this . show _message ( this . env . prev _uid , false , this . env . action == 'preview' ) ;
break ;
case 'firstmessage' :
if ( this . env . first _uid )
this . show _message ( this . env . first _uid ) ;
break ;
case 'compose' :
url = { } ;
if ( this . task == 'mail' ) {
url = { _mbox : this . env . mailbox , _search : this . env . search _request } ;
if ( props )
url . _to = props ;
}
// modify url if we're in addressbook
else if ( this . task == 'addressbook' ) {
// switch to mail compose step directly
if ( props && props . indexOf ( '@' ) > 0 ) {
url . _to = props ;
}
else {
var a _cids = [ ] ;
// use contact id passed as command parameter
if ( props )
a _cids . push ( props ) ;
// get selected contacts
else if ( this . contact _list )
a _cids = this . contact _list . get _selection ( ) ;
if ( a _cids . length ) {
this . http _post ( 'mailto' , { _cid : a _cids . join ( ',' ) , _source : this . env . source } , true ) ;
break ;
}
else if ( this . env . group && this . env . pagecount ) {
this . http _post ( 'mailto' , { _gid : this . env . group , _source : this . env . source } , true ) ;
break ;
}
}
}
else if ( props && typeof props == 'string' ) {
url . _to = props ;
}
else if ( props && typeof props == 'object' ) {
$ . extend ( url , props ) ;
}
this . open _compose _step ( url ) ;
break ;
case 'spellcheck' :
if ( this . spellcheck _state ( ) ) {
this . editor . spellcheck _stop ( ) ;
}
else {
this . editor . spellcheck _start ( ) ;
}
break ;
case 'savedraft' :
// Reset the auto-save timer
clearTimeout ( this . save _timer ) ;
// compose form did not change (and draft wasn't saved already)
if ( this . env . draft _id && this . cmp _hash == this . compose _field _hash ( ) ) {
this . auto _save _start ( ) ;
break ;
}
this . submit _messageform ( true ) ;
break ;
case 'send' :
if ( ! props . nocheck && ! this . env . is _sent && ! this . check _compose _input ( command ) )
break ;
// Reset the auto-save timer
clearTimeout ( this . save _timer ) ;
this . submit _messageform ( ) ;
break ;
case 'send-attachment' :
// Reset the auto-save timer
clearTimeout ( this . save _timer ) ;
if ( ! ( flag = this . upload _file ( props || this . gui _objects . uploadform , 'upload' ) ) ) {
if ( flag !== false )
this . alert _dialog ( this . get _label ( 'selectimportfile' ) ) ;
aborted = true ;
}
break ;
case 'insert-sig' :
this . change _identity ( $ ( "[name='_from']" ) [ 0 ] , true ) ;
break ;
case 'list-addresses' :
this . list _contacts ( props ) ;
this . enable _command ( 'add-recipient' , false ) ;
break ;
case 'add-recipient' :
this . compose _add _recipient ( props ) ;
break ;
case 'reply-all' :
case 'reply-list' :
case 'reply' :
if ( uid = this . get _single _uid ( ) ) {
url = { _reply _uid : uid , _mbox : this . get _message _mailbox ( uid ) , _search : this . env . search _request } ;
if ( command == 'reply-all' )
// do reply-list, when list is detected and popup menu wasn't used
url . _all = ( ! props && this . env . reply _all _mode == 1 && this . commands [ 'reply-list' ] ? 'list' : 'all' ) ;
else if ( command == 'reply-list' )
url . _all = 'list' ;
this . open _compose _step ( url ) ;
}
break ;
case 'forward-attachment' :
case 'forward-inline' :
case 'forward' :
var uids = this . env . uid ? [ this . env . uid ] : ( this . message _list ? this . message _list . get _selection ( ) : [ ] ) ;
if ( uids . length ) {
url = { _forward _uid : this . uids _to _list ( uids ) , _mbox : this . env . mailbox , _search : this . env . search _request } ;
if ( command == 'forward-attachment' || ( ! props && this . env . forward _attachment ) || uids . length > 1 )
url . _attachment = 1 ;
this . open _compose _step ( url ) ;
}
break ;
case 'print' :
if ( this . task == 'addressbook' ) {
if ( uid = this . get _single _cid ( ) ) {
url = '&_action=print&_cid=' + uid ;
if ( this . env . source )
url += '&_source=' + urlencode ( this . env . source ) ;
this . open _window ( this . env . comm _path + url , true , true ) ;
}
}
else if ( this . env . action == 'get' && ! this . env . is _message ) {
this . gui _objects . messagepartframe . contentWindow . print ( ) ;
}
else if ( uid = this . get _single _uid ( ) ) {
url = this . url ( 'print' , this . params _from _uid ( uid , { _safe : this . env . safemode ? 1 : 0 } ) ) ;
if ( this . open _window ( url , true , true ) ) {
if ( this . env . action != 'show' && this . env . action != 'get' )
this . mark _message ( 'read' , uid ) ;
}
}
break ;
case 'viewsource' :
if ( uid = this . get _single _uid ( ) )
this . open _window ( this . url ( 'viewsource' , this . params _from _uid ( uid ) ) , true , true ) ;
break ;
case 'download' :
if ( this . env . action == 'get' ) {
location . href = this . secure _url ( location . href . replace ( /_frame=/ , '_download=' ) ) ;
}
else if ( uid = this . get _single _uid ( ) ) {
this . goto _url ( 'viewsource' , this . params _from _uid ( uid , { _save : 1 } ) , false , true ) ;
}
break ;
// quicksearch
case 'search' :
return this . qsearch ( props ) ;
// reset quicksearch
case 'reset-search' :
var n , s = this . env . search _request || this . env . qsearch ;
this . reset _qsearch ( true ) ;
if ( s && this . env . action == 'compose' ) {
if ( this . contact _list )
this . list _contacts _clear ( ) ;
}
else if ( s && this . env . mailbox ) {
this . list _mailbox ( this . env . mailbox , 1 ) ;
}
else if ( s && this . task == 'addressbook' ) {
if ( this . env . source == '' ) {
for ( n in this . env . address _sources ) break ;
this . env . source = n ;
this . env . group = '' ;
}
this . list _contacts ( this . env . source , this . env . group , 1 ) ;
}
break ;
case 'pushgroup' :
// add group ID and current search to stack
var group = {
id : props . id ,
search _request : this . env . search _request ,
page : this . env . current _page ,
search : this . env . search _request && this . gui _objects . qsearchbox ? this . gui _objects . qsearchbox . value : null
} ;
this . env . address _group _stack . push ( group ) ;
if ( obj && event )
rcube _event . cancel ( event ) ;
// FIXME: no break?
case 'listgroup' :
this . reset _qsearch ( ) ;
this . list _contacts ( props . source , props . id , 1 , group ) ;
break ;
case 'popgroup' :
if ( this . env . address _group _stack . length ) {
var old = this . env . address _group _stack . pop ( ) ;
this . reset _qsearch ( ) ;
if ( old . search _request ) {
// this code is executed when going back to the search result
if ( old . search && this . gui _objects . qsearchbox )
$ ( this . gui _objects . qsearchbox ) . val ( old . search ) ;
this . env . search _request = old . search _request ;
this . list _contacts _remote ( null , null , this . env . current _page = old . page ) ;
}
else
this . list _contacts ( props . source , this . env . address _group _stack [ this . env . address _group _stack . length - 1 ] . id ) ;
}
break ;
case 'import-messages' :
var form = props || this . gui _objects . importform ,
importlock = this . set _busy ( true , 'importwait' ) ;
if ( ! ( flag = this . upload _file ( form , 'import' , importlock ) ) ) {
this . set _busy ( false , null , importlock ) ;
if ( flag !== false )
this . alert _dialog ( this . get _label ( 'selectimportfile' ) ) ;
this . command _aborted = true ;
}
break ;
case 'import' :
var dialog = $ ( '<iframe>' ) . attr ( 'src' , this . url ( 'import' , { _framed : 1 , _target : this . env . source } ) ) ,
import _func = function ( e ) {
var win = dialog [ 0 ] . contentWindow ,
form = win . rcmail . gui _objects . importform ;
if ( form ) {
var lock , file = win . $ ( '#rcmimportfile' ) [ 0 ] ;
if ( file && ! file . value ) {
win . rcmail . alert _dialog ( win . rcmail . get _label ( 'selectimportfile' ) ) ;
return ;
}
lock = win . rcmail . set _busy ( true , 'importwait' ) ;
$ ( '[name="_unlock"]' , form ) . val ( lock ) ;
form . submit ( ) ;
win . rcmail . lock _form ( form , true ) ;
// disable Import button
$ ( e . target ) . attr ( 'disabled' , true ) . next ( ) . focus ( ) ;
}
} ,
close _func = function ( event , ui ) {
$ ( this ) . remove ( ) ;
if ( ref . import _state == 'reload' )
ref . command ( 'list' ) ;
} ;
this . import _state = null ;
this . import _dialog = this . simple _dialog ( dialog , this . gettext ( 'importcontacts' ) , import _func , {
close : close _func ,
button : 'import' ,
width : 500 ,
height : 300
} ) ;
break ;
case 'export' :
if ( this . contact _list . rowcount > 0 ) {
this . goto _url ( 'export' , { _source : this . env . source , _gid : this . env . group , _search : this . env . search _request } , false , true ) ;
}
break ;
case 'export-selected' :
if ( this . contact _list . rowcount > 0 ) {
this . goto _url ( 'export' , { _source : this . env . source , _gid : this . env . group , _cid : this . contact _list . get _selection ( ) . join ( ',' ) } , false , true ) ;
}
break ;
case 'upload-photo' :
this . upload _contact _photo ( props || this . gui _objects . uploadform ) ;
break ;
case 'delete-photo' :
this . replace _contact _photo ( '-del-' ) ;
break ;
case 'undo' :
this . http _request ( 'undo' , '' , this . display _message ( '' , 'loading' ) ) ;
break ;
// unified command call (command name == function name)
default :
var func = command . replace ( /-/g , '_' ) ;
if ( this [ func ] && typeof this [ func ] === 'function' ) {
return this [ func ] ( props , obj , event ) ;
}
}
} ;
// set command(s) enabled or disabled
this . enable _command = function ( )
{
var i , n , args = Array . prototype . slice . call ( arguments ) ,
enable = args . pop ( ) , cmd ;
for ( n = 0 ; n < args . length ; n ++ ) {
cmd = args [ n ] ;
// argument of type array
if ( typeof cmd === 'string' ) {
this . commands [ cmd ] = enable ;
this . set _button ( cmd , ( enable ? 'act' : 'pas' ) ) ;
this . triggerEvent ( 'enable-command' , { command : cmd , status : enable } ) ;
}
// push array elements into commands array
else {
for ( i in cmd )
args . push ( cmd [ i ] ) ;
}
}
this . set _menu _buttons ( ) ;
} ;
this . command _enabled = function ( cmd )
{
return this . commands [ cmd ] ;
} ;
// lock/unlock interface
this . set _busy = function ( a , message , id )
{
if ( a && message ) {
var msg = this . get _label ( message ) ;
if ( msg == message )
msg = 'Loading...' ;
id = this . display _message ( msg , 'loading' ) ;
}
else if ( ! a && id ) {
this . hide _message ( id ) ;
}
this . busy = a ;
if ( this . gui _objects . editform )
this . lock _form ( this . gui _objects . editform , a ) ;
return id ;
} ;
// return a localized string
this . get _label = function ( name , domain )
{
if ( domain && this . labels [ domain + '.' + name ] )
return this . labels [ domain + '.' + name ] ;
else if ( this . labels [ name ] )
return this . labels [ name ] ;
else
return name ;
} ;
// alias for convenience reasons
this . gettext = this . get _label ;
// switch to another application task
this . switch _task = function ( task )
{
var action , path ;
if ( ( path = task . split ( '/' ) ) . length == 2 ) {
task = path [ 0 ] ;
action = path [ 1 ] ;
}
if ( this . task === task && task != 'mail' )
return ;
var url = this . get _task _url ( task ) ;
if ( action )
url += '&_action=' + action ;
if ( task == 'mail' )
url += '&_mbox=INBOX' ;
else if ( task == 'logout' ) {
url = this . secure _url ( url ) ;
this . clear _compose _data ( ) ;
}
this . redirect ( url ) ;
} ;
this . get _task _url = function ( task , url )
{
if ( ! url )
url = this . env . comm _path ;
if ( url . match ( /[?&]_task=[a-zA-Z0-9_-]+/ ) )
return url . replace ( /_task=[a-zA-Z0-9_-]+/ , '_task=' + task ) ;
else
return url . replace ( /\?.*$/ , '' ) + '?_task=' + task ;
} ;
this . reload = function ( delay )
{
if ( this . is _framed ( ) )
parent . rcmail . reload ( delay ) ;
else if ( delay )
setTimeout ( function ( ) { ref . reload ( ) ; } , delay ) ;
else if ( window . location )
location . href = this . url ( '' , { _extwin : this . env . extwin } ) ;
} ;
// Add variable to GET string, replace old value if exists
this . add _url = function ( url , name , value )
{
var urldata , datax , hash = '' ;
value = urlencode ( value ) ;
if ( /(#[a-z0-9_-]+)$/ . test ( url ) ) {
hash = RegExp . $1 ;
url = url . substr ( 0 , url . length - hash . length ) ;
}
if ( /(\?.*)$/ . test ( url ) ) {
urldata = RegExp . $1 ;
datax = RegExp ( '((\\?|&)' + RegExp . escape ( name ) + '=[^&]*)' ) ;
if ( datax . test ( urldata ) )
urldata = urldata . replace ( datax , RegExp . $2 + name + '=' + value ) ;
else
urldata += '&' + name + '=' + value ;
return url . replace ( /(\?.*)$/ , urldata ) + hash ;
}
return url + '?' + name + '=' + value + hash ;
} ;
// append CSRF protection token to the given url
this . secure _url = function ( url )
{
return this . add _url ( url , '_token' , this . env . request _token ) ;
} ,
this . is _framed = function ( )
{
return this . env . framed && parent . rcmail && parent . rcmail != this && typeof parent . rcmail . command == 'function' ;
} ;
this . save _pref = function ( prop )
{
var request = { _name : prop . name , _value : prop . value } ;
if ( prop . session )
request . _session = prop . session ;
if ( prop . env )
this . env [ prop . env ] = prop . value ;
this . http _post ( 'save-pref' , request ) ;
} ;
this . html _identifier = function ( str , encode )
{
return encode ? this . html _identifier _encode ( str ) : String ( str ) . replace ( this . identifier _expr , '_' ) ;
} ;
this . html _identifier _encode = function ( str )
{
return Base64 . encode ( String ( str ) ) . replace ( /=+$/ , '' ) . replace ( /\+/g , '-' ) . replace ( /\//g , '_' ) ;
} ;
this . html _identifier _decode = function ( str )
{
str = String ( str ) . replace ( /-/g , '+' ) . replace ( /_/g , '/' ) ;
while ( str . length % 4 ) str += '=' ;
return Base64 . decode ( str ) ;
} ;
/*********************************************************/
/********* event handling methods *********/
/*********************************************************/
this . drag _menu = function ( e , target )
{
var modkey = rcube _event . get _modifier ( e ) ,
menu = this . gui _objects . dragmenu ;
if ( menu && modkey == SHIFT _KEY && this . commands [ 'copy' ] ) {
var pos = rcube _event . get _mouse _pos ( e ) ;
this . env . drag _target = target ;
this . show _menu ( this . gui _objects . dragmenu . id , true , e ) ;
$ ( menu ) . css ( { top : ( pos . y - 10 ) + 'px' , left : ( pos . x - 10 ) + 'px' } ) ;
return true ;
}
return false ;
} ;
this . drag _menu _action = function ( action )
{
var menu = this . gui _objects . dragmenu ;
if ( menu ) {
$ ( menu ) . hide ( ) ;
}
this . command ( action , this . env . drag _target ) ;
this . env . drag _target = null ;
} ;
this . drag _start = function ( list )
{
this . drag _active = true ;
if ( this . preview _timer )
clearTimeout ( this . preview _timer ) ;
// prepare treelist widget for dragging interactions
if ( this . treelist )
this . treelist . drag _start ( ) ;
} ;
this . drag _end = function ( e )
{
var list , model ;
if ( this . treelist )
this . treelist . drag _end ( ) ;
// execute drag & drop action when mouse was released
if ( list = this . message _list )
model = this . env . mailboxes ;
else if ( list = this . contact _list )
model = this . env . contactfolders ;
// Note: we accept only mouse events to ignore dragging aborts with ESC key (#6623)
if ( this . drag _active && model && this . env . last _folder _target && ! rcube _event . is _keyboard ( e ) ) {
var target = model [ this . env . last _folder _target ] ;
list . draglayer . hide ( ) ;
if ( this . contact _list ) {
if ( ! this . contacts _drag _menu ( e , target ) )
this . command ( 'move' , target ) ;
}
else if ( ! this . drag _menu ( e , target ) )
this . command ( 'move' , target ) ;
}
this . drag _active = false ;
this . env . last _folder _target = null ;
} ;
this . drag _move = function ( e )
{
if ( this . gui _objects . folderlist ) {
var drag _target , oldclass ,
layerclass = 'draglayernormal' ,
mouse = rcube _event . get _mouse _pos ( e ) ;
if ( this . contact _list && this . contact _list . draglayer )
oldclass = this . contact _list . draglayer . attr ( 'class' ) ;
// mouse intersects a valid drop target on the treelist
if ( this . treelist && ( drag _target = this . treelist . intersects ( mouse , true ) ) ) {
this . env . last _folder _target = drag _target ;
layerclass = 'draglayer' + ( this . check _droptarget ( drag _target ) > 1 ? 'copy' : 'normal' ) ;
}
else {
// Clear target, otherwise drag end will trigger move into last valid droptarget
this . env . last _folder _target = null ;
}
if ( layerclass != oldclass && this . contact _list && this . contact _list . draglayer )
this . contact _list . draglayer . attr ( 'class' , layerclass ) ;
}
} ;
this . collapse _folder = function ( name )
{
if ( this . treelist )
this . treelist . toggle ( name ) ;
} ;
this . folder _collapsed = function ( node )
{
if ( this . folder _collapsed _timer )
clearTimeout ( this . folder _collapsed _timer ) ;
var prefname = this . env . task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders' ,
old = this . env [ prefname ] ;
if ( node . collapsed ) {
this . env [ prefname ] = this . env [ prefname ] + '&' + urlencode ( node . id ) + '&' ;
// select the folder if one of its childs is currently selected
// don't select if it's virtual (#1488346)
if ( ! node . virtual && this . env . mailbox && this . env . mailbox . startsWith ( node . id + this . env . delimiter ) )
this . command ( 'list' , node . id ) ;
}
else {
var reg = new RegExp ( '&' + urlencode ( node . id ) + '&' ) ;
this . env [ prefname ] = this . env [ prefname ] . replace ( reg , '' ) ;
}
if ( ! this . drag _active ) {
if ( old !== this . env [ prefname ] )
this . folder _collapsed _timer = setTimeout ( function ( ) { ref . command ( 'save-pref' , { name : prefname , value : ref . env [ prefname ] } ) ; } , 10 )
if ( this . env . unread _counts )
this . set _unread _count _display ( node . id , false ) ;
}
} ;
// global mouse-click handler to cleanup some UI elements
this . doc _mouse _up = function ( e )
{
var list , id , target = rcube _event . get _target ( e ) ;
// ignore event if jquery UI dialog is open
if ( $ ( target ) . closest ( '.ui-dialog, .ui-widget-overlay' ) . length )
return ;
// remove focus from list widgets
if ( window . rcube _list _widget && rcube _list _widget . _instances . length ) {
$ . each ( rcube _list _widget . _instances , function ( i , list ) {
if ( list && ! rcube _mouse _is _over ( e , list . list . parentNode ) )
list . blur ( ) ;
} ) ;
}
// reset 'pressed' buttons
if ( this . buttons _sel ) {
for ( id in this . buttons _sel )
if ( typeof id !== 'function' )
this . button _out ( this . buttons _sel [ id ] , id ) ;
this . buttons _sel = { } ;
}
// reset popup menus; delayed to have updated menu_stack data
setTimeout ( function ( e ) {
var obj , skip , config , id , i , parents = $ ( target ) . parents ( ) ;
for ( i = ref . menu _stack . length - 1 ; i >= 0 ; i -- ) {
id = ref . menu _stack [ i ] ;
obj = $ ( '#' + id ) ;
if ( obj . is ( ':visible' )
&& target != obj . data ( 'opener' )
&& target != obj . get ( 0 ) // check if scroll bar was clicked (#1489832)
&& ! parents . is ( obj . data ( 'opener' ) )
&& id != skip
&& ( obj . attr ( 'data-editable' ) != 'true' || ! $ ( target ) . parents ( '#' + id ) . length )
&& ( obj . attr ( 'data-sticky' ) != 'true' || ! rcube _mouse _is _over ( e , obj . get ( 0 ) ) )
) {
ref . hide _menu ( id , e ) ;
}
skip = obj . data ( 'parent' ) ;
}
} , 10 , e ) ;
} ;
// global keypress event handler
this . doc _keypress = function ( e )
{
// Helper method to move focus to the next/prev active menu item
var focus _menu _item = function ( dir ) {
var obj , item , mod = dir < 0 ? 'prevAll' : 'nextAll' , limit = dir < 0 ? 'last' : 'first' ;
if ( ref . focused _menu && ( obj = $ ( '#' + ref . focused _menu ) ) ) {
item = obj . find ( ':focus' ) . closest ( 'li' ) [ mod ] ( ) . has ( ':not([aria-disabled=true])' ) . find ( 'a,input' ) [ limit ] ( ) ;
if ( ! item . length )
item = obj . find ( ':focus' ) . closest ( 'ul' ) [ mod ] ( ) . has ( ':not([aria-disabled=true])' ) . find ( 'a,input' ) [ limit ] ( ) ;
return item . focus ( ) . length ;
}
return 0 ;
} ;
var target = e . target || { } ,
keyCode = rcube _event . get _keycode ( e ) ;
if ( e . keyCode != 27 && ( ! this . menu _keyboard _active || target . nodeName == 'TEXTAREA' || target . nodeName == 'SELECT' ) ) {
return true ;
}
switch ( keyCode ) {
case 38 :
case 40 :
case 63232 : // "up", in safari keypress
case 63233 : // "down", in safari keypress
focus _menu _item ( keyCode == 38 || keyCode == 63232 ? - 1 : 1 ) ;
return rcube _event . cancel ( e ) ;
case 9 : // tab
if ( this . focused _menu ) {
var mod = rcube _event . get _modifier ( e ) ;
if ( ! focus _menu _item ( mod == SHIFT _KEY ? - 1 : 1 ) ) {
this . hide _menu ( this . focused _menu , e ) ;
}
}
return rcube _event . cancel ( e ) ;
case 27 : // esc
if ( this . menu _stack . length )
this . hide _menu ( this . menu _stack [ this . menu _stack . length - 1 ] , e ) ;
break ;
}
return true ;
}
// Common handler for a keypress event on a list widget
this . list _keypress = function ( list , conf )
{
if ( list . modkey == CONTROL _KEY )
return ;
if ( list . key _pressed == list . DELETE _KEY || list . key _pressed == list . BACKSPACE _KEY )
this . command ( conf && conf . del ? conf . del : 'delete' ) ;
else if ( list . key _pressed == 33 )
this . command ( conf && conf . prev ? conf . prev : 'previouspage' ) ;
else if ( list . key _pressed == 34 )
this . command ( conf && conf . next ? conf . next : 'nextpage' ) ;
} ;
// Handler for a keypress event on a messages list widget
this . msglist _keypress = function ( list )
{
// On Enter open the message in list layout mode (no preview frame)
if ( list . key _pressed == list . ENTER _KEY && ! this . env . contentframe )
this . command ( 'show' ) ;
else
this . list _keypress ( list ) ;
} ;
this . msglist _select = function ( list )
{
if ( this . preview _timer )
clearTimeout ( this . preview _timer ) ;
var selected = list . get _single _selection ( ) ,
selected _count = list . get _selection ( false ) . length ;
this . enable _command ( this . env . message _commands , selected != null ) ;
if ( selected ) {
// Hide certain command buttons when Drafts folder is selected
if ( this . get _message _mailbox ( selected ) == this . env . drafts _mailbox )
this . enable _command ( 'reply' , 'reply-all' , 'reply-list' , 'forward' , 'forward-attachment' , 'forward-inline' , false ) ;
// Disable reply-list when List-Post header is not set
else {
var msg = this . env . messages [ selected ] ;
if ( ! msg . ml )
this . enable _command ( 'reply-list' , false ) ;
}
}
// Multi-message commands
this . enable _command ( 'delete' , 'move' , 'copy' , 'mark' , 'forward' , 'forward-attachment' , selected _count > 0 ) ;
// reset all-pages-selection
if ( selected || ( selected _count && selected _count != list . rowcount ) )
this . select _all _mode = false ;
// start timer for message preview (wait for double click)
if ( selected && this . env . contentframe && ! list . multi _selecting && ! this . dummy _select ) {
// try to be responsive and try not to overload the server when user is pressing up/down key repeatedly
var now = new Date ( ) . getTime ( ) ,
time _diff = now - ( this . _last _msglist _select _time || 0 ) ,
preview _pane _delay = this . preview _delay _click ;
// user is selecting messages repeatedly, wait until this ends (use larger delay)
if ( time _diff < this . preview _delay _select ) {
preview _pane _delay = this . preview _delay _select ;
if ( this . preview _timer ) {
clearTimeout ( this . preview _timer ) ;
}
if ( this . env . contentframe ) {
this . show _contentframe ( false ) ;
}
}
this . _last _msglist _select _time = now ;
this . preview _timer = setTimeout ( function ( ) { ref . msglist _get _preview ( ) ; } , preview _pane _delay ) ;
}
else if ( this . env . contentframe ) {
this . show _contentframe ( false ) ;
}
} ;
this . msglist _dbl _click = function ( list )
{
if ( this . preview _timer )
clearTimeout ( this . preview _timer ) ;
var mbox , uid = list . get _single _selection ( ) ;
// TODO: Here we should just use this.command('show') but we can't
// because at this point this.busy=true (set by msglist_get_preview())
if ( uid ) {
mbox = this . get _message _mailbox ( uid ) ;
if ( mbox == this . env . drafts _mailbox )
this . open _compose _step ( { _draft _uid : uid , _mbox : mbox } ) ;
else
this . show _message ( uid ) ;
}
} ;
this . msglist _get _preview = function ( )
{
var uid = this . get _single _uid ( ) ;
if ( uid && this . env . contentframe && ! this . drag _active )
this . show _message ( uid , false , true ) ;
else if ( this . env . contentframe )
this . show _contentframe ( false ) ;
} ;
this . msglist _expand = function ( row )
{
if ( this . env . messages [ row . uid ] )
this . env . messages [ row . uid ] . expanded = row . expanded ;
$ ( row . obj ) [ row . expanded ? 'addClass' : 'removeClass' ] ( 'expanded' ) ;
} ;
this . msglist _set _coltypes = function ( list )
{
var i , found , name , cols = list . thead . rows [ 0 ] . cells ;
this . env . listcols = [ ] ;
for ( i = 0 ; i < cols . length ; i ++ )
if ( cols [ i ] . id && cols [ i ] . id . startsWith ( 'rcm' ) ) {
name = cols [ i ] . id . slice ( 3 ) ;
this . env . listcols . push ( name ) ;
}
if ( ( found = $ . inArray ( 'flag' , this . env . listcols ) ) >= 0 )
this . env . flagged _col = found ;
if ( ( found = $ . inArray ( 'subject' , this . env . listcols ) ) >= 0 )
this . env . subject _col = found ;
this . command ( 'save-pref' , { name : 'list_cols' , value : this . env . listcols , session : 'list_attrib/columns' } ) ;
} ;
this . check _droptarget = function ( id )
{
switch ( this . task ) {
case 'mail' :
return ( this . env . mailboxes [ id ]
&& ! this . env . mailboxes [ id ] . virtual
&& ( this . env . mailboxes [ id ] . id != this . env . mailbox || this . is _multifolder _listing ( ) ) ) ? 1 : 0 ;
case 'addressbook' :
var target ;
if ( id != this . env . source && ( target = this . env . contactfolders [ id ] ) ) {
// droptarget is a group
if ( target . type == 'group' ) {
if ( target . id != this . env . group && ! this . env . contactfolders [ target . source ] . readonly ) {
var is _other = this . env . selection _sources . length > 1 || $ . inArray ( target . source , this . env . selection _sources ) == - 1 ;
return ! is _other || this . commands . move ? 1 : 2 ;
}
}
// droptarget is a (writable) addressbook and it's not the source
else if ( ! target . readonly && ( this . env . selection _sources . length > 1 || $ . inArray ( id , this . env . selection _sources ) == - 1 ) ) {
return this . commands . move ? 1 : 2 ;
}
}
}
return 0 ;
} ;
// open popup window
this . open _window = function ( url , small , toolbar )
{
var wname = 'rcmextwin' + new Date ( ) . getTime ( ) ;
url += ( url . match ( /\?/ ) ? '&' : '?' ) + '_extwin=1' ;
if ( this . env . standard _windows )
var extwin = window . open ( url , wname ) ;
else {
var win = this . is _framed ( ) ? parent . window : window ,
page = $ ( win ) ,
page _width = page . width ( ) ,
page _height = bw . mz ? $ ( 'body' , win ) . height ( ) : page . height ( ) ,
w = Math . min ( small ? this . env . popup _width _small : this . env . popup _width , page _width ) ,
h = page _height , // always use same height
l = ( win . screenLeft || win . screenX ) + 20 ,
t = ( win . screenTop || win . screenY ) + 20 ,
extwin = window . open ( url , wname ,
'width=' + w + ',height=' + h + ',top=' + t + ',left=' + l + ',resizable=yes,location=no,scrollbars=yes'
+ ( toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no' ) ) ;
}
// detect popup blocker (#1489618)
// don't care this might not work with all browsers
if ( ! extwin || extwin . closed ) {
this . display _message ( 'windowopenerror' , 'warning' ) ;
return ;
}
// write loading... message to empty windows
if ( ! url && extwin . document ) {
extwin . document . write ( '<html><body>' + this . get _label ( 'loading' ) + '</body></html>' ) ;
}
// allow plugins to grab the window reference (#1489413)
this . triggerEvent ( 'openwindow' , { url : url , handle : extwin } ) ;
// focus window, delayed to bring to front
setTimeout ( function ( ) { extwin && extwin . focus ( ) ; } , 10 ) ;
return extwin ;
} ;
/*********************************************************/
/********* (message) list functionality *********/
/*********************************************************/
this . init _message _row = function ( row )
{
var i , fn = { } , uid = row . uid ,
status _icon = ( this . env . status _col != null ? 'status' : 'msg' ) + 'icn' + row . id ;
if ( uid && this . env . messages [ uid ] )
$ . extend ( row , this . env . messages [ uid ] ) ;
// set eventhandler to status icon
if ( row . icon = document . getElementById ( status _icon ) ) {
fn . icon = function ( e ) { ref . command ( 'toggle_status' , uid ) ; } ;
}
// save message icon position too
if ( this . env . status _col != null )
row . msgicon = document . getElementById ( 'msgicn' + row . id ) ;
else
row . msgicon = row . icon ;
// set eventhandler to flag icon
if ( this . env . flagged _col != null && ( row . flagicon = document . getElementById ( 'flagicn' + row . id ) ) ) {
fn . flagicon = function ( e ) { ref . command ( 'toggle_flag' , uid ) ; } ;
}
// set event handler to thread expand/collapse icon
if ( ! row . depth && row . has _children && ( row . expando = document . getElementById ( 'rcmexpando' + row . id ) ) ) {
fn . expando = function ( e ) { ref . expand _message _row ( e , uid ) ; } ;
}
// attach events
$ . each ( fn , function ( i , f ) {
row [ i ] . onclick = function ( e ) { f ( e ) ; return rcube _event . cancel ( e ) ; } ;
if ( bw . touch && row [ i ] . addEventListener ) {
row [ i ] . addEventListener ( 'touchend' , function ( e ) {
if ( e . changedTouches . length == 1 ) {
f ( e ) ;
return rcube _event . cancel ( e ) ;
}
} , false ) ;
}
} ) ;
this . triggerEvent ( 'insertrow' , { uid : uid , row : row } ) ;
} ;
// create a table row in the message list
this . add _message _row = function ( uid , cols , flags , attop )
{
if ( ! this . gui _objects . messagelist || ! this . message _list )
return false ;
// Prevent from adding messages from different folder (#1487752)
if ( flags . mbox != this . env . mailbox && ! flags . skip _mbox _check )
return false ;
// When deleting messages fast it may happen that the same message
// from the next page could be added many times, we prevent this here
if ( this . message _list . rows [ uid ] )
return false ;
if ( ! this . env . messages [ uid ] )
this . env . messages [ uid ] = { } ;
// merge flags over local message object
$ . extend ( this . env . messages [ uid ] , {
deleted : flags . deleted ? 1 : 0 ,
replied : flags . answered ? 1 : 0 ,
unread : ! flags . seen ? 1 : 0 ,
forwarded : flags . forwarded ? 1 : 0 ,
flagged : flags . flagged ? 1 : 0 ,
has _children : flags . has _children ? 1 : 0 ,
depth : flags . depth ? flags . depth : 0 ,
unread _children : flags . unread _children || 0 ,
flagged _children : flags . flagged _children || 0 ,
parent _uid : flags . parent _uid || 0 ,
selected : this . select _all _mode || this . message _list . in _selection ( uid ) ,
ml : flags . ml ? 1 : 0 ,
ctype : flags . ctype ,
mbox : flags . mbox ,
// flags from plugins
flags : flags . extra _flags
} ) ;
var c , n , col , html , css _class , label , status _class = '' , status _label = '' ,
tree = '' , expando = '' ,
list = this . message _list ,
rows = list . rows ,
message = this . env . messages [ uid ] ,
msg _id = this . html _identifier ( uid , true ) ,
row _class = 'message'
+ ( ! flags . seen ? ' unread' : '' )
+ ( flags . deleted ? ' deleted' : '' )
+ ( flags . flagged ? ' flagged' : '' )
+ ( message . selected ? ' selected' : '' ) ,
row = { cols : [ ] , style : { } , id : 'rcmrow' + msg _id , uid : uid } ;
// message status icons
css _class = 'msgicon' ;
if ( this . env . status _col === null ) {
css _class += ' status' ;
if ( flags . deleted ) {
status _class += ' deleted' ;
status _label += this . get _label ( 'deleted' ) + ' ' ;
}
else if ( ! flags . seen ) {
status _class += ' unread' ;
status _label += this . get _label ( 'unread' ) + ' ' ;
}
else if ( flags . unread _children > 0 ) {
status _class += ' unreadchildren' ;
}
}
if ( flags . answered ) {
status _class += ' replied' ;
status _label += this . get _label ( 'replied' ) + ' ' ;
}
if ( flags . forwarded ) {
status _class += ' forwarded' ;
status _label += this . get _label ( 'forwarded' ) + ' ' ;
}
// update selection
if ( message . selected && ! list . in _selection ( uid ) )
list . selection . push ( uid ) ;
// threads
if ( this . env . threading ) {
if ( message . depth ) {
// This assumes that div width is hardcoded to 15px,
tree += '<span id="rcmtab' + msg _id + '" class="branch" style="width:' + ( message . depth * 15 ) + 'px;"> </span>' ;
if ( ( rows [ message . parent _uid ] && rows [ message . parent _uid ] . expanded === false )
|| ( ( this . env . autoexpand _threads == 0 || this . env . autoexpand _threads == 2 ) &&
( ! rows [ message . parent _uid ] || ! rows [ message . parent _uid ] . expanded ) )
) {
row . style . display = 'none' ;
message . expanded = false ;
}
else
message . expanded = true ;
row _class += ' thread expanded' ;
}
else if ( message . has _children ) {
if ( message . expanded === undefined && ( this . env . autoexpand _threads == 1 || ( this . env . autoexpand _threads == 2 && message . unread _children ) ) ) {
message . expanded = true ;
}
expando = '<div id="rcmexpando' + row . id + '" class="' + ( message . expanded ? 'expanded' : 'collapsed' ) + '"> </div>' ;
row _class += ' thread' + ( message . expanded ? ' expanded' : '' ) ;
}
if ( flags . unread _children && flags . seen && ! message . expanded )
row _class += ' unroot' ;
if ( flags . flagged _children && ! message . expanded )
row _class += ' flaggedroot' ;
}
tree += '<span id="msgicn' + row . id + '" class="' + css _class + status _class + '" title="' + status _label + '"></span>' ;
row . className = row _class ;
// build subject link
if ( cols . subject ) {
var action = flags . mbox == this . env . drafts _mailbox ? 'compose' : 'show' ,
uid _param = flags . mbox == this . env . drafts _mailbox ? '_draft_uid' : '_uid' ,
query = { _mbox : flags . mbox } ;
query [ uid _param ] = uid ;
cols . subject = '<a href="' + this . url ( action , query ) + '" onclick="return rcube_event.keyboard_only(event)"' +
' onmouseover="rcube_webmail.long_subject_title(this,' + ( message . depth + 1 ) + ')" tabindex="-1"><span>' + cols . subject + '</span></a>' ;
}
// add each submitted col
for ( n in this . env . listcols ) {
c = this . env . listcols [ n ] ;
col = { className : String ( c ) . toLowerCase ( ) , events : { } } ;
if ( this . env . coltypes [ c ] && this . env . coltypes [ c ] . hidden ) {
col . className += ' hidden' ;
}
if ( c == 'flag' ) {
css _class = ( flags . flagged ? 'flagged' : 'unflagged' ) ;
label = this . get _label ( css _class ) ;
html = '<span id="flagicn' + row . id + '" class="' + css _class + '" title="' + label + '"></span>' ;
}
else if ( c == 'attachment' ) {
label = this . get _label ( 'withattachment' ) ;
if ( flags . attachmentClass )
html = '<span class="' + flags . attachmentClass + '" title="' + label + '"></span>' ;
else if ( flags . ctype == 'multipart/report' )
html = '<span class="report"></span>' ;
else if ( flags . ctype == 'multipart/encrypted' || flags . ctype == 'application/pkcs7-mime' )
html = '<span class="encrypted"></span>' ;
else if ( flags . hasattachment || ( ! flags . hasnoattachment && /application\/|multipart\/(m|signed)/ . test ( flags . ctype ) ) )
html = '<span class="attachment" title="' + label + '"></span>' ;
else
html = ' ' ;
}
else if ( c == 'status' ) {
label = '' ;
if ( flags . deleted ) {
css _class = 'deleted' ;
label = this . get _label ( 'deleted' ) ;
}
else if ( ! flags . seen ) {
css _class = 'unread' ;
label = this . get _label ( 'unread' ) ;
}
else if ( flags . unread _children > 0 ) {
css _class = 'unreadchildren' ;
}
else
css _class = 'msgicon' ;
html = '<span id="statusicn' + row . id + '" class="' + css _class + status _class + '" title="' + label + '"></span>' ;
}
else if ( c == 'threads' )
html = expando ;
else if ( c == 'subject' ) {
html = tree + cols [ c ] ;
}
else if ( c == 'priority' ) {
if ( flags . prio > 0 && flags . prio < 6 ) {
label = this . get _label ( 'priority' ) + ' ' + flags . prio ;
html = '<span class="prio' + flags . prio + '" title="' + label + '"></span>' ;
}
else
html = ' ' ;
}
else if ( c == 'folder' ) {
html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols [ c ] + '<span>' ;
}
else
html = cols [ c ] ;
col . innerHTML = html ;
row . cols . push ( col ) ;
}
if ( this . env . layout == 'widescreen' )
row = this . widescreen _message _row ( row , uid , message ) ;
list . insert _row ( row , attop ) ;
// remove 'old' row
if ( attop && this . env . pagesize && list . rowcount > this . env . pagesize ) {
var uid = list . get _last _row ( ) ;
list . remove _row ( uid ) ;
list . clear _selection ( uid ) ;
}
} ;
// Converts standard message list record into "widescreen" (3-column) layout
this . widescreen _message _row = function ( row , uid , message )
{
var domrow = document . createElement ( 'tr' ) ;
domrow . id = row . id ;
domrow . uid = row . uid ;
domrow . className = row . className ;
if ( row . style ) $ . extend ( domrow . style , row . style ) ;
$ . each ( this . env . widescreen _list _template , function ( ) {
if ( ! ref . env . threading && this . className == 'threads' )
return ;
var i , n , e , col , domcol ,
domcell = document . createElement ( 'td' ) ;
if ( this . className ) domcell . className = this . className ;
for ( i = 0 ; this . cells && i < this . cells . length ; i ++ ) {
for ( n = 0 ; row . cols && n < row . cols . length ; n ++ ) {
if ( this . cells [ i ] == row . cols [ n ] . className ) {
col = row . cols [ n ] ;
domcol = document . createElement ( 'span' ) ;
domcol . className = this . cells [ i ] ;
if ( this . className == 'subject' && domcol . className != 'subject' )
domcol . className += ' skip-on-drag' ;
if ( col . innerHTML )
domcol . innerHTML = col . innerHTML ;
domcell . appendChild ( domcol ) ;
break ;
}
}
}
domrow . appendChild ( domcell ) ;
} ) ;
if ( this . env . threading && message . depth ) {
n = this . calculate _thread _padding ( message . depth ) ;
$ ( 'td.subject' , domrow ) . attr ( 'style' , 'padding-left:' + n + ' !important' ) ;
$ ( 'span.branch' , domrow ) . remove ( ) ;
}
return domrow ;
} ;
this . calculate _thread _padding = function ( level )
{
ref . env . thread _padding . match ( /^([0-9.]+)(.+)/ ) ;
return ( Math . min ( 6 , level ) * parseFloat ( RegExp . $1 ) ) + RegExp . $2 ;
} ;
this . set _list _sorting = function ( sort _col , sort _order )
{
var sort _old = this . env . sort _col == 'arrival' ? 'date' : this . env . sort _col ,
sort _new = sort _col == 'arrival' ? 'date' : sort _col ;
// set table header class
$ ( '#rcm' + sort _old ) . removeClass ( 'sorted' + this . env . sort _order . toUpperCase ( ) ) ;
if ( sort _new )
$ ( '#rcm' + sort _new ) . addClass ( 'sorted' + sort _order ) ;
// if sorting by 'arrival' is selected, click on date column should not switch to 'date'
$ ( '#rcmdate > a' ) . prop ( 'rel' , sort _col == 'arrival' ? 'arrival' : 'date' ) ;
this . env . sort _col = sort _col ;
this . env . sort _order = sort _order ;
} ;
this . set _list _options = function ( cols , sort _col , sort _order , threads , layout )
{
var update , post _data = { } ;
if ( sort _col === undefined )
sort _col = this . env . sort _col ;
if ( ! sort _order )
sort _order = this . env . sort _order ;
if ( this . env . sort _col != sort _col || this . env . sort _order != sort _order ) {
update = 1 ;
this . set _list _sorting ( sort _col , sort _order ) ;
}
if ( this . env . threading != threads ) {
update = 1 ;
post _data . _threads = threads ;
}
if ( layout && this . env . layout != layout ) {
this . triggerEvent ( 'layout-change' , { old _layout : this . env . layout , new _layout : layout } ) ;
update = 1 ;
this . env . layout = post _data . _layout = layout ;
}
if ( cols && cols . length ) {
// make sure new columns are added at the end of the list
var i , idx , name , newcols = [ ] , oldcols = this . env . listcols ;
for ( i = 0 ; i < oldcols . length ; i ++ ) {
name = oldcols [ i ] ;
idx = $ . inArray ( name , cols ) ;
if ( idx != - 1 ) {
newcols . push ( name ) ;
delete cols [ idx ] ;
}
}
for ( i = 0 ; i < cols . length ; i ++ )
if ( cols [ i ] )
newcols . push ( cols [ i ] ) ;
if ( newcols . join ( ) != oldcols . join ( ) ) {
update = 1 ;
post _data . _cols = newcols . join ( ',' ) ;
}
}
if ( update )
this . list _mailbox ( '' , '' , sort _col + '_' + sort _order , post _data ) ;
} ;
// when user double-clicks on a row
this . show _message = function ( id , safe , preview )
{
if ( ! id )
return ;
var win , target = window ,
url = this . params _from _uid ( id , { _caps : this . browser _capabilities ( ) } ) ;
if ( preview && ( win = this . get _frame _window ( this . env . contentframe ) ) ) {
target = win ;
url . _framed = 1 ;
}
if ( safe )
url . _safe = 1 ;
// also send search request to get the right messages
if ( this . env . search _request )
url . _search = this . env . search _request ;
if ( this . env . extwin )
url . _extwin = 1 ;
url = this . url ( preview ? 'preview' : 'show' , url ) ;
if ( preview )
this . preview _id = id ;
if ( preview && String ( target . location . href ) . indexOf ( url ) >= 0 ) {
this . show _contentframe ( true ) ;
}
else {
if ( ! preview && this . env . message _extwin && ! this . env . extwin )
this . open _window ( url , true ) ;
else
this . location _href ( url , target , true ) ;
}
} ;
// update message status and unread counter after marking a message as read
this . set _unread _message = function ( id , folder )
{
var self = this ;
// find window with messages list
if ( ! self . message _list )
self = self . opener ( ) ;
if ( ! self && window . parent )
self = parent . rcmail ;
if ( ! self || ! self . message _list )
return ;
// this may fail in multifolder mode
if ( self . set _message ( id , 'unread' , false ) === false )
self . set _message ( id + '-' + folder , 'unread' , false ) ;
if ( self . env . unread _counts [ folder ] > 0 ) {
self . env . unread _counts [ folder ] -= 1 ;
self . set _unread _count ( folder , self . env . unread _counts [ folder ] , folder == 'INBOX' && ! self . is _multifolder _listing ( ) ) ;
}
} ;
this . show _contentframe = function ( show )
{
var frame , win , name = this . env . contentframe ;
if ( frame = this . get _frame _element ( name ) ) {
if ( ! show && ( win = this . get _frame _window ( name ) ) ) {
if ( win . location . href . indexOf ( this . env . blankpage ) < 0 ) {
if ( win . stop )
win . stop ( ) ;
else // IE
win . document . execCommand ( 'Stop' ) ;
win . location . href = this . env . blankpage ;
}
}
else if ( ! bw . safari && ! bw . konq )
$ ( frame ) [ show ? 'show' : 'hide' ] ( ) ;
}
if ( ! show ) {
this . unlock _frame ( ) ;
delete this . preview _id ;
}
} ;
this . get _frame _element = function ( id )
{
var frame ;
if ( id && ( frame = document . getElementById ( id ) ) )
return frame ;
} ;
this . get _frame _window = function ( id )
{
var frame = this . get _frame _element ( id ) ;
if ( frame && frame . name && window . frames )
return window . frames [ frame . name ] ;
} ;
this . lock _frame = function ( target )
{
var rc = this . is _framed ( ) ? parent . rcmail : this ;
if ( ! rc . env . frame _lock )
rc . env . frame _lock = rc . set _busy ( true , 'loading' ) ;
if ( target . frameElement )
$ ( target . frameElement ) . on ( 'load.lock' , function ( e ) {
rc . unlock _frame ( ) ;
$ ( this ) . off ( 'load.lock' ) ;
} ) ;
} ;
this . unlock _frame = function ( )
{
if ( this . env . frame _lock ) {
this . set _busy ( false , null , this . env . frame _lock ) ;
this . env . frame _lock = null ;
}
} ;
// list a specific page
this . list _page = function ( page )
{
if ( page == 'next' )
page = this . env . current _page + 1 ;
else if ( page == 'last' )
page = this . env . pagecount ;
else if ( page == 'prev' && this . env . current _page > 1 )
page = this . env . current _page - 1 ;
else if ( page == 'first' && this . env . current _page > 1 )
page = 1 ;
if ( page > 0 && page <= this . env . pagecount ) {
this . env . current _page = page ;
if ( this . task == 'addressbook' || this . contact _list )
this . list _contacts ( this . env . source , this . env . group , page ) ;
else if ( this . task == 'mail' )
this . list _mailbox ( this . env . mailbox , page ) ;
}
} ;
// sends request to check for recent messages
this . checkmail = function ( )
{
var lock = this . set _busy ( true , 'checkingmail' ) ,
params = this . check _recent _params ( ) ;
this . http _post ( 'check-recent' , params , lock ) ;
} ;
// list messages of a specific mailbox using filter
this . filter _mailbox = function ( filter )
{
if ( this . filter _disabled )
return ;
var params = this . search _params ( false , filter ) ,
lock = this . set _busy ( true , 'searching' ) ;
this . clear _message _list ( ) ;
// reset vars
this . env . current _page = 1 ;
this . env . search _filter = filter ;
this . http _request ( 'search' , params , lock ) ;
this . update _state ( { _mbox : params . _mbox , _filter : filter , _scope : params . _scope } ) ;
} ;
// reload the current message listing
this . refresh _list = function ( )
{
this . list _mailbox ( this . env . mailbox , this . env . current _page || 1 , null , { _clear : 1 } , true ) ;
if ( this . message _list )
this . message _list . clear _selection ( ) ;
} ;
// list messages of a specific mailbox
this . list _mailbox = function ( mbox , page , sort , url , update _only )
{
var win , target = window ;
if ( typeof url != 'object' )
url = { } ;
if ( ! mbox )
mbox = this . env . mailbox ? this . env . mailbox : 'INBOX' ;
// add sort to url if set
if ( sort )
url . _sort = sort ;
// folder change, reset page, search scope, etc.
if ( this . env . mailbox != mbox ) {
page = 1 ;
this . env . current _page = page ;
this . env . search _scope = 'base' ;
this . select _all _mode = false ;
this . reset _search _filter ( ) ;
}
// also send search request to get the right messages
else if ( this . env . search _request )
url . _search = this . env . search _request ;
if ( ! update _only ) {
// unselect selected messages and clear the list and message data
this . clear _message _list ( ) ;
if ( mbox != this . env . mailbox || ( mbox == this . env . mailbox && ! page && ! sort ) )
url . _refresh = 1 ;
this . select _folder ( mbox , '' , true ) ;
this . unmark _folder ( mbox , 'recent' , '' , true ) ;
this . env . mailbox = mbox ;
}
// load message list remotely
if ( this . gui _objects . messagelist ) {
this . list _mailbox _remote ( mbox , page , url ) ;
return ;
}
if ( win = this . get _frame _window ( this . env . contentframe ) ) {
target = win ;
url . _framed = 1 ;
}
if ( this . env . uid )
url . _uid = this . env . uid ;
// load message list to target frame/window
if ( mbox ) {
url . _mbox = mbox ;
url . _page = page ;
this . set _busy ( true , 'loading' ) ;
this . location _href ( url , target ) ;
}
} ;
this . clear _message _list = function ( )
{
this . env . messages = { } ;
this . show _contentframe ( false ) ;
if ( this . message _list )
this . message _list . clear ( true ) ;
} ;
// send remote request to load message list
this . list _mailbox _remote = function ( mbox , page , url )
{
var lock = this . set _busy ( true , 'loading' ) ;
if ( typeof url != 'object' )
url = { } ;
url . _layout = this . env . layout
url . _mbox = mbox ;
url . _page = page ;
this . http _request ( 'list' , url , lock ) ;
this . update _state ( { _mbox : mbox , _page : ( page && page > 1 ? page : null ) } ) ;
} ;
// removes messages that doesn't exists from list selection array
this . update _selection = function ( )
{
var list = this . message _list ,
selected = list . selection ,
rows = list . rows ,
i , selection = [ ] ;
for ( i in selected )
if ( rows [ selected [ i ] ] )
selection . push ( selected [ i ] ) ;
list . selection = selection ;
// reset preview frame, if currently previewed message is not selected (has been removed)
try {
var win = this . get _frame _window ( this . env . contentframe ) ,
id = win . rcmail . env . uid ;
if ( id && ! list . in _selection ( id ) )
this . show _contentframe ( false ) ;
}
catch ( e ) { } ;
} ;
// expand all threads with unread children
this . expand _unread = function ( )
{
var r , tbody = this . message _list . tbody ,
new _row = tbody . firstChild ;
while ( new _row ) {
if ( new _row . nodeType == 1 && ( r = this . message _list . rows [ new _row . uid ] ) && r . unread _children ) {
this . message _list . expand _all ( r ) ;
this . set _unread _children ( r . uid ) ;
}
new _row = new _row . nextSibling ;
}
return false ;
} ;
// thread expanding/collapsing handler
this . expand _message _row = function ( e , uid )
{
var row = this . message _list . rows [ uid ] ;
// handle unread_children/flagged_children mark
row . expanded = ! row . expanded ;
this . set _unread _children ( uid ) ;
this . set _flagged _children ( uid ) ;
row . expanded = ! row . expanded ;
this . message _list . expand _row ( e , uid ) ;
} ;
// message list expanding
this . expand _threads = function ( )
{
if ( ! this . env . threading || ! this . env . autoexpand _threads || ! this . message _list )
return ;
switch ( this . env . autoexpand _threads ) {
case 2 : this . expand _unread ( ) ; break ;
case 1 : this . message _list . expand _all ( ) ; break ;
}
} ;
// Initializes threads indicators/expanders after list update
this . init _threads = function ( roots , mbox )
{
// #1487752
if ( mbox && mbox != this . env . mailbox )
return false ;
for ( var n = 0 , len = roots . length ; n < len ; n ++ )
this . add _tree _icons ( roots [ n ] ) ;
this . expand _threads ( ) ;
} ;
// adds threads tree icons to the list (or specified thread)
this . add _tree _icons = function ( root )
{
var i , l , r , n , len , pos , tmp = [ ] , uid = [ ] ,
row , rows = this . message _list . rows ;
if ( root )
row = rows [ root ] ? rows [ root ] . obj : null ;
else
row = this . message _list . tbody . firstChild ;
while ( row ) {
if ( row . nodeType == 1 && ( r = rows [ row . uid ] ) ) {
if ( r . depth ) {
for ( i = tmp . length - 1 ; i >= 0 ; i -- ) {
len = tmp [ i ] . length ;
if ( len > r . depth ) {
pos = len - r . depth ;
if ( ! ( tmp [ i ] [ pos ] & 2 ) )
tmp [ i ] [ pos ] = tmp [ i ] [ pos ] ? tmp [ i ] [ pos ] + 2 : 2 ;
}
else if ( len == r . depth ) {
if ( ! ( tmp [ i ] [ 0 ] & 2 ) )
tmp [ i ] [ 0 ] += 2 ;
}
if ( r . depth > len )
break ;
}
tmp . push ( new Array ( r . depth ) ) ;
tmp [ tmp . length - 1 ] [ 0 ] = 1 ;
uid . push ( r . uid ) ;
}
else {
if ( tmp . length ) {
for ( i in tmp ) {
this . set _tree _icons ( uid [ i ] , tmp [ i ] ) ;
}
tmp = [ ] ;
uid = [ ] ;
}
if ( root && row != rows [ root ] . obj )
break ;
}
}
row = row . nextSibling ;
}
if ( tmp . length ) {
for ( i in tmp ) {
this . set _tree _icons ( uid [ i ] , tmp [ i ] ) ;
}
}
} ;
// adds tree icons to specified message row
this . set _tree _icons = function ( uid , tree )
{
var i , divs = [ ] , html = '' , len = tree . length ;
for ( i = 0 ; i < len ; i ++ ) {
if ( tree [ i ] > 2 )
divs . push ( { 'class' : 'l3' , width : 15 } ) ;
else if ( tree [ i ] > 1 )
divs . push ( { 'class' : 'l2' , width : 15 } ) ;
else if ( tree [ i ] > 0 )
divs . push ( { 'class' : 'l1' , width : 15 } ) ;
// separator div
else if ( divs . length && ! divs [ divs . length - 1 ] [ 'class' ] )
divs [ divs . length - 1 ] . width += 15 ;
else
divs . push ( { 'class' : null , width : 15 } ) ;
}
for ( i = divs . length - 1 ; i >= 0 ; i -- ) {
if ( divs [ i ] [ 'class' ] )
html += '<div class="tree ' + divs [ i ] [ 'class' ] + '" />' ;
else
html += '<div style="width:' + divs [ i ] . width + 'px" />' ;
}
if ( html )
$ ( '#rcmtab' + this . html _identifier ( uid , true ) ) . html ( html ) ;
} ;
// update parent in a thread
this . update _thread _root = function ( uid , flag )
{
if ( ! this . env . threading )
return ;
var root = this . message _list . find _root ( uid ) ;
if ( uid == root )
return ;
var p = this . message _list . rows [ root ] ;
if ( flag == 'read' && p . unread _children ) {
p . unread _children -- ;
}
else if ( flag == 'unread' && p . has _children ) {
// unread_children may be undefined
p . unread _children = ( p . unread _children || 0 ) + 1 ;
}
else if ( flag == 'unflagged' && p . flagged _children ) {
p . flagged _children -- ;
}
else if ( flag == 'flagged' && p . has _children ) {
p . flagged _children = ( p . flagged _children || 0 ) + 1 ;
}
else {
return ;
}
this . set _message _icon ( root ) ;
this . set _unread _children ( root ) ;
this . set _flagged _children ( root ) ;
} ;
// update thread indicators for all messages in a thread below the specified message
// return number of removed/added root level messages
this . update _thread = function ( uid )
{
if ( ! this . env . threading || ! this . message _list . rows [ uid ] )
return 0 ;
var r , parent , count = 0 ,
list = this . message _list ,
rows = list . rows ,
row = rows [ uid ] ,
depth = rows [ uid ] . depth ,
roots = [ ] ;
if ( ! row . depth ) // root message: decrease roots count
count -- ;
// update unread_children for thread root
if ( row . depth && row . unread ) {
parent = list . find _root ( uid ) ;
rows [ parent ] . unread _children -- ;
this . set _unread _children ( parent ) ;
}
// update unread_children for thread root
if ( row . depth && row . flagged ) {
parent = list . find _root ( uid ) ;
rows [ parent ] . flagged _children -- ;
this . set _flagged _children ( parent ) ;
}
parent = row . parent _uid ;
// childrens
row = row . obj . nextSibling ;
while ( row ) {
if ( row . nodeType == 1 && ( r = rows [ row . uid ] ) ) {
if ( ! r . depth || r . depth <= depth )
break ;
r . depth -- ; // move left
// reset width and clear the content of a tab, icons will be added later
$ ( '#rcmtab' + r . id ) . width ( r . depth * 15 ) . html ( '' ) ;
if ( ! r . depth ) { // a new root
count ++ ; // increase roots count
r . parent _uid = 0 ;
if ( r . has _children ) {
// replace 'leaf' with 'collapsed'
$ ( '#' + r . id + ' .leaf' ) . first ( )
. attr ( 'id' , 'rcmexpando' + r . id )
. attr ( 'class' , ( r . obj . style . display != 'none' ? 'expanded' : 'collapsed' ) )
. mousedown ( { uid : r . uid } , function ( e ) {
return ref . expand _message _row ( e , e . data . uid ) ;
} ) ;
r . unread _children = 0 ;
roots . push ( r ) ;
}
// show if it was hidden
if ( r . obj . style . display == 'none' )
$ ( r . obj ) . show ( ) ;
}
else {
if ( r . depth == depth )
r . parent _uid = parent ;
if ( r . unread && roots . length )
roots [ roots . length - 1 ] . unread _children ++ ;
}
}
row = row . nextSibling ;
}
// update unread_children/flagged_children for roots
for ( r = 0 ; r < roots . length ; r ++ ) {
this . set _unread _children ( roots [ r ] . uid ) ;
this . set _flagged _children ( roots [ r ] . uid ) ;
}
return count ;
} ;
this . delete _excessive _thread _rows = function ( )
{
var rows = this . message _list . rows ,
tbody = this . message _list . tbody ,
row = tbody . firstChild ,
cnt = this . env . pagesize + 1 ;
while ( row ) {
if ( row . nodeType == 1 && ( r = rows [ row . uid ] ) ) {
if ( ! r . depth && cnt )
cnt -- ;
if ( ! cnt )
this . message _list . remove _row ( row . uid ) ;
}
row = row . nextSibling ;
}
} ;
// set message icon
this . set _message _icon = function ( uid )
{
var css _class , label = '' ,
row = this . message _list . rows [ uid ] ;
if ( ! row )
return false ;
if ( row . icon ) {
css _class = 'msgicon' ;
if ( row . deleted ) {
css _class += ' deleted' ;
label += this . get _label ( 'deleted' ) + ' ' ;
}
else if ( row . unread ) {
css _class += ' unread' ;
label += this . get _label ( 'unread' ) + ' ' ;
}
else if ( row . unread _children )
css _class += ' unreadchildren' ;
if ( row . msgicon == row . icon ) {
if ( row . replied ) {
css _class += ' replied' ;
label += this . get _label ( 'replied' ) + ' ' ;
}
if ( row . forwarded ) {
css _class += ' forwarded' ;
label += this . get _label ( 'forwarded' ) + ' ' ;
}
css _class += ' status' ;
}
$ ( row . icon ) . attr ( { 'class' : css _class , title : label } ) ;
}
if ( row . msgicon && row . msgicon != row . icon ) {
label = '' ;
css _class = 'msgicon' ;
if ( ! row . unread && row . unread _children ) {
css _class += ' unreadchildren' ;
}
if ( row . replied ) {
css _class += ' replied' ;
label += this . get _label ( 'replied' ) + ' ' ;
}
if ( row . forwarded ) {
css _class += ' forwarded' ;
label += this . get _label ( 'forwarded' ) + ' ' ;
}
$ ( row . msgicon ) . attr ( { 'class' : css _class , title : label } ) ;
}
if ( row . flagicon ) {
css _class = ( row . flagged ? 'flagged' : 'unflagged' ) ;
label = this . get _label ( css _class ) ;
$ ( row . flagicon ) . attr ( 'class' , css _class )
. attr ( { 'aria-label' : label , title : label } ) ;
}
} ;
// set message status
this . set _message _status = function ( uid , flag , status )
{
var row = this . message _list . rows [ uid ] ;
if ( ! row )
return false ;
if ( flag == 'unread' ) {
if ( row . unread != status )
this . update _thread _root ( uid , status ? 'unread' : 'read' ) ;
}
else if ( flag == 'flagged' ) {
this . update _thread _root ( uid , status ? 'flagged' : 'unflagged' ) ;
}
if ( $ . inArray ( flag , [ 'unread' , 'deleted' , 'replied' , 'forwarded' , 'flagged' ] ) > - 1 )
row [ flag ] = status ;
} ;
// set message row status, class and icon
this . set _message = function ( uid , flag , status )
{
var row = this . message _list && this . message _list . rows [ uid ] ;
if ( ! row )
return false ;
if ( flag )
this . set _message _status ( uid , flag , status ) ;
if ( $ . inArray ( flag , [ 'unread' , 'deleted' , 'flagged' ] ) > - 1 )
$ ( row . obj ) [ row [ flag ] ? 'addClass' : 'removeClass' ] ( flag ) ;
this . set _unread _children ( uid ) ;
this . set _message _icon ( uid ) ;
} ;
// sets unroot (unread_children) class of parent row
this . set _unread _children = function ( uid )
{
var row = this . message _list . rows [ uid ] ;
if ( row . parent _uid )
return ;
var enable = ! row . unread && row . unread _children && ! row . expanded ;
$ ( row . obj ) [ enable ? 'addClass' : 'removeClass' ] ( 'unroot' ) ;
} ;
// sets flaggedroot (flagged_children) class of parent row
this . set _flagged _children = function ( uid )
{
var row = this . message _list . rows [ uid ] ;
if ( row . parent _uid )
return ;
var enable = row . flagged _children && ! row . expanded ;
$ ( row . obj ) [ enable ? 'addClass' : 'removeClass' ] ( 'flaggedroot' ) ;
} ;
// copy selected messages to the specified mailbox
this . copy _messages = function ( mbox , event , uids )
{
if ( mbox && typeof mbox === 'object' ) {
if ( mbox . uids ) uids = mbox . uids ;
mbox = mbox . id ;
}
else if ( ! mbox ) {
uids = this . env . uid ? [ this . env . uid ] : this . message _list . get _selection ( ) ;
return this . folder _selector ( event , function ( folder , obj ) {
ref . command ( 'copy' , { id : folder , uids : uids } , obj , event , true ) ;
} ) ;
}
// exit if current or no mailbox specified
if ( ! mbox || mbox == this . env . mailbox )
return ;
var post _data = this . selection _post _data ( { _target _mbox : mbox , _uid : uids } ) ;
// exit if selection is empty
if ( ! post _data . _uid )
return ;
// send request to server
this . http _post ( 'copy' , post _data , this . display _message ( 'copyingmessage' , 'loading' ) ) ;
} ;
// move selected messages to the specified mailbox
this . move _messages = function ( mbox , event , uids )
{
if ( mbox && typeof mbox === 'object' ) {
if ( mbox . uids ) uids = mbox . uids ;
mbox = mbox . id ;
}
else if ( ! mbox ) {
uids = this . env . uid ? [ this . env . uid ] : this . message _list . get _selection ( ) ;
return this . folder _selector ( event , function ( folder , obj ) {
ref . command ( 'move' , { id : folder , uids : uids } , obj , event , true ) ;
} ) ;
}
// exit if current or no mailbox specified
if ( ! mbox || ( mbox == this . env . mailbox && ! this . is _multifolder _listing ( ) ) )
return ;
var lock = false , post _data = this . selection _post _data ( { _target _mbox : mbox , _uid : uids } ) ;
// exit if selection is empty
if ( ! post _data . _uid )
return ;
// show wait message
if ( this . env . action == 'show' )
lock = this . set _busy ( true , 'movingmessage' ) ;
// Hide message command buttons until a message is selected
this . enable _command ( this . env . message _commands , false ) ;
this . with _selected _messages ( 'move' , post _data , lock ) ;
if ( this . env . action != 'show' )
this . show _contentframe ( false ) ;
} ;
// delete selected messages from the current mailbox
this . delete _messages = function ( event )
{
var list = this . message _list , trash = this . env . trash _mailbox ;
// if config is set to flag for deletion
if ( this . env . flag _for _deletion ) {
this . mark _message ( 'delete' ) ;
return false ;
}
// if there isn't a defined trash mailbox or we are in it
else if ( ! trash || this . env . mailbox == trash )
this . permanently _remove _messages ( ) ;
// we're in Junk folder and delete_junk is enabled
else if ( this . env . delete _junk && this . env . junk _mailbox && this . env . mailbox == this . env . junk _mailbox )
this . permanently _remove _messages ( ) ;
// if there is a trash mailbox defined and we're not currently in it
else {
// if shift was pressed delete it immediately
if ( ( list && list . modkey == SHIFT _KEY ) || ( event && rcube _event . get _modifier ( event ) == SHIFT _KEY ) ) {
this . confirm _dialog ( this . get _label ( 'deletemessagesconfirm' ) , 'delete' , function ( ) {
ref . permanently _remove _messages ( ) ;
} ) ;
}
else
this . move _messages ( trash ) ;
}
return true ;
} ;
// delete the selected messages permanently
this . permanently _remove _messages = function ( )
{
var post _data = this . selection _post _data ( ) ;
// exit if selection is empty
if ( ! post _data . _uid )
return ;
this . with _selected _messages ( 'delete' , post _data ) ;
this . show _contentframe ( false ) ;
} ;
// Send a specific move/delete request with UIDs of all selected messages
this . with _selected _messages = function ( action , post _data , lock , http _action )
{
var count = 0 , msg ,
remove = ( action == 'delete' || ! this . is _multifolder _listing ( ) ) ;
// update the list (remove rows, clear selection)
if ( this . message _list ) {
var n , len , id , root , roots = [ ] ,
selection = post _data . _uid ,
display _next = this . check _display _next ( ) ;
if ( selection === '*' )
selection = this . message _list . get _selection ( ) ;
else if ( typeof selection == 'string' )
selection = selection . split ( ',' ) ;
for ( n = 0 , len = selection . length ; n < len ; n ++ ) {
id = selection [ n ] ;
if ( this . env . threading ) {
count += this . update _thread ( id ) ;
root = this . message _list . find _root ( id ) ;
if ( root != id && $ . inArray ( root , roots ) < 0 ) {
roots . push ( root ) ;
}
}
if ( remove )
this . message _list . remove _row ( id , display _next && n == selection . length - 1 ) ;
}
// make sure there are no selected rows
if ( ! display _next && remove )
this . message _list . clear _selection ( ) ;
// update thread tree icons
for ( n = 0 , len = roots . length ; n < len ; n ++ ) {
this . add _tree _icons ( roots [ n ] ) ;
}
}
if ( count < 0 )
post _data . _count = ( count * - 1 ) ;
// remove threads from the end of the list
else if ( count > 0 && remove )
this . delete _excessive _thread _rows ( ) ;
if ( ! remove )
post _data . _refresh = 1 ;
if ( ! lock ) {
msg = action == 'move' ? 'movingmessage' : 'deletingmessage' ;
lock = this . display _message ( msg , 'loading' ) ;
}
// send request to server
this . http _post ( http _action || action , post _data , lock ) ;
} ;
// build post data for message delete/move/copy/flag requests
this . selection _post _data = function ( data )
{
if ( typeof ( data ) != 'object' )
data = { } ;
if ( ! data . _uid )
data . _uid = this . env . uid ? [ this . env . uid ] : this . message _list . get _selection ( ) ;
data . _mbox = this . env . mailbox ;
data . _uid = this . uids _to _list ( data . _uid ) ;
if ( this . env . action )
data . _from = this . env . action ;
// also send search request to get the right messages
if ( this . env . search _request )
data . _search = this . env . search _request ;
if ( this . env . display _next && this . env . next _uid )
data . _next _uid = this . env . next _uid ;
return data ;
} ;
this . check _display _next = function ( )
{
return this . env . display _next && ( this . preview _id || ! this . env . contentframe ) ;
} ;
// set a specific flag to one or more messages
this . mark _message = function ( flag , uid )
{
var a _uids = [ ] , r _uids = [ ] , len , n , id ,
list = this . message _list ;
if ( uid )
a _uids [ 0 ] = uid ;
else if ( this . env . uid )
a _uids [ 0 ] = this . env . uid ;
else if ( list )
a _uids = list . get _selection ( ) ;
if ( ! list )
r _uids = a _uids ;
else {
list . focus ( ) ;
for ( n = 0 , len = a _uids . length ; n < len ; n ++ ) {
id = a _uids [ n ] ;
if ( ( flag == 'read' && list . rows [ id ] . unread )
|| ( flag == 'unread' && ! list . rows [ id ] . unread )
|| ( flag == 'delete' && ! list . rows [ id ] . deleted )
|| ( flag == 'undelete' && list . rows [ id ] . deleted )
|| ( flag == 'flagged' && ! list . rows [ id ] . flagged )
|| ( flag == 'unflagged' && list . rows [ id ] . flagged ) )
{
r _uids . push ( id ) ;
}
}
}
// nothing to do
if ( ! r _uids . length && ! this . select _all _mode )
return ;
switch ( flag ) {
case 'read' :
case 'unread' :
this . toggle _read _status ( flag , r _uids ) ;
break ;
case 'delete' :
case 'undelete' :
this . toggle _delete _status ( r _uids ) ;
break ;
case 'flagged' :
case 'unflagged' :
this . toggle _flagged _status ( flag , a _uids ) ;
break ;
}
} ;
// set class to read/unread
this . toggle _read _status = function ( flag , a _uids )
{
var i , len = a _uids . length ,
post _data = this . selection _post _data ( { _uid : a _uids , _flag : flag } ) ,
lock = this . display _message ( 'markingmessage' , 'loading' ) ;
// mark all message rows as read/unread
for ( i = 0 ; i < len ; i ++ )
this . set _message ( a _uids [ i ] , 'unread' , ( flag == 'unread' ? true : false ) ) ;
this . http _post ( 'mark' , post _data , lock ) ;
} ;
// set image to flagged or unflagged
this . toggle _flagged _status = function ( flag , a _uids )
{
var i , len = a _uids . length ,
win = this . env . contentframe ? this . get _frame _window ( this . env . contentframe ) : window ,
post _data = this . selection _post _data ( { _uid : a _uids , _flag : flag } ) ,
lock = this . display _message ( 'markingmessage' , 'loading' ) ;
// mark all message rows as flagged/unflagged
for ( i = 0 ; i < len ; i ++ )
this . set _message ( a _uids [ i ] , 'flagged' , ( flag == 'flagged' ? true : false ) ) ;
if ( this . env . action == 'show' || $ . inArray ( this . preview _id , a _uids ) >= 0 )
$ ( win . document . body ) [ flag == 'flagged' ? 'addClass' : 'removeClass' ] ( 'status-flagged' ) ;
this . http _post ( 'mark' , post _data , lock ) ;
} ;
// mark all message rows as deleted/undeleted
this . toggle _delete _status = function ( a _uids )
{
var i , uid , all _deleted = true ,
len = a _uids . length ,
rows = this . message _list ? this . message _list . rows : { } ;
if ( len == 1 ) {
if ( ! this . message _list || ( rows [ a _uids [ 0 ] ] && ! rows [ a _uids [ 0 ] ] . deleted ) )
this . flag _as _deleted ( a _uids ) ;
else
this . flag _as _undeleted ( a _uids ) ;
return true ;
}
for ( i = 0 ; i < len ; i ++ ) {
uid = a _uids [ i ] ;
if ( rows [ uid ] && ! rows [ uid ] . deleted ) {
all _deleted = false ;
break ;
}
}
if ( all _deleted )
this . flag _as _undeleted ( a _uids ) ;
else
this . flag _as _deleted ( a _uids ) ;
return true ;
} ;
this . flag _as _undeleted = function ( a _uids )
{
var i , len = a _uids . length ,
post _data = this . selection _post _data ( { _uid : a _uids , _flag : 'undelete' } ) ,
lock = this . display _message ( 'markingmessage' , 'loading' ) ;
for ( i = 0 ; i < len ; i ++ )
this . set _message ( a _uids [ i ] , 'deleted' , false ) ;
this . http _post ( 'mark' , post _data , lock ) ;
} ;
this . flag _as _deleted = function ( a _uids )
{
var r _uids = [ ] ,
post _data = this . selection _post _data ( { _uid : a _uids , _flag : 'delete' } ) ,
lock = this . display _message ( 'markingmessage' , 'loading' ) ,
list = this . message _list ,
rows = list ? list . rows : { } ,
count = 0 ,
display _next = this . check _display _next ( ) ;
for ( var i = 0 , len = a _uids . length ; i < len ; i ++ ) {
uid = a _uids [ i ] ;
if ( rows [ uid ] ) {
if ( rows [ uid ] . unread )
r _uids [ r _uids . length ] = uid ;
if ( this . env . skip _deleted ) {
count += this . update _thread ( uid ) ;
list . remove _row ( uid , display _next && i == list . get _selection ( false ) . length - 1 ) ;
}
else
this . set _message ( uid , 'deleted' , true ) ;
}
}
// make sure there are no selected rows
if ( this . env . skip _deleted && list ) {
if ( ! display _next || ! list . rowcount )
list . clear _selection ( ) ;
if ( count < 0 )
post _data . _count = ( count * - 1 ) ;
else if ( count > 0 )
// remove threads from the end of the list
this . delete _excessive _thread _rows ( ) ;
}
// set of messages to mark as seen
if ( r _uids . length )
post _data . _ruid = this . uids _to _list ( r _uids ) ;
if ( this . env . skip _deleted && this . env . display _next && this . env . next _uid )
post _data . _next _uid = this . env . next _uid ;
this . http _post ( 'mark' , post _data , lock ) ;
} ;
// flag as read without mark request (called from backend)
// argument should be a coma-separated list of uids
this . flag _deleted _as _read = function ( uids )
{
var uid , i , len ,
rows = this . message _list ? this . message _list . rows : { } ;
if ( typeof uids == 'string' )
uids = uids . split ( ',' ) ;
for ( i = 0 , len = uids . length ; i < len ; i ++ ) {
uid = uids [ i ] ;
if ( rows [ uid ] )
this . set _message ( uid , 'unread' , false ) ;
}
} ;
// Converts array of message UIDs to comma-separated list for use in URL
// with select_all mode checking
this . uids _to _list = function ( uids )
{
if ( this . select _all _mode )
return '*' ;
// multi-folder list of uids cannot be passed as a string (#6845)
if ( $ . isArray ( uids ) && ( uids . length == 1 || String ( uids [ 0 ] ) . indexOf ( '-' ) == - 1 ) )
uids = uids . join ( ',' ) ;
return uids ;
} ;
// Sets title of the delete button
this . set _button _titles = function ( )
{
var label = 'deletemessage' ;
if ( ! this . env . flag _for _deletion
&& this . env . trash _mailbox && this . env . mailbox != this . env . trash _mailbox
&& ( ! this . env . delete _junk || ! this . env . junk _mailbox || this . env . mailbox != this . env . junk _mailbox )
)
label = 'movemessagetotrash' ;
this . set _alttext ( 'delete' , label ) ;
} ;
// Initialize input element for list page jump
this . init _pagejumper = function ( element )
{
$ ( element ) . addClass ( 'rcpagejumper' )
. on ( 'focus' , function ( e ) {
// create and display popup with page selection
var i , html = '' ;
for ( i = 1 ; i <= ref . env . pagecount ; i ++ )
html += '<li>' + i + '</li>' ;
html = '<ul class="toolbarmenu menu">' + html + '</ul>' ;
if ( ! ref . pagejump ) {
ref . pagejump = $ ( '<div id="pagejump-selector" class="popupmenu"></div>' )
. appendTo ( document . body )
. on ( 'click' , 'li' , function ( ) {
if ( ! ref . busy )
$ ( element ) . val ( $ ( this ) . text ( ) ) . change ( ) ;
} ) ;
}
if ( ref . pagejump . data ( 'count' ) != i )
ref . pagejump . html ( html ) ;
ref . pagejump . attr ( 'rel' , '#' + this . id ) . data ( 'count' , i ) ;
// display page selector
ref . show _menu ( 'pagejump-selector' , true , e ) ;
$ ( this ) . keydown ( ) ;
} )
// keyboard navigation
. on ( 'keydown keyup click' , function ( e ) {
var current , selector = $ ( '#pagejump-selector' ) ,
ul = $ ( 'ul' , selector ) ,
list = $ ( 'li' , ul ) ,
height = ul . height ( ) ,
p = parseInt ( this . value ) ;
if ( e . which != 27 && e . which != 9 && e . which != 13 && ! selector . is ( ':visible' ) )
return ref . show _menu ( 'pagejump-selector' , true , e ) ;
if ( e . type == 'keydown' ) {
// arrow-down
if ( e . which == 40 ) {
if ( list . length > p )
this . value = ( p += 1 ) ;
}
// arrow-up
else if ( e . which == 38 ) {
if ( p > 1 && list . length > p - 1 )
this . value = ( p -= 1 ) ;
}
// enter
else if ( e . which == 13 ) {
return $ ( this ) . change ( ) ;
}
// esc, tab
else if ( e . which == 27 || e . which == 9 ) {
ref . hide _menu ( 'pagejump-selector' , e ) ;
return $ ( element ) . val ( ref . env . current _page ) ;
}
}
$ ( 'li.selected' , ul ) . removeClass ( 'selected' ) ;
if ( ( current = $ ( list [ p - 1 ] ) ) . length ) {
current . addClass ( 'selected' ) ;
$ ( '#pagejump-selector' ) . scrollTop ( ( ( ul . height ( ) / list . length ) * ( p - 1 ) ) - selector . height ( ) / 2 ) ;
}
} )
. on ( 'change' , function ( e ) {
// go to specified page
var p = parseInt ( this . value ) ;
if ( p && p != ref . env . current _page && ! ref . busy ) {
ref . hide _menu ( 'pagejump-selector' , e ) ;
ref . list _page ( p ) ;
}
} ) ;
} ;
// Update page-jumper state on list updates
this . update _pagejumper = function ( )
{
$ ( 'input.rcpagejumper' ) . val ( this . env . current _page ) . prop ( 'disabled' , this . env . pagecount < 2 ) ;
} ;
// check for mailvelope API
this . check _mailvelope = function ( action )
{
if ( typeof window . mailvelope !== 'undefined' ) {
this . mailvelope _load ( action ) ;
}
else {
$ ( window ) . on ( 'mailvelope' , function ( ) {
ref . mailvelope _load ( action ) ;
} ) ;
}
} ;
// Load Mailvelope functionality (and initialize keyring if needed)
this . mailvelope _load = function ( action )
{
if ( this . env . browser _capabilities )
this . env . browser _capabilities [ 'pgpmime' ] = 1 ;
var keyring = this . env . user _id ,
fn = function ( kr ) {
ref . mailvelope _keyring = kr ;
ref . mailvelope _init ( action , kr ) ;
} ;
mailvelope . getVersion ( ) . then ( function ( v ) {
mailvelope . VERSION = v ;
mailvelope . VERSION _MAJOR = Math . floor ( parseFloat ( v ) ) ;
return mailvelope . getKeyring ( keyring ) ;
} ) . then ( fn , function ( err ) {
// attempt to create a new keyring for this app/user
mailvelope . createKeyring ( keyring ) . then ( fn , function ( err ) {
console . error ( err ) ;
} ) ;
} ) ;
} ;
// Initializes Mailvelope editor or display container
this . mailvelope _init = function ( action , keyring )
{
if ( ! window . mailvelope )
return ;
if ( action == 'show' || action == 'preview' || action == 'print' ) {
// decrypt text body
if ( this . env . is _pgp _content ) {
var data = $ ( this . env . is _pgp _content ) . text ( ) ;
ref . mailvelope _display _container ( this . env . is _pgp _content , data , keyring ) ;
}
// load pgp/mime message and pass it to the mailvelope display container
else if ( this . env . pgp _mime _part ) {
var msgid = this . display _message ( 'loadingdata' , 'loading' ) ,
selector = this . env . pgp _mime _container ;
$ . ajax ( {
type : 'GET' ,
url : this . url ( 'get' , { '_mbox' : this . env . mailbox , '_uid' : this . env . uid , '_part' : this . env . pgp _mime _part } ) ,
error : function ( o , status , err ) {
ref . http _error ( o , status , err , msgid ) ;
} ,
success : function ( data ) {
ref . mailvelope _display _container ( selector , data , keyring , msgid ) ;
}
} ) ;
}
}
else if ( action == 'compose' ) {
this . env . compose _commands . push ( 'compose-encrypted' ) ;
var sign _supported = mailvelope . VERSION _MAJOR >= 2 ;
var is _html = $ ( '[name="_is_html"]' ) . val ( ) > 0 ;
if ( sign _supported )
this . env . compose _commands . push ( 'compose-encrypted-signed' ) ;
if ( this . env . pgp _mime _message ) {
// fetch PGP/Mime part and open load into Mailvelope editor
var lock = this . set _busy ( true , this . get _label ( 'loadingdata' ) ) ;
$ . ajax ( {
type : 'GET' ,
url : this . url ( 'get' , this . env . pgp _mime _message ) ,
error : function ( o , status , err ) {
ref . http _error ( o , status , err , lock ) ;
ref . enable _command ( 'compose-encrypted' , ! is _html ) ;
if ( sign _supported )
ref . enable _command ( 'compose-encrypted-signed' , ! is _html ) ;
} ,
success : function ( data ) {
ref . set _busy ( false , null , lock ) ;
if ( is _html ) {
ref . command ( 'toggle-editor' , { html : false , noconvert : true } ) ;
$ ( '#' + ref . env . composebody ) . val ( '' ) ;
}
ref . compose _encrypted ( { quotedMail : data } ) ;
ref . enable _command ( 'compose-encrypted' , true ) ;
ref . enable _command ( 'compose-encrypted-signed' , false ) ;
}
} ) ;
}
else {
// enable encrypted compose toggle
this . enable _command ( 'compose-encrypted' , ! is _html ) ;
if ( sign _supported )
this . enable _command ( 'compose-encrypted-signed' , ! is _html ) ;
}
// make sure to disable encryption button after toggling editor into HTML mode
this . addEventListener ( 'actionafter' , function ( args ) {
if ( args . ret && args . action == 'toggle-editor' ) {
ref . enable _command ( 'compose-encrypted' , ! args . props . html ) ;
if ( sign _supported )
ref . enable _command ( 'compose-encrypted-signed' , ! args . props . html ) ;
}
} ) ;
} else if ( action == 'edit-identity' ) {
ref . mailvelope _identity _keygen ( ) ;
}
} ;
// handler for the 'compose-encrypted-signed' command
this . compose _encrypted _signed = function ( props )
{
props = props || { } ;
props . signMsg = true ;
this . compose _encrypted ( props ) ;
} ;
// handler for the 'compose-encrypted' command
this . compose _encrypted = function ( props )
{
var options , container = $ ( '#' + this . env . composebody ) . parent ( ) ;
// remove Mailvelope editor if active
if ( ref . mailvelope _editor ) {
ref . mailvelope _editor = null ;
ref . compose _skip _unsavedcheck = false ;
ref . set _button ( 'compose-encrypted' , 'act' ) ;
container . removeClass ( 'mailvelope' )
. find ( 'iframe:not([aria-hidden=true])' ) . remove ( ) ;
$ ( '#' + ref . env . composebody ) . show ( ) ;
$ ( "[name='_pgpmime']" ) . remove ( ) ;
// re-enable commands that operate on the compose body
ref . enable _command ( 'toggle-editor' , 'insert-response' , 'save-response' , true ) ;
ref . enable _command ( 'spellcheck' , ! ! window . googie ) ;
ref . enable _command ( 'insert-sig' , ! ! ( ref . env . signatures && ref . env . identity && ref . env . signatures [ ref . env . identity ] ) ) ;
ref . triggerEvent ( 'compose-encrypted' , { active : false } ) ;
}
// embed Mailvelope editor container
else {
if ( this . spellcheck _state ( ) )
this . editor . spellcheck _stop ( ) ;
if ( props . quotedMail ) {
options = { quotedMail : props . quotedMail , quotedMailIndent : false } ;
}
else {
options = { predefinedText : $ ( '#' + this . env . composebody ) . val ( ) } ;
}
if ( props . signMsg ) {
options . signMsg = props . signMsg ;
}
if ( this . env . compose _mode == 'reply' ) {
options . quotedMailIndent = true ;
options . quotedMailHeader = this . env . compose _reply _header ;
}
mailvelope . createEditorContainer ( '#' + container . attr ( 'id' ) , ref . mailvelope _keyring , options ) . then ( function ( editor ) {
ref . mailvelope _editor = editor ;
ref . compose _skip _unsavedcheck = true ;
ref . set _button ( 'compose-encrypted' , 'sel' ) ;
container . addClass ( 'mailvelope' ) ;
$ ( '#' + ref . env . composebody ) . hide ( ) ;
// disable commands that operate on the compose body
ref . enable _command ( 'spellcheck' , 'insert-sig' , 'toggle-editor' , 'insert-response' , 'save-response' , false ) ;
ref . triggerEvent ( 'compose-encrypted' , { active : true } ) ;
// notify user about loosing attachments
if ( ref . env . attachments && ! $ . isEmptyObject ( ref . env . attachments ) ) {
ref . alert _dialog ( ref . get _label ( 'encryptnoattachments' ) ) ;
$ . each ( ref . env . attachments , function ( name , attach ) {
ref . remove _from _attachment _list ( name ) ;
} ) ;
}
} , function ( err ) {
console . error ( err ) ;
console . log ( options ) ;
} ) ;
}
} ;
// callback to replace the message body with the full armored
this . mailvelope _submit _messageform = function ( draft , saveonly )
{
// get recipients
var recipients = [ ] ;
$ . each ( [ 'to' , 'cc' , 'bcc' ] , function ( i , field ) {
var pos , rcpt , val = $ ( '[name="_' + field + '"]' ) . val ( ) . trim ( ) ;
while ( val . length && rcube _check _email ( val , true ) ) {
rcpt = RegExp . $2 . replace ( /^<+/ , '' ) . replace ( />+$/ , '' ) ;
recipients . push ( rcpt ) ;
val = val . substr ( val . indexOf ( rcpt ) + rcpt . length + 1 ) . replace ( /^\s*,\s*/ , '' ) ;
}
} ) ;
// check if we have keys for all recipients
var isvalid = recipients . length > 0 ;
ref . mailvelope _keyring . validKeyForAddress ( recipients ) . then ( function ( status ) {
var missing _keys = [ ] ;
$ . each ( status , function ( k , v ) {
if ( v === false ) {
isvalid = false ;
missing _keys . push ( k ) ;
}
} ) ;
// list recipients with missing keys
if ( ! isvalid && missing _keys . length ) {
// display dialog with missing keys
ref . simple _dialog (
ref . get _label ( 'nopubkeyfor' ) . replace ( '$email' , missing _keys . join ( ', ' ) ) +
'<p>' + ref . get _label ( 'searchpubkeyservers' ) + '</p>' ,
'encryptedsendialog' ,
function ( ) {
ref . mailvelope _search _pubkeys ( missing _keys , function ( ) {
return true ; // close dialog
} ) ;
} ,
{ button : 'search' }
) ;
return false ;
}
if ( ! isvalid ) {
if ( ! recipients . length ) {
ref . alert _dialog ( ref . get _label ( 'norecipientwarning' ) , function ( ) {
$ ( "[name='_to']" ) . focus ( ) ;
} ) ;
}
return false ;
}
// add sender identity to recipients to be able to decrypt our very own message
var senders = [ ] , selected _sender = ref . env . identities [ $ ( "[name='_from'] option:selected" ) . val ( ) ] ;
$ . each ( ref . env . identities , function ( k , sender ) {
senders . push ( sender . email ) ;
} ) ;
ref . mailvelope _keyring . validKeyForAddress ( senders ) . then ( function ( status ) {
valid _sender = null ;
$ . each ( status , function ( k , v ) {
if ( v !== false ) {
valid _sender = k ;
if ( valid _sender == selected _sender ) {
return false ; // break
}
}
} ) ;
if ( ! valid _sender ) {
if ( ! confirm ( ref . get _label ( 'nopubkeyforsender' ) ) ) {
return false ;
}
}
recipients . push ( valid _sender ) ;
ref . mailvelope _editor . encrypt ( recipients ) . then ( function ( armored ) {
// all checks passed, send message
var form = ref . gui _objects . messageform ,
hidden = $ ( "[name='_pgpmime']" , form ) ,
msgid = ref . set _busy ( true , draft || saveonly ? 'savingmessage' : 'sendingmessage' ) ;
form . target = ref . get _save _target ( ) ;
form . _draft . value = draft ? '1' : '' ;
form . action = ref . add _url ( form . action , '_unlock' , msgid ) ;
form . action = ref . add _url ( form . action , '_framed' , 1 ) ;
if ( saveonly ) {
form . action = ref . add _url ( form . action , '_saveonly' , 1 ) ;
}
// send pgp conent via hidden field
if ( ! hidden . length ) {
hidden = $ ( '<input type="hidden" name="_pgpmime">' ) . appendTo ( form ) ;
}
hidden . val ( armored ) ;
form . submit ( ) ;
} , function ( err ) {
console . log ( err ) ;
} ) ; // mailvelope_editor.encrypt()
} , function ( err ) {
console . error ( err ) ;
} ) ; // mailvelope_keyring.validKeyForAddress(senders)
} , function ( err ) {
console . error ( err ) ;
} ) ; // mailvelope_keyring.validKeyForAddress(recipients)
return false ;
} ;
// wrapper for the mailvelope.createDisplayContainer API call
this . mailvelope _display _container = function ( selector , data , keyring , msgid )
{
var error _handler = function ( error ) {
// remove mailvelope frame with the error message
$ ( selector + ' > iframe' ) . remove ( ) ;
ref . hide _message ( msgid ) ;
ref . display _message ( error . message , 'error' ) ;
} ;
mailvelope . createDisplayContainer ( selector , data , keyring , { senderAddress : this . env . sender } ) . then ( function ( status ) {
if ( status . error && status . error . message ) {
return error _handler ( status . error ) ;
}
ref . hide _message ( msgid ) ;
$ ( selector ) . addClass ( 'mailvelope' ) . children ( ) . not ( 'iframe' ) . hide ( ) ;
// on success we can remove encrypted part from the attachments list
if ( ref . env . pgp _mime _part )
$ ( '#attach' + ref . env . pgp _mime _part ) . remove ( ) ;
setTimeout ( function ( ) { $ ( window ) . resize ( ) ; } , 10 ) ;
} , error _handler ) ;
} ;
// subroutine to query keyservers for public keys
this . mailvelope _search _pubkeys = function ( emails , resolve , import _handler )
{
// query with publickey.js
var deferreds = [ ] ,
pk = new PublicKey ( this . env . keyservers ) ,
lock = ref . display _message ( '' , 'loading' ) ;
$ . each ( emails , function ( i , email ) {
var d = $ . Deferred ( ) ;
pk . search ( email , function ( results , errorCode ) {
if ( errorCode !== null ) {
// rejecting would make all fail
// d.reject(email);
d . resolve ( [ email ] ) ;
}
else {
d . resolve ( [ email ] . concat ( results ) ) ;
}
} ) ;
deferreds . push ( d ) ;
} ) ;
$ . when . apply ( $ , deferreds ) . then ( function ( ) {
var missing _keys = [ ] ,
key _selection = [ ] ;
// alanyze results of all queries
$ . each ( arguments , function ( i , result ) {
var email = result . shift ( ) ;
if ( ! result . length ) {
missing _keys . push ( email ) ;
}
else {
key _selection = key _selection . concat ( result ) ;
}
} ) ;
ref . hide _message ( lock ) ;
resolve ( true ) ;
// show key import dialog
if ( key _selection . length ) {
ref . mailvelope _key _import _dialog ( key _selection , import _handler ) ;
}
// some keys could not be found
if ( missing _keys . length ) {
ref . display _message ( ref . get _label ( 'nopubkeyfor' ) . replace ( '$email' , missing _keys . join ( ', ' ) ) , 'warning' ) ;
}
} ) . fail ( function ( ) {
console . error ( 'Pubkey lookup failed with' , arguments ) ;
ref . hide _message ( lock ) ;
ref . display _message ( 'pubkeysearcherror' , 'error' ) ;
resolve ( false ) ;
} ) ;
} ;
// list the given public keys in a dialog with options to import
// them into the local Maivelope keyring
this . mailvelope _key _import _dialog = function ( candidates , import _handler )
{
var ul = $ ( '<div>' ) . addClass ( 'listing pgpkeyimport' ) ;
$ . each ( candidates , function ( i , keyrec ) {
var li = $ ( '<div>' ) . addClass ( 'key' ) ;
if ( keyrec . revoked ) li . addClass ( 'revoked' ) ;
if ( keyrec . disabled ) li . addClass ( 'disabled' ) ;
if ( keyrec . expired ) li . addClass ( 'expired' ) ;
li . append ( $ ( '<label>' ) . addClass ( 'keyid' ) . text ( ref . get _label ( 'keyid' ) ) ) ;
li . append ( $ ( '<a>' ) . text ( keyrec . keyid . substr ( - 8 ) . toUpperCase ( ) )
. attr ( { href : keyrec . info , target : '_blank' , tabindex : '-1' } ) ) ;
li . append ( $ ( '<label>' ) . addClass ( 'keylen' ) . text ( ref . get _label ( 'keylength' ) ) ) ;
li . append ( $ ( '<span>' ) . text ( keyrec . keylen ) ) ;
if ( keyrec . expirationdate ) {
li . append ( $ ( '<label>' ) . addClass ( 'keyexpired' ) . text ( ref . get _label ( 'keyexpired' ) ) ) ;
li . append ( $ ( '<span>' ) . text ( new Date ( keyrec . expirationdate * 1000 ) . toDateString ( ) ) ) ;
}
if ( keyrec . revoked ) {
li . append ( $ ( '<span>' ) . addClass ( 'keyrevoked' ) . text ( ref . get _label ( 'keyrevoked' ) ) ) ;
}
var ul _ = $ ( '<ul>' ) . addClass ( 'uids' ) ;
$ . each ( keyrec . uids , function ( j , uid ) {
var li _ = $ ( '<li>' ) . addClass ( 'uid' ) ;
if ( uid . revoked ) li _ . addClass ( 'revoked' ) ;
if ( uid . disabled ) li _ . addClass ( 'disabled' ) ;
if ( uid . expired ) li _ . addClass ( 'expired' ) ;
ul _ . append ( li _ . text ( uid . uid ) ) ;
} ) ;
li . append ( ul _ ) ;
li . append ( $ ( '<button>' )
. attr ( 'rel' , keyrec . keyid )
. text ( ref . get _label ( 'import' ) )
. addClass ( 'button import importkey' )
. prop ( 'disabled' , keyrec . revoked || keyrec . disabled || keyrec . expired ) ) ;
ul . append ( li ) ;
} ) ;
// display dialog with missing keys
ref . simple _dialog (
$ ( '<div>' )
. append ( $ ( '<p>' ) . html ( ref . get _label ( 'encryptpubkeysfound' ) ) )
. append ( ul ) ,
ref . get _label ( 'importpubkeys' ) ,
null ,
{ cancel _label : 'close' , cancel _button : 'close' }
) ;
// delegate handler for import button clicks
ul . on ( 'click' , 'button.importkey' , function ( ) {
var btn = $ ( this ) ,
keyid = btn . attr ( 'rel' ) ,
pk = new PublicKey ( ref . env . keyservers ) ,
lock = ref . display _message ( '' , 'loading' ) ;
// fetch from keyserver and import to Mailvelope keyring
pk . get ( keyid , function ( armored , errorCode ) {
ref . hide _message ( lock ) ;
if ( errorCode ) {
ref . display _message ( 'keyservererror' , 'error' ) ;
return ;
}
if ( import _handler ) {
import _handler ( armored ) ;
return ;
}
// import to keyring
ref . mailvelope _keyring . importPublicKey ( armored ) . then ( function ( status ) {
if ( status === 'REJECTED' ) {
// ref.alert_dialog(ref.get_label('Key import was rejected'));
}
else {
var $key = keyid . substr ( - 8 ) . toUpperCase ( ) ;
btn . closest ( '.key' ) . fadeOut ( ) ;
ref . display _message ( ref . get _label ( 'keyimportsuccess' ) . replace ( '$key' , $key ) , 'confirmation' ) ;
}
} , function ( err ) {
console . log ( err ) ;
} ) ;
} ) ;
} ) ;
} ;
// enable key management for identity
this . mailvelope _identity _keygen = function ( )
{
var container = $ ( this . gui _objects . editform ) . find ( '.identity-encryption' ) . first ( ) ;
var identity _email = $ ( this . gui _objects . editform ) . find ( '.ff_email' ) . val ( ) . trim ( ) ;
if ( ! container . length || ! identity _email || ! this . mailvelope _keyring . createKeyGenContainer )
return ;
var key _fingerprint ;
this . mailvelope _keyring . validKeyForAddress ( [ identity _email ] )
. then ( function ( keys ) {
var private _keys = [ ] ;
if ( keys && keys [ identity _email ] && Array . isArray ( keys [ identity _email ] . keys ) ) {
var checks = [ ] ;
for ( var j = 0 ; j < keys [ identity _email ] . keys . length ; j ++ ) {
checks . push ( ( function ( key ) {
return ref . mailvelope _keyring . hasPrivateKey ( key . fingerprint )
. then ( function ( found ) {
if ( found ) {
private _keys . push ( key ) ;
}
} ) ;
} ) ( keys [ identity _email ] . keys [ j ] ) ) ;
}
return Promise . all ( checks )
. then ( function ( ) {
return private _keys ;
} ) ;
}
return private _keys ;
} ) . then ( function ( private _keys ) {
var content = container . find ( '.identity-encryption-block' ) . empty ( ) ;
if ( private _keys && private _keys . length ) {
// show private key information
$ ( '<p>' ) . text ( ref . get _label ( 'encryptionprivkeysinmailvelope' ) . replace ( '$nr' , private _keys . length ) ) . appendTo ( content ) ;
var ul = $ ( '<ul>' ) . addClass ( 'keylist' ) . appendTo ( content ) ;
$ . each ( private _keys , function ( i , key ) {
$ ( '<li>' ) . appendTo ( ul )
. append ( $ ( '<strong>' ) . addClass ( 'fingerprint' ) . text ( String ( key . fingerprint ) . toUpperCase ( ) ) )
. append ( $ ( '<span>' ) . addClass ( 'identity' ) . text ( '<' + identity _email + '> ' ) ) ;
} ) ;
} else {
$ ( '<p>' ) . text ( ref . get _label ( 'encryptionnoprivkeysinmailvelope' ) ) . appendTo ( content ) ;
}
// show button to create a new key
$ ( '<button>' )
. attr ( 'type' , 'button' )
. addClass ( 'button create' )
. text ( ref . get _label ( 'encryptioncreatekey' ) )
. appendTo ( content )
. on ( 'click' , function ( ) { ref . mailvelope _show _keygen _container ( content , identity _email ) ; } ) ;
$ ( '<span>' ) . addClass ( 'space' ) . html ( ' ' ) . appendTo ( content ) ;
$ ( '<button>' )
. attr ( 'type' , 'button' )
. addClass ( 'button settings' )
. text ( ref . get _label ( 'openmailvelopesettings' ) )
. appendTo ( content )
. on ( 'click' , function ( ) { ref . mailvelope _keyring . openSettings ( ) ; } ) ;
container . show ( ) ;
ref . triggerEvent ( 'identity-encryption-show' , { container : container } ) ;
} )
. catch ( function ( err ) {
console . error ( 'Mailvelope keyring error' , err ) ;
} )
} ;
// start pgp key generation using Mailvelope
this . mailvelope _show _keygen _container = function ( container , identity _email )
{
var cid = new Date ( ) . getTime ( ) ;
var user _id = { email : identity _email , fullName : $ ( ref . gui _objects . editform ) . find ( '.ff_name' ) . val ( ) . trim ( ) } ;
var options = { userIds : [ user _id ] , keySize : 4096 } ;
$ ( '<div>' ) . attr ( 'id' , 'mailvelope-keygen-container-' + cid )
. css ( { height : '245px' , marginBottom : '10px' } )
. appendTo ( container . empty ( ) ) ;
this . mailvelope _keyring . createKeyGenContainer ( '#mailvelope-keygen-container-' + cid , options )
. then ( function ( generator ) {
if ( generator instanceof Error ) {
throw generator ;
}
// append button to start key generation
$ ( '<button>' )
. attr ( 'type' , 'button' )
. addClass ( 'button mainaction generate' )
. text ( ref . get _label ( 'generate' ) )
. appendTo ( container )
. on ( 'click' , function ( ) {
var btn = $ ( this ) . prop ( 'disabled' , true ) ;
generator . generate ( )
. then ( function ( result ) {
if ( typeof result === 'string' && result . indexOf ( 'BEGIN PGP' ) > 0 ) {
ref . display _message ( ref . get _label ( 'keypaircreatesuccess' ) . replace ( '$identity' , identity _email ) , 'confirmation' ) ;
// reset keygen view
ref . mailvelope _identity _keygen ( ) ;
}
} )
. catch ( function ( err ) {
debugger ;
ref . display _message ( err . message || 'errortitle' , 'error' ) ;
btn . prop ( 'disabled' , false ) ;
} ) ;
} ) ;
$ ( '<span>' ) . addClass ( 'space' ) . html ( ' ' ) . appendTo ( container ) ;
$ ( '<button>' )
. attr ( 'type' , 'button' )
. addClass ( 'button cancel' )
. text ( ref . get _label ( 'cancel' ) )
. appendTo ( container )
. on ( 'click' , function ( ) { ref . mailvelope _identity _keygen ( ) ; } ) ;
ref . triggerEvent ( 'identity-encryption-update' , { container : container } ) ;
} )
. catch ( function ( err ) {
ref . display _message ( 'errortitle' , 'error' ) ;
// start over
ref . mailvelope _identity _keygen ( ) ;
} ) ;
} ;
/*********************************************************/
/********* mailbox folders methods *********/
/*********************************************************/
this . expunge _mailbox = function ( mbox )
{
var lock , post _data = { _mbox : mbox } ;
// lock interface if it's the active mailbox
if ( mbox == this . env . mailbox ) {
lock = this . set _busy ( true , 'loading' ) ;
post _data . _reload = 1 ;
if ( this . env . search _request )
post _data . _search = this . env . search _request ;
}
// send request to server
this . http _post ( 'expunge' , post _data , lock ) ;
} ;
this . purge _mailbox = function ( mbox )
{
this . confirm _dialog ( this . get _label ( 'purgefolderconfirm' ) , 'delete' , function ( ) {
var lock , post _data = { _mbox : mbox } ;
// lock interface if it's the active mailbox
if ( mbox == ref . env . mailbox ) {
lock = ref . set _busy ( true , 'loading' ) ;
post _data . _reload = 1 ;
}
// send request to server
ref . http _post ( 'purge' , post _data , lock ) ;
} ) ;
return false ;
} ;
// test if purge command is allowed
this . purge _mailbox _test = function ( )
{
return ( this . env . exists && (
this . env . mailbox == this . env . trash _mailbox
|| this . env . mailbox == this . env . junk _mailbox
|| this . env . mailbox . startsWith ( this . env . trash _mailbox + this . env . delimiter )
|| this . env . mailbox . startsWith ( this . env . junk _mailbox + this . env . delimiter )
) ) ;
} ;
// Mark all messages as read in:
// - selected folder (mode=cur)
// - selected folder and its subfolders (mode=sub)
// - all folders (mode=all)
this . mark _all _read = function ( mbox , mode )
{
var state , content , nodes = [ ] ,
list = this . message _list ,
folder = mbox || this . env . mailbox ,
post _data = { _uid : '*' , _flag : 'read' , _mbox : folder , _folders : mode } ;
if ( typeof mode != 'string' ) {
state = this . mark _all _read _state ( folder ) ;
if ( ! state )
return ;
if ( state > 1 ) {
// build content of the dialog
$ . each ( { cur : 1 , sub : 2 , all : 4 } , function ( i , v ) {
var id = 'readallmode' + i ,
label = $ ( '<label>' ) . attr ( 'for' , id ) . text ( ref . get _label ( 'folders-' + i ) ) ,
input = $ ( '<input>' ) . attr ( { type : 'radio' , value : i , name : 'mode' , id : id , disabled : ! ( state & v ) } ) ;
nodes . push ( $ ( '<li>' ) . append ( [ input , label ] ) ) ;
} ) ;
content = $ ( '<ul class="proplist">' ) . append ( nodes ) ;
$ ( 'input:not([disabled])' , content ) . first ( ) . attr ( 'checked' , true ) ;
this . simple _dialog ( content , this . get _label ( 'markallread' ) ,
function ( ) {
ref . mark _all _read ( folder , $ ( 'input:checked' , content ) . val ( ) ) ;
return true ;
} ,
{ button : 'mark' , button _class : 'save' }
) ;
return ;
}
post _data . _folders = 'cur' ; // only current folder has unread messages
}
// mark messages on the list
$ . each ( list ? list . rows : [ ] , function ( uid , row ) {
if ( ! row . unread )
return ;
var mbox = ref . env . messages [ uid ] . mbox ;
if ( mode == 'all' || mbox == ref . env . mailbox
|| ( mode == 'sub' && mbox . startsWith ( ref . env . mailbox + ref . env . delimiter ) )
) {
ref . set _message ( uid , 'unread' , false ) ;
}
} ) ;
// send the request
this . http _post ( 'mark' , post _data , this . display _message ( 'markingmessage' , 'loading' ) ) ;
} ;
// Enable/disable mark-all-read action depending on folders state
this . mark _all _read _state = function ( mbox )
{
var state = 0 ,
li = this . treelist . get _item ( mbox || this . env . mailbox ) ,
folder _item = $ ( li ) . is ( '.unread' ) ? 1 : 0 ,
subfolder _items = $ ( 'li.unread' , li ) . length ,
all _items = $ ( 'li.unread' , ref . gui _objects . folderlist ) . length ;
state += folder _item ;
state += subfolder _items ? 2 : 0 ;
state += all _items > folder _item + subfolder _items ? 4 : 0 ;
this . enable _command ( 'mark-all-read' , state > 0 ) ;
return state ;
} ;
// Display "bounce message" dialog
this . bounce = function ( props , obj , event )
{
// get message uid and folder
var uid = this . get _single _uid ( ) ,
url = this . url ( 'bounce' , { _framed : 1 , _uid : uid , _mbox : this . get _message _mailbox ( uid ) } ) ,
dialog = $ ( '<iframe>' ) . attr ( 'src' , url ) ,
get _form = function ( ) {
var rc = $ ( 'iframe' , dialog ) [ 0 ] . contentWindow . rcmail ;
return { rc : rc , form : rc . gui _objects . messageform } ;
} ,
post _func = function ( ) {
var post = { } , form = get _form ( ) ;
$ . each ( $ ( form . form ) . serializeArray ( ) , function ( ) { post [ this . name ] = this . value ; } ) ;
post . _uid = form . rc . env . uid ;
post . _mbox = form . rc . env . mailbox ;
delete post . _action ;
delete post . _task ;
if ( post . _to || post . _cc || post . _bcc ) {
ref . http _post ( 'bounce' , post , ref . set _busy ( true , 'sendingmessage' ) ) ;
dialog . dialog ( 'close' ) ;
}
} ,
submit _func = function ( ) {
var form = get _form ( ) ;
if ( typeof form . form != 'object' )
return false ;
if ( ! form . rc . check _compose _address _fields ( post _func , form . form ) )
return false ;
return post _func ( ) ;
} ;
this . hide _menu ( 'forwardmenu' , event ) ;
dialog = this . simple _dialog ( dialog , this . gettext ( 'bouncemsg' ) , submit _func , {
button : 'bounce' ,
width : 400 ,
height : 300
} ) ;
return true ;
} ;
/*********************************************************/
/********* message compose methods *********/
/*********************************************************/
this . open _compose _step = function ( p )
{
var url = this . url ( 'mail/compose' , p ) ;
// open new compose window
if ( this . env . compose _extwin && ! this . env . extwin ) {
this . open _window ( url ) ;
}
else {
this . redirect ( url ) ;
if ( this . env . extwin )
window . resizeTo ( Math . max ( this . env . popup _width , $ ( window ) . width ( ) ) , $ ( window ) . height ( ) + 24 ) ;
}
} ;
// init message compose form: set focus and eventhandlers
this . init _messageform = function ( )
{
if ( ! this . gui _objects . messageform )
return false ;
var elem , pos ,
input _from = $ ( "[name='_from']" ) ,
input _to = $ ( "[name='_to']" ) ,
input _subject = $ ( "[name='_subject']" ) ,
input _message = $ ( "[name='_message']" ) . get ( 0 ) ,
html _mode = $ ( "[name='_is_html']" ) . val ( ) == '1' ,
opener _rc = this . opener ( ) ;
// close compose step in opener
if ( opener _rc && opener _rc . env . action == 'compose' ) {
setTimeout ( function ( ) {
if ( opener . history . length > 1 )
opener . history . back ( ) ;
else
opener _rc . redirect ( opener _rc . get _task _url ( 'mail' ) ) ;
} , 100 ) ;
this . env . opened _extwin = true ;
}
if ( ! html _mode ) {
// On Back button Chrome will overwrite textarea with old content
// causing e.g. the same signature is added twice (#5809)
if ( input _message . value && input _message . defaultValue !== undefined )
input _message . value = input _message . defaultValue ;
pos = this . env . top _posting && this . env . compose _mode ? 0 : input _message . value . length ;
// add signature according to selected identity
// if we have HTML editor, signature is added in a callback
if ( input _from . prop ( 'type' ) == 'select-one' ) {
// for some reason the caret initially is not at pos=0 in Firefox 51 (#5628)
this . set _caret _pos ( input _message , 0 ) ;
this . change _identity ( input _from [ 0 ] ) ;
}
// set initial cursor position
this . set _caret _pos ( input _message , pos ) ;
// scroll to the bottom of the textarea (#1490114)
if ( pos ) {
$ ( input _message ) . scrollTop ( input _message . scrollHeight ) ;
}
}
// check for locally stored compose data
if ( this . env . save _localstorage )
this . compose _restore _dialog ( 0 , html _mode )
if ( input _to . val ( ) == '' )
elem = input _to ;
else if ( input _subject . val ( ) == '' )
elem = input _subject ;
else if ( input _message )
elem = input _message ;
this . env . compose _focus _elem = this . init _messageform _inputs ( elem ) ;
// get summary of all field values
this . compose _field _hash ( true ) ;
// start the auto-save timer
this . auto _save _start ( ) ;
} ;
// init autocomplete events on compose form inputs
this . init _messageform _inputs = function ( focused )
{
var i ,
input _to = $ ( "[name='_to']" ) ,
ac _fields = [ 'cc' , 'bcc' , 'replyto' , 'followupto' ] ;
// init live search events
this . init _address _input _events ( input _to ) ;
for ( i in ac _fields ) {
this . init _address _input _events ( $ ( "[name='_" + ac _fields [ i ] + "']" ) ) ;
}
if ( ! focused )
focused = input _to ;
// focus first empty element (and return it)
return $ ( focused ) . focus ( ) . get ( 0 ) ;
} ;
this . compose _restore _dialog = function ( j , html _mode )
{
var i , key , formdata , index = this . local _storage _get _item ( 'compose.index' , [ ] ) ;
var show _next = function ( i ) {
if ( ++ i < index . length )
ref . compose _restore _dialog ( i , html _mode )
}
for ( i = j || 0 ; i < index . length ; i ++ ) {
key = index [ i ] ;
formdata = this . local _storage _get _item ( 'compose.' + key , null , true ) ;
if ( ! formdata ) {
continue ;
}
// restore saved copy of current compose_id
if ( formdata . changed && key == this . env . compose _id ) {
this . restore _compose _form ( key , html _mode ) ;
break ;
}
// skip records from 'other' drafts
if ( this . env . draft _id && formdata . draft _id && formdata . draft _id != this . env . draft _id ) {
continue ;
}
// skip records on reply
if ( this . env . reply _msgid && formdata . reply _msgid != this . env . reply _msgid ) {
continue ;
}
// show dialog asking to restore the message
if ( formdata . changed && formdata . session != this . env . session _id ) {
this . show _popup _dialog (
this . get _label ( 'restoresavedcomposedata' )
. replace ( '$date' , new Date ( formdata . changed ) . toLocaleString ( ) )
. replace ( '$subject' , formdata . _subject )
. replace ( /\n/g , '<br/>' ) ,
this . get _label ( 'restoremessage' ) ,
[ {
text : this . get _label ( 'restore' ) ,
'class' : 'mainaction restore' ,
click : function ( ) {
ref . restore _compose _form ( key , html _mode ) ;
ref . remove _compose _data ( key ) ; // remove old copy
ref . save _compose _form _local ( ) ; // save under current compose_id
$ ( this ) . dialog ( 'close' ) ;
}
} ,
{
text : this . get _label ( 'delete' ) ,
'class' : 'delete' ,
click : function ( ) {
ref . remove _compose _data ( key ) ;
$ ( this ) . dialog ( 'close' ) ;
show _next ( i ) ;
}
} ,
{
text : this . get _label ( 'ignore' ) ,
'class' : 'cancel' ,
click : function ( ) {
$ ( this ) . dialog ( 'close' ) ;
show _next ( i ) ;
}
} ]
) ;
break ;
}
}
}
this . init _address _input _events = function ( obj , props )
{
// configure parallel autocompletion
if ( ! props && this . env . autocomplete _threads > 0 ) {
props = {
threads : this . env . autocomplete _threads ,
sources : this . env . autocomplete _sources
} ;
}
obj . keydown ( function ( e ) { return ref . ksearch _keydown ( e , this , props ) ; } )
. attr ( { autocomplete : 'off' , 'aria-autocomplete' : 'list' , 'aria-expanded' : 'false' , role : 'combobox' } ) ;
// hide the popup on any click
var callback = function ( ) { ref . ksearch _hide ( ) ; } ;
$ ( document ) . on ( 'click' , callback ) ;
// and on scroll (that cannot be jQuery.on())
document . addEventListener ( 'scroll' , callback , true ) ;
} ;
this . submit _messageform = function ( draft , saveonly )
{
var form = this . gui _objects . messageform ;
if ( ! form )
return ;
// the message has been sent but not saved, ask the user what to do
if ( ! saveonly && this . env . is _sent ) {
return this . simple _dialog ( this . get _label ( 'messageissent' ) , '' , // TODO: dialog title
function ( ) {
ref . submit _messageform ( false , true ) ;
return true ;
}
) ;
}
// delegate sending to Mailvelope routine
if ( this . mailvelope _editor ) {
return this . mailvelope _submit _messageform ( draft , saveonly ) ;
}
// all checks passed, send message
var msgid = this . set _busy ( true , draft || saveonly ? 'savingmessage' : 'sendingmessage' ) ,
lang = this . spellcheck _lang ( ) ,
files = [ ] ;
// send files list
$ ( 'li' , this . gui _objects . attachmentlist ) . each ( function ( ) { files . push ( this . id . replace ( /^rcmfile/ , '' ) ) ; } ) ;
$ ( '[name="_attachments"]' , form ) . val ( files . join ( ) ) ;
form . target = this . get _save _target ( ) ;
form . _draft . value = draft ? '1' : '' ;
form . action = this . add _url ( form . action , '_unlock' , msgid ) ;
form . action = this . add _url ( form . action , '_framed' , 1 ) ;
if ( lang )
form . action = this . add _url ( form . action , '_lang' , lang ) ;
if ( saveonly )
form . action = this . add _url ( form . action , '_saveonly' , 1 ) ;
// register timer to notify about connection timeout
this . submit _timer = setTimeout ( function ( ) {
ref . set _busy ( false , null , msgid ) ;
ref . display _message ( 'requesttimedout' , 'error' ) ;
} , this . env . request _timeout * 1000 ) ;
form . submit ( ) ;
} ;
this . compose _recipient _select = function ( list )
{
var id , n , recipients = 0 , selection = list . get _selection ( ) ;
for ( n = 0 ; n < selection . length ; n ++ ) {
id = selection [ n ] ;
if ( this . env . contactdata [ id ] )
recipients ++ ;
}
this . enable _command ( 'add-recipient' , recipients ) ;
} ;
this . compose _add _recipient = function ( field )
{
// find last focused field name
if ( ! field ) {
field = $ ( this . env . focused _field ) . filter ( ':visible' ) ;
field = field . length ? field . attr ( 'id' ) . replace ( '_' , '' ) : 'to' ;
}
var recipients = [ ] , input = $ ( '#_' + field ) , selection = this . contact _list . get _selection ( ) ;
if ( this . contact _list && selection . length ) {
for ( var id , n = 0 ; n < selection . length ; n ++ ) {
id = selection [ n ] ;
if ( id && this . env . contactdata [ id ] ) {
recipients . push ( this . env . contactdata [ id ] ) ;
// group is added, expand it
if ( id . charAt ( 0 ) == 'E' && this . env . contactdata [ id ] . indexOf ( '@' ) < 0 && input . length ) {
var gid = id . substr ( 1 ) ;
this . group2expand [ gid ] = { name : this . env . contactdata [ id ] , input : input . get ( 0 ) } ;
this . http _request ( 'group-expand' , { _source : this . env . source , _gid : gid } , false ) ;
}
}
}
}
if ( recipients . length && input . length ) {
var oldval = input . val ( ) ;
if ( oldval && ! /[,;]\s*$/ . test ( oldval ) )
oldval += ', ' ;
input . val ( oldval + recipients . join ( ', ' ) + ', ' ) . change ( ) ;
this . triggerEvent ( 'add-recipient' , { field : field , recipients : recipients } ) ;
}
return recipients . length ;
} ;
// checks the input fields before sending a message
this . check _compose _input = function ( cmd )
{
var key ,
input _subject = $ ( "[name='_subject']" ) ;
// check if all files has been uploaded
for ( key in this . env . attachments ) {
if ( typeof this . env . attachments [ key ] === 'object' && ! this . env . attachments [ key ] . complete ) {
this . alert _dialog ( this . get _label ( 'notuploadedwarning' ) ) ;
return false ;
}
}
// display localized warning for missing subject
if ( ! this . env . nosubject _warned && input _subject . val ( ) == '' ) {
var dialog ,
prompt _value = $ ( '<input>' ) . attr ( { type : 'text' , size : 40 } ) ,
myprompt = $ ( '<div class="prompt">' )
. append ( $ ( '<p class="message">' ) . text ( this . get _label ( 'nosubjectwarning' ) ) )
. append ( prompt _value ) ,
save _func = function ( ) {
input _subject . val ( prompt _value . val ( ) ) ;
dialog . dialog ( 'close' ) ;
if ( ref . check _compose _input ( cmd ) )
ref . command ( cmd , { nocheck : true } ) ; // repeat command which triggered this
} ;
dialog = this . show _popup _dialog (
myprompt ,
this . get _label ( 'nosubjecttitle' ) ,
[ {
text : this . get _label ( 'sendmessage' ) ,
'class' : 'mainaction send' ,
click : function ( ) { save _func ( ) ; }
} , {
text : this . get _label ( 'cancel' ) ,
'class' : 'cancel' ,
click : function ( ) {
input _subject . focus ( ) ;
dialog . dialog ( 'close' ) ;
}
} ] ,
{ dialogClass : 'warning' }
) ;
prompt _value . select ( ) . keydown ( function ( e ) {
if ( e . which == 13 ) save _func ( ) ;
} ) ;
this . env . nosubject _warned = true ;
return false ;
}
// check for empty body (only possible if not mailvelope encrypted)
if ( ! this . mailvelope _editor && ! this . editor . get _content ( ) && ! confirm ( this . get _label ( 'nobodywarning' ) ) ) {
this . editor . focus ( ) ;
return false ;
}
if ( ! this . check _compose _address _fields ( cmd ) )
return false ;
// move body from html editor to textarea (just to be sure, #1485860)
this . editor . save ( ) ;
return true ;
} ;
this . check _compose _address _fields = function ( cmd , form )
{
if ( ! form )
form = window . document ;
// check input fields
var key , recipients , dialog ,
limit = this . env . max _disclosed _recipients ,
input _to = $ ( "[name='_to']" , form ) ,
input _cc = $ ( "[name='_cc']" , form ) ,
input _bcc = $ ( "[name='_bcc']" , form ) ,
input _from = $ ( "[name='_from']" , form ) ,
get _recipients = function ( fields ) {
fields = $ . map ( fields , function ( v ) {
v = v . val ( ) . trim ( ) ;
return v . length ? v : null ;
} ) ;
return fields . join ( ',' ) . replace ( /^[\s,;]+/ , '' ) . replace ( /[\s,;]+$/ , '' ) ;
} ;
// check sender (if have no identities)
if ( input _from . prop ( 'type' ) == 'text' && ! rcube _check _email ( input _from . val ( ) , true ) ) {
this . alert _dialog ( this . get _label ( 'nosenderwarning' ) , function ( ) {
input _from . focus ( ) ;
} ) ;
return false ;
}
// check for empty recipient
if ( ! rcube _check _email ( get _recipients ( [ input _to , input _cc , input _bcc ] ) , true ) ) {
this . alert _dialog ( this . get _label ( 'norecipientwarning' ) , function ( ) {
input _to . focus ( ) ;
} ) ;
return false ;
}
// check disclosed recipients limit
if ( limit && ! this . env . disclosed _recipients _warned
&& rcube _check _email ( recipients = get _recipients ( [ input _to , input _cc ] ) , true , true ) > limit
) {
var save _func = function ( move _to _bcc ) {
if ( move _to _bcc ) {
var bcc = input _bcc . val ( ) ;
input _bcc . val ( ( bcc ? ( bcc + ', ' ) : '' ) + recipients ) . change ( ) ;
input _to . val ( '' ) . change ( ) ;
input _cc . val ( '' ) . change ( ) ;
}
dialog . dialog ( 'close' ) ;
if ( typeof cmd == 'function' )
cmd ( ) ;
else if ( cmd )
ref . command ( cmd , { nocheck : true } ) ; // repeat command which triggered this
} ;
dialog = this . show _popup _dialog (
this . get _label ( 'disclosedrecipwarning' ) ,
this . get _label ( 'disclosedreciptitle' ) ,
[ {
text : this . get _label ( 'sendmessage' ) ,
click : function ( ) { save _func ( false ) ; } ,
'class' : 'mainaction'
} , {
text : this . get _label ( 'bccinstead' ) ,
click : function ( ) { save _func ( true ) ; }
} , {
text : this . get _label ( 'cancel' ) ,
click : function ( ) { dialog . dialog ( 'close' ) ; } ,
'class' : 'cancel'
} ] ,
{ dialogClass : 'warning' }
) ;
this . env . disclosed _recipients _warned = true ;
return false ;
}
return true ;
} ;
this . toggle _editor = function ( props , obj , e )
{
// @todo: this should work also with many editors on page
var mode , result = this . editor . toggle ( props . html , props . noconvert || false ) ,
control = $ ( '#' + this . editor . id ) . data ( 'control' ) || $ ( e ? e . target : [ ] ) ;
if ( result )
mode = props . html ? 'html' : 'plain' ;
else
mode = props . html ? 'plain' : 'html' ;
// update internal format flag
$ ( "[name='_is_html']" ) . val ( mode == 'html' ? 1 : 0 ) ;
if ( control . is ( '[type=checkbox]' ) )
control . prop ( 'checked' , mode == 'html' ) ;
else
control . val ( mode ) ;
return result ;
} ;
// Inserts a predefined response to the compose editor
this . insert _response = function ( key )
{
this . editor . replace ( this . env . textresponses [ key ] ) ;
this . display _message ( 'responseinserted' , 'confirmation' ) ;
} ;
/ * *
* Open the dialog to save a new canned response
* /
this . save _response = function ( )
{
// show dialog to enter a name and to modify the text to be saved
var buttons = { } , text = this . editor . get _content ( { selection : true , format : 'text' , nosig : true } ) ,
html = '<form class="propform">' +
'<div class="prop block"><label for="ffresponsename">' + this . get _label ( 'responsename' ) + '</label>' +
'<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
'<div class="prop block"><label for="ffresponsetext">' + this . get _label ( 'responsetext' ) + '</label>' +
'<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
'</form>' ;
buttons [ this . get _label ( 'save' ) ] = function ( e ) {
var name = $ ( '#ffresponsename' ) . val ( ) ,
text = $ ( '#ffresponsetext' ) . val ( ) ;
if ( ! text ) {
$ ( '#ffresponsetext' ) . select ( ) ;
return false ;
}
if ( ! name )
name = text . replace ( /[\r\n]+/g , ' ' ) . substring ( 0 , 40 ) ;
var lock = ref . display _message ( 'savingresponse' , 'loading' ) ;
ref . http _post ( 'settings/responses' , { _insert : 1 , _name : name , _text : text } , lock ) ;
$ ( this ) . dialog ( 'close' ) ;
} ;
buttons [ this . get _label ( 'cancel' ) ] = function ( ) {
$ ( this ) . dialog ( 'close' ) ;
} ;
this . show _popup _dialog ( html , this . get _label ( 'newresponse' ) , buttons , { button _classes : [ 'mainaction save' , 'cancel' ] } ) ;
$ ( '#ffresponsetext' ) . val ( text ) ;
$ ( '#ffresponsename' ) . select ( ) ;
} ;
this . add _response _item = function ( response )
{
var key = response . key ;
this . env . textresponses [ key ] = response ;
// append to responses list
if ( this . gui _objects . responseslist ) {
var li = $ ( '<li>' ) . appendTo ( this . gui _objects . responseslist ) ;
$ ( '<a>' ) . addClass ( 'insertresponse active' )
. attr ( { href : '#' , rel : key , tabindex : '0' } )
. html ( this . quote _html ( response . name ) )
. appendTo ( li )
. mousedown ( function ( e ) {
return rcube _event . cancel ( e ) ;
} )
. on ( 'mouseup keypress' , function ( e ) {
if ( e . type == 'mouseup' || rcube _event . get _keycode ( e ) == 13 ) {
ref . command ( 'insert-response' , $ ( this ) . attr ( 'rel' ) ) ;
$ ( document . body ) . trigger ( 'mouseup' ) ; // hides the menu
return rcube _event . cancel ( e ) ;
}
} ) ;
}
} ;
this . edit _responses = function ( )
{
// TODO: implement inline editing of responses
} ;
this . delete _response = function ( key )
{
if ( ! key && this . responses _list ) {
var selection = this . responses _list . get _selection ( ) ;
key = selection [ 0 ] ;
}
// submit delete request
if ( key ) {
this . confirm _dialog ( this . get _label ( 'deleteresponseconfirm' ) , 'delete' , function ( ) {
ref . http _post ( 'settings/delete-response' , { _key : key } , false ) ;
} ) ;
}
} ;
// updates spellchecker buttons on state change
this . spellcheck _state = function ( )
{
var active = this . editor . spellcheck _state ( ) ;
$ . each ( this . buttons . spellcheck || [ ] , function ( i , v ) {
$ ( '#' + v . id ) [ active ? 'addClass' : 'removeClass' ] ( 'selected' ) ;
} ) ;
return active ;
} ;
// get selected language
this . spellcheck _lang = function ( )
{
return this . editor . get _language ( ) ;
} ;
this . spellcheck _lang _set = function ( lang )
{
this . editor . set _language ( lang ) ;
} ;
// resume spellchecking, highlight provided mispellings without new ajax request
this . spellcheck _resume = function ( data )
{
this . editor . spellcheck _resume ( data ) ;
} ;
this . set _draft _id = function ( id )
{
if ( id && id != this . env . draft _id ) {
var filter = { task : 'mail' , action : '' } ,
rc = this . opener ( false , filter ) || this . opener ( true , filter ) ;
// refresh the drafts folder in the opener window
if ( rc && rc . env . mailbox == this . env . drafts _mailbox )
rc . command ( 'checkmail' ) ;
this . env . draft _id = id ;
$ ( "[name='_draft_saveid']" ) . val ( id ) ;
}
// always remove local copy upon saving as draft
this . remove _compose _data ( this . env . compose _id ) ;
this . compose _skip _unsavedcheck = false ;
} ;
// Create (attach) 'savetarget' iframe before use
this . get _save _target = function ( )
{
// Removing the frame on load/error to workaround issues with window history
this . dummy _iframe ( 'savetarget' , 'about:blank' )
. on ( 'load error' , function ( ) { $ ( this ) . remove ( ) ; } ) ;
return 'savetarget' ;
} ;
this . auto _save _start = function ( )
{
if ( this . env . draft _autosave ) {
this . save _timer = setTimeout ( function ( ) {
ref . command ( "savedraft" ) ;
} , this . env . draft _autosave * 1000 ) ;
}
// save compose form content to local storage every 5 seconds
if ( ! this . local _save _timer && window . localStorage && this . env . save _localstorage ) {
// track typing activity and only save on changes
this . compose _type _activity = this . compose _type _activity _last = 0 ;
$ ( document ) . keypress ( function ( e ) { ref . compose _type _activity ++ ; } ) ;
this . local _save _timer = setInterval ( function ( ) {
if ( ref . compose _type _activity > ref . compose _type _activity _last ) {
ref . save _compose _form _local ( ) ;
ref . compose _type _activity _last = ref . compose _type _activity ;
}
} , 5000 ) ;
$ ( window ) . on ( 'unload' , function ( ) {
// remove copy from local storage if compose screen is left after warning
if ( ! ref . env . server _error )
ref . remove _compose _data ( ref . env . compose _id ) ;
} ) ;
}
// check for unsaved changes before leaving the compose page
if ( ! window . onbeforeunload ) {
window . onbeforeunload = function ( ) {
if ( ! ref . compose _skip _unsavedcheck && ref . cmp _hash != ref . compose _field _hash ( ) ) {
return ref . get _label ( 'notsentwarning' ) ;
}
} ;
}
// Unlock interface now that saving is complete
this . busy = false ;
} ;
this . compose _field _hash = function ( save )
{
// check input fields
var i , id , val , str = '' , hash _fields = [ 'to' , 'cc' , 'bcc' , 'subject' ] ;
for ( i = 0 ; i < hash _fields . length ; i ++ )
if ( val = $ ( '[name="_' + hash _fields [ i ] + '"]' ) . val ( ) )
str += val + ':' ;
str += this . editor . get _content ( { refresh : false } ) ;
if ( this . env . attachments )
for ( id in this . env . attachments )
str += id ;
// we can't detect changes in the Mailvelope editor so assume it changed
if ( this . mailvelope _editor ) {
str += ';' + new Date ( ) . getTime ( ) ;
}
if ( save )
this . cmp _hash = str ;
return str ;
} ;
// store the contents of the compose form to localstorage
this . save _compose _form _local = function ( )
{
// feature is disabled
if ( ! this . env . save _localstorage )
return ;
var formdata = { session : this . env . session _id , changed : new Date ( ) . getTime ( ) } ,
ed , empty = true ;
// get fresh content from editor
this . editor . save ( ) ;
if ( this . env . draft _id ) {
formdata . draft _id = this . env . draft _id ;
}
if ( this . env . reply _msgid ) {
formdata . reply _msgid = this . env . reply _msgid ;
}
$ ( 'input, select, textarea' , this . gui _objects . messageform ) . each ( function ( i , elem ) {
switch ( elem . tagName . toLowerCase ( ) ) {
case 'input' :
if ( elem . type == 'button' || elem . type == 'submit' || ( elem . type == 'hidden' && elem . name != '_is_html' ) ) {
break ;
}
formdata [ elem . name ] = elem . type != 'checkbox' || elem . checked ? $ ( elem ) . val ( ) : '' ;
if ( formdata [ elem . name ] != '' && elem . type != 'hidden' )
empty = false ;
break ;
case 'select' :
formdata [ elem . name ] = $ ( 'option:checked' , elem ) . val ( ) ;
break ;
default :
formdata [ elem . name ] = $ ( elem ) . val ( ) ;
if ( formdata [ elem . name ] != '' )
empty = false ;
}
} ) ;
if ( ! empty ) {
var index = this . local _storage _get _item ( 'compose.index' , [ ] ) ,
key = this . env . compose _id ;
if ( $ . inArray ( key , index ) < 0 ) {
index . push ( key ) ;
}
this . local _storage _set _item ( 'compose.' + key , formdata , true ) ;
this . local _storage _set _item ( 'compose.index' , index ) ;
}
} ;
// write stored compose data back to form
this . restore _compose _form = function ( key , html _mode )
{
var ed , formdata = this . local _storage _get _item ( 'compose.' + key , true ) ;
if ( formdata && typeof formdata == 'object' ) {
$ . each ( formdata , function ( k , value ) {
if ( k [ 0 ] == '_' ) {
var elem = $ ( "*[name='" + k + "']" ) ;
if ( elem [ 0 ] && elem [ 0 ] . type == 'checkbox' ) {
elem . prop ( 'checked' , value != '' ) ;
}
else {
elem . val ( value ) ;
}
}
} ) ;
// initialize HTML editor
if ( ( formdata . _is _html == '1' && ! html _mode ) || ( formdata . _is _html != '1' && html _mode ) ) {
this . command ( 'toggle-editor' , { id : this . env . composebody , html : ! html _mode , noconvert : true } ) ;
}
}
} ;
// remove stored compose data from localStorage
this . remove _compose _data = function ( key )
{
var index = this . local _storage _get _item ( 'compose.index' , [ ] ) ;
if ( $ . inArray ( key , index ) >= 0 ) {
this . local _storage _remove _item ( 'compose.' + key ) ;
this . local _storage _set _item ( 'compose.index' , $ . grep ( index , function ( val , i ) { return val != key ; } ) ) ;
}
} ;
// clear all stored compose data of this user
this . clear _compose _data = function ( )
{
var i , index = this . local _storage _get _item ( 'compose.index' , [ ] ) ;
for ( i = 0 ; i < index . length ; i ++ ) {
this . local _storage _remove _item ( 'compose.' + index [ i ] ) ;
}
this . local _storage _remove _item ( 'compose.index' ) ;
} ;
this . change _identity = function ( obj , show )
{
if ( ! obj || ! obj . options )
return false ;
var id = $ ( obj ) . val ( ) ,
got _sig = this . env . signatures && this . env . signatures [ id ] ,
sig = this . env . identity ,
show _sig = show ? show : this . env . show _sig ;
// enable manual signature insert
if ( got _sig ) {
this . enable _command ( 'insert-sig' , true ) ;
this . env . compose _commands . push ( 'insert-sig' ) ;
got _sig = true ;
}
else
this . enable _command ( 'insert-sig' , false ) ;
// first function execution
if ( ! this . env . identities _initialized ) {
this . env . identities _initialized = true ;
if ( this . env . show _sig _later )
this . env . show _sig = true ;
if ( this . env . opened _extwin )
return ;
}
// update reply-to/bcc fields with addresses defined in identities
$ . each ( [ 'replyto' , 'bcc' ] , function ( ) {
var rx , key = this ,
old _val = sig && ref . env . identities [ sig ] ? ref . env . identities [ sig ] [ key ] : '' ,
new _val = id && ref . env . identities [ id ] ? ref . env . identities [ id ] [ key ] : '' ,
input = $ ( '[name="_' + key + '"]' ) , input _val = input . val ( ) ;
// remove old address(es)
if ( old _val && input _val ) {
rx = new RegExp ( '\\s*' + RegExp . escape ( old _val ) + '\\s*' ) ;
input _val = input _val . replace ( rx , '' ) ;
}
// cleanup
input _val = String ( input _val ) . replace ( /[,;]\s*[,;]/g , ',' ) . replace ( /^[\s,;]+/ , '' ) ;
// add new address(es)
if ( new _val && input _val . indexOf ( new _val ) == - 1 && input _val . indexOf ( new _val . replace ( /"/g , '' ) ) == - 1 ) {
if ( input _val ) {
input _val = input _val . replace ( /[,;\s]+$/ , '' ) + ', ' ;
}
input _val += new _val + ', ' ;
}
if ( old _val || new _val )
input . val ( input _val ) . change ( ) ;
} ) ;
if ( this . editor )
this . editor . change _signature ( id , show _sig ) ;
if ( show && got _sig )
this . display _message ( 'siginserted' , 'confirmation' ) ;
this . env . identity = id ;
this . triggerEvent ( 'change_identity' ) ;
return true ;
} ;
// Open file selection dialog for defined upload form
// Works only on click and only with smart-upload forms
this . upload _input = function ( name )
{
$ ( '#' + name + ' input[type="file"]' ) . click ( ) ;
} ;
// upload (attachment) file
this . upload _file = function ( form , action , lock )
{
if ( form ) {
var fname , files = [ ] ;
$ ( 'input' , form ) . each ( function ( ) {
if ( this . files ) {
fname = this . name ;
for ( var i = 0 ; i < this . files . length ; i ++ )
files . push ( this . files [ i ] ) ;
}
} ) ;
return this . file _upload ( files , { _id : this . env . compose _id || '' } , {
name : fname ,
action : action ,
lock : lock
} ) ;
}
} ;
// add file name to attachment list
// called from upload page
this . add2attachment _list = function ( name , att , upload _id )
{
if ( upload _id )
this . triggerEvent ( 'fileuploaded' , { name : name , attachment : att , id : upload _id } ) ;
if ( ! this . env . attachments )
this . env . attachments = { } ;
if ( upload _id && this . env . attachments [ upload _id ] )
delete this . env . attachments [ upload _id ] ;
this . env . attachments [ name ] = att ;
if ( ! this . gui _objects . attachmentlist )
return false ;
var label , indicator , li = $ ( '<li>' ) ;
if ( ! att . complete && att . html . indexOf ( '<' ) < 0 )
att . html = '<span class="uploading">' + att . html + '</span>' ;
if ( ! att . complete && this . env . loadingicon )
att . html = '<img src="' + this . env . loadingicon + '" alt="" class="uploading" />' + att . html ;
if ( ! att . complete ) {
label = this . get _label ( 'cancel' ) ;
att . html = '<a title="' + label + '" onclick="return rcmail.cancel_attachment_upload(\'' + name + '\');" href="#cancelupload" class="cancelupload">'
+ ( this . env . cancelicon ? '<img src="' + this . env . cancelicon + '" alt="' + label + '" />' : '<span class="inner">' + label + '</span>' ) + '</a>' + att . html ;
}
li . attr ( 'id' , name ) . addClass ( att . classname ) . html ( att . html )
. find ( '.attachment-name' ) . on ( 'mouseover' , function ( ) { rcube _webmail . long _subject _title _ex ( this ) ; } ) ;
// replace indicator's li
if ( upload _id && ( indicator = document . getElementById ( upload _id ) ) ) {
li . replaceAll ( indicator ) ;
}
else { // add new li
li . appendTo ( this . gui _objects . attachmentlist ) ;
}
// set tabindex attribute
var tabindex = $ ( this . gui _objects . attachmentlist ) . attr ( 'data-tabindex' ) || '0' ;
li . find ( 'a' ) . attr ( 'tabindex' , tabindex ) ;
this . triggerEvent ( 'fileappended' , { name : name , attachment : att , id : upload _id , item : li } ) ;
return true ;
} ;
this . remove _from _attachment _list = function ( name )
{
if ( this . env . attachments ) {
delete this . env . attachments [ name ] ;
$ ( '#' + name ) . remove ( ) ;
}
} ;
this . remove _attachment = function ( name )
{
if ( name && this . env . attachments [ name ] )
this . http _post ( 'remove-attachment' , { _id : this . env . compose _id , _file : name } ) ;
return false ;
} ;
this . cancel _attachment _upload = function ( name )
{
if ( ! name || ! this . uploads [ name ] )
return false ;
this . remove _from _attachment _list ( name ) ;
this . uploads [ name ] . abort ( ) ;
return false ;
} ;
// rename uploaded attachment (in compose)
this . rename _attachment = function ( id )
{
var attachment = this . env . attachments ? this . env . attachments [ id ] : null ;
if ( ! attachment )
return ;
var input = $ ( '<input>' ) . attr ( { type : 'text' , size : 50 } ) . val ( attachment . name ) ,
content = $ ( '<label>' ) . text ( this . get _label ( 'namex' ) ) . append ( input ) ;
this . simple _dialog ( content , 'attachmentrename' , function ( ) {
var name ;
if ( ( name = input . val ( ) ) && name != attachment . name ) {
ref . http _post ( 'rename-attachment' , { _id : ref . env . compose _id , _file : id , _name : name } ,
ref . set _busy ( true , 'loading' ) ) ;
return true ;
}
}
) ;
} ;
// update attachments list with the new name
this . rename _attachment _handler = function ( id , name )
{
var attachment = this . env . attachments ? this . env . attachments [ id ] : null ;
if ( ! attachment || ! name )
return ;
attachment . name = name ;
$ ( '#' + id + ' .attachment-name' ) . text ( name ) . attr ( 'title' , '' ) ;
} ;
// send remote request to add a new contact
this . add _contact = function ( value , reload )
{
if ( value )
this . http _post ( 'addcontact' , { _address : value , _reload : reload } ) ;
} ;
// send remote request to search mail or contacts
this . qsearch = function ( value )
{
// Note: Some plugins would like to do search without value,
// so we keep value != '' check to allow that use-case. Which means
// e.g. that qsearch() with no argument will execute the search.
if ( value != '' || $ ( this . gui _objects . qsearchbox ) . val ( ) || $ ( this . gui _objects . search _interval ) . val ( ) ) {
var r , lock = this . set _busy ( true , 'searching' ) ,
url = this . search _params ( value ) ,
action = this . env . action == 'compose' && this . contact _list ? 'search-contacts' : 'search' ;
if ( this . message _list )
this . clear _message _list ( ) ;
else if ( this . contact _list )
this . list _contacts _clear ( ) ;
if ( this . env . source )
url . _source = this . env . source ;
if ( this . env . group )
url . _gid = this . env . group ;
// reset vars
this . env . current _page = 1 ;
r = this . http _request ( action , url , lock ) ;
this . env . qsearch = { lock : lock , request : r } ;
this . enable _command ( 'set-listmode' , this . env . threads && ( this . env . search _scope || 'base' ) == 'base' ) ;
return true ;
}
return false ;
} ;
this . continue _search = function ( request _id )
{
var lock = this . set _busy ( true , 'stillsearching' ) ;
setTimeout ( function ( ) {
var url = ref . search _params ( ) ;
url . _continue = request _id ;
ref . env . qsearch = { lock : lock , request : ref . http _request ( 'search' , url , lock ) } ;
} , 100 ) ;
} ;
// build URL params for search
this . search _params = function ( search , filter )
{
var n , url = { } , mods _arr = [ ] ,
mods = this . env . search _mods ,
scope = this . env . search _scope || 'base' ,
mbox = scope == 'all' ? '*' : this . env . mailbox ;
if ( ! filter && this . gui _objects . search _filter )
filter = this . gui _objects . search _filter . value ;
if ( ! search && this . gui _objects . qsearchbox )
search = this . gui _objects . qsearchbox . value ;
if ( this . gui _objects . search _interval )
url . _interval = $ ( this . gui _objects . search _interval ) . val ( ) ;
if ( search ) {
url . _q = search ;
if ( mods && this . message _list )
mods = mods [ mbox ] || mods [ '*' ] ;
if ( mods ) {
for ( n in mods )
mods _arr . push ( n ) ;
url . _headers = mods _arr . join ( ',' ) ;
}
}
url . _layout = this . env . layout ;
url . _filter = filter ;
url . _scope = scope ;
if ( mbox && scope != 'all' )
url . _mbox = mbox ;
return url ;
} ;
// reset search filter
this . reset _search _filter = function ( )
{
this . filter _disabled = true ;
if ( this . gui _objects . search _filter )
$ ( this . gui _objects . search _filter ) . val ( 'ALL' ) . change ( ) ;
this . filter _disabled = false ;
} ;
// reset quick-search form
this . reset _qsearch = function ( all )
{
if ( this . gui _objects . qsearchbox )
this . gui _objects . qsearchbox . value = '' ;
if ( this . gui _objects . search _interval )
$ ( this . gui _objects . search _interval ) . val ( '' ) ;
if ( this . env . qsearch )
this . abort _request ( this . env . qsearch ) ;
if ( all ) {
this . env . search _scope = 'base' ;
this . reset _search _filter ( ) ;
}
this . env . qsearch = null ;
this . env . search _request = null ;
this . env . search _id = null ;
this . select _all _mode = false ;
this . enable _command ( 'set-listmode' , this . env . threads ) ;
} ;
this . set _searchscope = function ( scope )
{
var old = this . env . search _scope ;
this . env . search _scope = scope ;
// re-send search query with new scope
if ( scope != old && this . env . search _request ) {
if ( ! this . qsearch ( this . gui _objects . qsearchbox . value ) && this . env . search _filter && this . env . search _filter != 'ALL' )
this . filter _mailbox ( this . env . search _filter ) ;
if ( scope != 'all' )
this . select _folder ( this . env . mailbox , '' , true ) ;
}
} ;
this . set _searchinterval = function ( interval )
{
var old = this . env . search _interval ;
this . env . search _interval = interval ;
// re-send search query with new interval
if ( interval != old && this . env . search _request ) {
if ( ! this . qsearch ( this . gui _objects . qsearchbox . value ) && this . env . search _filter && this . env . search _filter != 'ALL' )
this . filter _mailbox ( this . env . search _filter ) ;
if ( interval )
this . select _folder ( this . env . mailbox , '' , true ) ;
}
} ;
this . set _searchmods = function ( mods )
{
var mbox = this . env . mailbox ,
scope = this . env . search _scope || 'base' ;
if ( scope == 'all' )
mbox = '*' ;
if ( ! this . env . search _mods )
this . env . search _mods = { } ;
if ( mbox )
this . env . search _mods [ mbox ] = mods ;
} ;
this . is _multifolder _listing = function ( )
{
return this . env . multifolder _listing !== undefined ? this . env . multifolder _listing :
( this . env . search _request && ( this . env . search _scope || 'base' ) != 'base' ) ;
} ;
// action executed after mail is sent
this . sent _successfully = function ( type , msg , folders , save _error )
{
this . display _message ( msg , type ) ;
this . compose _skip _unsavedcheck = true ;
if ( this . env . extwin ) {
if ( ! save _error )
this . lock _form ( this . gui _objects . messageform ) ;
var filter = { task : 'mail' , action : '' } ,
rc = this . opener ( false , filter ) || this . opener ( true , filter ) ;
if ( rc ) {
rc . display _message ( msg , type ) ;
// refresh the folder where sent message was saved or replied message comes from
if ( folders && $ . inArray ( rc . env . mailbox , folders ) >= 0 ) {
rc . command ( 'checkmail' ) ;
}
}
if ( ! save _error )
setTimeout ( function ( ) { window . close ( ) ; } , 1000 ) ;
}
else if ( ! save _error ) {
// before redirect we need to wait some time for Chrome (#1486177)
setTimeout ( function ( ) { ref . list _mailbox ( ) ; } , 500 ) ;
}
if ( save _error )
this . env . is _sent = true ;
} ;
this . image _rotate = function ( )
{
var curr = this . image _style ? ( this . image _style . rotate || 0 ) : 0 ;
this . image _style . rotate = curr > 180 ? 0 : curr + 90 ;
this . apply _image _style ( ) ;
} ;
this . image _scale = function ( prop )
{
var curr = this . image _style ? ( this . image _style . scale || 1 ) : 1 ;
this . image _style . scale = Math . max ( 0.1 , curr + 0.1 * ( prop == '-' ? - 1 : 1 ) ) ;
this . apply _image _style ( ) ;
} ;
this . apply _image _style = function ( )
{
var style = [ ] ,
head = $ ( this . gui _objects . messagepartframe ) . contents ( ) . find ( 'head' ) ;
$ ( '#image-style' , head ) . remove ( ) ;
$ . each ( { scale : '' , rotate : 'deg' } , function ( i , v ) {
var val = ref . image _style [ i ] ;
if ( val )
style . push ( i + '(' + val + v + ')' ) ;
} ) ;
if ( style )
head . append ( $ ( '<style id="image-style">' ) . text ( 'img { transform: ' + style . join ( ' ' ) + '}' ) ) ;
} ;
// Update import dialog state
this . import _state _set = function ( state )
{
if ( this . import _dialog ) {
this . import _state = state ;
// activate Import button depending on state
$ ( this . import _dialog ) . parent ( ) . find ( '.ui-dialog-buttonset > button' ) . first ( ) . attr ( 'disabled' , state != 'error' ) ;
}
} ;
/*********************************************************/
/********* keyboard live-search methods *********/
/*********************************************************/
// handler for keyboard events on address-fields
this . ksearch _keydown = function ( e , obj , props )
{
if ( this . ksearch _timer )
clearTimeout ( this . ksearch _timer ) ;
var key = rcube _event . get _keycode ( e ) ;
switch ( key ) {
case 38 : // arrow up
case 40 : // arrow down
if ( ! this . ksearch _visible ( ) )
return ;
var dir = key == 38 ? 1 : 0 ,
highlight = this . ksearch _pane . find ( 'li.selected' ) [ 0 ] ;
if ( ! highlight )
highlight = this . ksearch _pane . _ _ul . firstChild ;
if ( highlight )
this . ksearch _select ( dir ? highlight . previousSibling : highlight . nextSibling ) ;
return rcube _event . cancel ( e ) ;
case 9 : // tab
if ( rcube _event . get _modifier ( e ) == SHIFT _KEY || ! this . ksearch _visible ( ) ) {
this . ksearch _hide ( ) ;
return ;
}
case 13 : // enter
if ( ! this . ksearch _visible ( ) )
return false ;
// insert selected address and hide ksearch pane
this . insert _recipient ( this . ksearch _selected ) ;
this . ksearch _hide ( ) ;
// Don't cancel on Tab, we want to jump to the next field (#5659)
return key == 9 ? null : rcube _event . cancel ( e ) ;
case 27 : // escape
this . ksearch _hide ( ) ;
return ;
case 37 : // left
case 39 : // right
return ;
}
// start timer
this . ksearch _timer = setTimeout ( function ( ) { ref . ksearch _get _results ( props ) ; } , 200 ) ;
this . ksearch _input = obj ;
return true ;
} ;
this . ksearch _visible = function ( )
{
return this . ksearch _selected !== null && this . ksearch _selected !== undefined && this . ksearch _value ;
} ;
this . ksearch _select = function ( node )
{
if ( this . ksearch _pane && node ) {
this . ksearch _pane . find ( 'li.selected' ) . removeClass ( 'selected' ) . removeAttr ( 'aria-selected' ) ;
}
if ( node ) {
$ ( node ) . addClass ( 'selected' ) . attr ( 'aria-selected' , 'true' ) ;
this . ksearch _selected = node . _rcm _id ;
$ ( this . ksearch _input ) . attr ( 'aria-activedescendant' , 'rcmkSearchItem' + this . ksearch _selected ) ;
}
} ;
this . insert _recipient = function ( id )
{
if ( id === null || ! this . env . contacts [ id ] || ! this . ksearch _input )
return ;
var trigger = false , insert = '' , delim = ', ' ,
contact = this . env . contacts [ id ] ;
this . ksearch _destroy ( ) ;
// insert all members of a group
if ( typeof contact === 'object' && contact . type == 'group' && ! contact . email && contact . id ) {
insert = contact . name + delim ;
this . group2expand [ contact . id ] = $ . extend ( { input : this . ksearch _input } , contact ) ;
this . http _request ( 'mail/group-expand' , { _source : contact . source , _gid : contact . id } , false ) ;
}
else if ( typeof contact === 'object' && contact . name ) {
insert = contact . name + delim ;
trigger = true ;
}
else if ( typeof contact === 'string' ) {
insert = contact + delim ;
trigger = true ;
}
this . ksearch _input _replace ( this . ksearch _value , insert ) ;
if ( trigger ) {
this . triggerEvent ( 'autocomplete_insert' , { field : this . ksearch _input , insert : insert , data : contact , search : this . ksearch _value _last , result _type : 'person' } ) ;
this . ksearch _value _last = null ;
this . compose _type _activity ++ ;
}
} ;
this . replace _group _recipients = function ( id , recipients )
{
if ( this . group2expand [ id ] ) {
this . ksearch _input _replace ( this . group2expand [ id ] . name , recipients , this . group2expand [ id ] . input ) ;
this . triggerEvent ( 'autocomplete_insert' , { field : this . group2expand [ id ] . input , insert : recipients , data : this . group2expand [ id ] , search : this . ksearch _value _last , result _type : 'group' } ) ;
this . ksearch _value _last = null ;
this . group2expand [ id ] = null ;
this . compose _type _activity ++ ;
}
} ;
// address search processor
this . ksearch _get _results = function ( props )
{
if ( this . ksearch _pane && this . ksearch _pane . is ( ":visible" ) )
this . ksearch _pane . hide ( ) ;
// get string from cursor position back to the last comma or semicolon
var q = this . ksearch _input _get ( ) ,
min = this . env . autocomplete _min _length ,
data = this . ksearch _data ;
// trim query string
q = q . trim ( ) ;
// Don't (re-)search if the last results are still active
if ( q == this . ksearch _value )
return ;
this . ksearch _destroy ( ) ;
if ( q . length && q . length < min ) {
if ( ! this . ksearch _info ) {
this . ksearch _info = this . display _message ( this . get _label ( 'autocompletechars' ) . replace ( '$min' , min ) ) ;
}
return ;
}
var old _value = this . ksearch _value ;
this . ksearch _value = q ;
this . ksearch _value _last = q ; // Group expansion clears ksearch_value before calling autocomplete_insert trigger, therefore store it in separate variable for later consumption.
// ...string is empty
if ( ! q . length )
return ;
// ...new search value contains old one and previous search was not finished or its result was empty
if ( old _value && old _value . length && q . startsWith ( old _value ) && ( ! data || data . num <= 0 ) && this . env . contacts && ! this . env . contacts . length )
return ;
var sources = props && props . sources ? props . sources : [ '' ] ;
var reqid = this . multi _thread _http _request ( {
items : sources ,
threads : props && props . threads ? props . threads : 1 ,
action : props && props . action ? props . action : 'mail/autocomplete' ,
postdata : { _search : q , _source : '%s' } ,
lock : this . display _message ( 'searching' , 'loading' )
} ) ;
this . ksearch _data = { id : reqid , sources : sources . slice ( ) , num : sources . length } ;
} ;
this . ksearch _query _results = function ( results , search , reqid )
{
// trigger multi-thread http response callback
this . multi _thread _http _response ( results , reqid ) ;
// search stopped in meantime?
if ( ! this . ksearch _value )
return ;
// ignore this outdated search response
if ( this . ksearch _input && search != this . ksearch _value )
return ;
// display search results
var i , id , len , ul , text , type , init ,
is _framed = this . is _framed ( ) ,
value = this . ksearch _value ,
maxlen = this . env . autocomplete _max ? this . env . autocomplete _max : 15 ;
// create results pane if not present
if ( ! this . ksearch _pane ) {
ul = $ ( '<ul>' ) ;
this . ksearch _pane = $ ( '<div>' )
. attr ( { id : 'rcmKSearchpane' , role : 'listbox' , 'class' : 'select-menu inline' } )
. css ( { position : 'absolute' , 'z-index' : 30000 } )
. append ( ul )
. appendTo ( is _framed ? parent . document . body : document . body ) ;
this . ksearch _pane . _ _ul = ul [ 0 ] ;
this . triggerEvent ( 'autocomplete_create' , { obj : this . ksearch _pane } ) ;
}
ul = this . ksearch _pane . _ _ul ;
// remove all search results or add to existing list if parallel search
if ( reqid && this . ksearch _pane . data ( 'reqid' ) == reqid ) {
maxlen -= ul . childNodes . length ;
}
else {
this . ksearch _pane . data ( 'reqid' , reqid ) ;
init = 1 ;
// reset content
ul . innerHTML = '' ;
this . env . contacts = [ ] ;
// Calculate the results pane position and size
// Elastic: On small screen we use the width/position of the whole .ac-input element (input's parent)
var is _composite _input = $ ( 'html' ) . is ( '.layout-small,.layout-phone' ) && $ ( this . ksearch _input ) . parents ( '.ac-input' ) . length == 1 ,
input = is _composite _input ? $ ( this . ksearch _input ) . parents ( '.ac-input' ) [ 0 ] : $ ( this . ksearch _input ) [ 0 ] ,
pos = $ ( input ) . offset ( ) ;
// ... consider scroll position
pos . left -= $ ( document . documentElement ) . scrollLeft ( ) ;
pos . top -= $ ( document . documentElement ) . scrollTop ( ) ;
// ... consider iframe position
if ( is _framed ) {
try {
parent . $ ( 'iframe' ) . each ( function ( ) {
if ( this . contentWindow == window ) {
var offset = $ ( this ) . offset ( ) ;
pos . left += offset . left ;
pos . top += offset . top ;
}
} ) ;
}
catch ( e ) { }
}
var w = $ ( is _framed ? parent : window ) . width ( ) ,
input _width = $ ( input ) . outerWidth ( ) ,
left = w - pos . left > 200 ? pos . left : w - 200 ,
top = ( pos . top + input . offsetHeight + 1 ) ,
width = Math . min ( 400 , w - left ) ;
this . ksearch _pane . css ( {
left : ( is _composite _input ? pos . left : left ) + 'px' ,
top : top + 'px' ,
maxWidth : ( is _composite _input ? input _width : width ) + 'px' ,
minWidth : '200px' ,
width : is _composite _input ? ( input _width + 'px' ) : 'auto' ,
display : 'none'
} ) ;
}
// add each result line to list
if ( results && ( len = results . length ) ) {
for ( i = 0 ; i < len && maxlen > 0 ; i ++ ) {
text = typeof results [ i ] === 'object' ? ( results [ i ] . display || results [ i ] . name ) : results [ i ] ;
type = typeof results [ i ] === 'object' ? results [ i ] . type : '' ;
id = i + this . env . contacts . length ;
$ ( '<li>' ) . attr ( { id : 'rcmkSearchItem' + id , role : 'option' } )
. html ( '<i class="icon"></i>' + this . quote _html ( text . replace ( new RegExp ( '(' + RegExp . escape ( value ) + ')' , 'ig' ) , '##$1%%' ) ) . replace ( /##([^%]+)%%/g , '<b>$1</b>' ) )
. addClass ( type || '' )
. appendTo ( ul )
. mouseover ( function ( ) { ref . ksearch _select ( this ) ; } )
. mouseup ( function ( ) { ref . ksearch _click ( this ) ; } )
. get ( 0 ) . _rcm _id = id ;
maxlen -= 1 ;
}
}
if ( ul . childNodes . length ) {
// set the right aria-* attributes to the input field
$ ( this . ksearch _input )
. attr ( { 'aria-haspopup' : 'true' , 'aria-expanded' : 'true' , 'aria-owns' : 'rcmKSearchpane' } ) ;
this . ksearch _pane . show ( ) ;
// select the first
if ( ! this . env . contacts . length ) {
this . ksearch _select ( $ ( 'li' , ul ) [ 0 ] ) ;
}
}
if ( len )
this . env . contacts = this . env . contacts . concat ( results ) ;
if ( this . ksearch _data . id == reqid )
this . ksearch _data . num -- ;
} ;
// Getter for input value
// returns a string from the last comma to current cursor position
this . ksearch _input _get = function ( )
{
if ( ! this . ksearch _input )
return '' ;
var cp = this . get _caret _pos ( this . ksearch _input ) ;
return this . ksearch _input . value . substr ( 0 , cp ) . split ( /[,;]/ ) . pop ( ) ;
} ;
// Setter for input value
// replaces 'from' string with 'to' and sets cursor position at the end
this . ksearch _input _replace = function ( from , to , input )
{
if ( ! this . ksearch _input && ! input )
return ;
if ( ! input )
input = this . ksearch _input ;
var cpos = this . get _caret _pos ( input ) ,
p = input . value . lastIndexOf ( from , cpos ) ,
pre = input . value . substring ( 0 , p ) ,
end = input . value . substring ( p + from . length , input . value . length ) ;
input . value = pre + to + end ;
// set caret to insert pos
this . set _caret _pos ( input , cpos + to . length - from . length ) ;
// run onchange action on the element
$ ( input ) . trigger ( 'change' , [ true ] ) ;
} ;
this . ksearch _click = function ( node )
{
if ( this . ksearch _input )
this . ksearch _input . focus ( ) ;
this . insert _recipient ( node . _rcm _id ) ;
this . ksearch _hide ( ) ;
} ;
this . ksearch _blur = function ( )
{
if ( this . ksearch _timer )
clearTimeout ( this . ksearch _timer ) ;
this . ksearch _input = null ;
this . ksearch _hide ( ) ;
} ;
this . ksearch _hide = function ( )
{
this . ksearch _selected = null ;
this . ksearch _value = '' ;
if ( this . ksearch _pane )
this . ksearch _pane . hide ( ) ;
$ ( this . ksearch _input )
. attr ( { 'aria-haspopup' : 'false' , 'aria-expanded' : 'false' } )
. removeAttr ( 'aria-activedescendant' )
. removeAttr ( 'aria-owns' ) ;
this . ksearch _destroy ( ) ;
} ;
// Clears autocomplete data/requests
this . ksearch _destroy = function ( )
{
if ( this . ksearch _data )
this . multi _thread _request _abort ( this . ksearch _data . id ) ;
if ( this . ksearch _info )
this . hide _message ( this . ksearch _info ) ;
if ( this . ksearch _msg )
this . hide _message ( this . ksearch _msg ) ;
this . ksearch _data = null ;
this . ksearch _info = null ;
this . ksearch _msg = null ;
} ;
/*********************************************************/
/********* address book methods *********/
/*********************************************************/
this . contactlist _select = function ( list )
{
if ( this . preview _timer )
clearTimeout ( this . preview _timer ) ;
var id , targets , groupcount = 0 , writable = false , copy _writable = false ,
selected = list . get _selection ( ) . length ,
source = this . env . source ? this . env . address _sources [ this . env . source ] : null ;
// we don't have dblclick handler here, so use 50 instead of this.dblclick_time
if ( this . env . contentframe && ! list . multi _selecting && ( id = list . get _single _selection ( ) ) )
this . preview _timer = setTimeout ( function ( ) { ref . load _contact ( id , 'show' ) ; } , this . preview _delay _click ) ;
else if ( this . env . contentframe )
this . show _contentframe ( false ) ;
if ( selected ) {
list . draggable = false ;
// no source = search result, we'll need to detect if any of
// selected contacts are in writable addressbook to enable edit/delete
// we'll also need to know sources used in selection for copy
// and group-addmember operations (drag&drop)
this . env . selection _sources = [ ] ;
if ( source ) {
this . env . selection _sources . push ( this . env . source ) ;
}
$ . each ( list . get _selection ( ) , function ( i , v ) {
var sid , contact = list . data [ v ] ;
if ( ! source ) {
sid = String ( v ) . replace ( /^[^-]+-/ , '' ) ;
if ( sid && ref . env . address _sources [ sid ] ) {
writable = writable || ( ! ref . env . address _sources [ sid ] . readonly && ! contact . readonly ) ;
ref . env . selection _sources . push ( sid ) ;
}
}
else {
writable = writable || ( ! source . readonly && ! contact . readonly ) ;
}
if ( contact . _type != 'group' )
list . draggable = true ;
} ) ;
this . env . selection _sources = $ . unique ( this . env . selection _sources ) ;
if ( source && source . groups )
$ . each ( this . env . contactgroups , function ( ) { if ( this . source === ref . env . source ) groupcount ++ ; } ) ;
targets = $ . map ( this . env . address _sources , function ( v , i ) { return v . readonly ? null : i ; } ) ;
copy _writable = $ . grep ( targets , function ( v ) { return jQuery . inArray ( v , ref . env . selection _sources ) < 0 ; } ) . length > 0 ;
}
// if a group is currently selected, and there is at least one contact selected
// we can enable the group-remove-selected command
this . enable _command ( 'group-assign-selected' , groupcount > 0 && writable ) ;
this . enable _command ( 'group-remove-selected' , this . env . group && writable ) ;
this . enable _command ( 'print' , 'qrcode' , selected == 1 ) ;
this . enable _command ( 'export-selected' , selected > 0 ) ;
this . enable _command ( 'edit' , id && writable ) ;
this . enable _command ( 'delete' , 'move' , writable ) ;
this . enable _command ( 'copy' , copy _writable ) ;
return false ;
} ;
this . list _contacts = function ( src , group , page , search )
{
var win , folder , index = - 1 , url = { } ,
refresh = src === undefined && group === undefined && page === undefined ,
target = window ;
if ( ! src )
src = this . env . source ;
if ( refresh )
group = this . env . group ;
if ( src != this . env . source ) {
page = this . env . current _page = 1 ;
this . reset _qsearch ( ) ;
}
else if ( ! refresh && group != this . env . group )
page = this . env . current _page = 1 ;
if ( this . env . search _id )
folder = 'S' + this . env . search _id ;
else if ( ! this . env . search _request )
folder = group ? 'G' + src + group : src ;
this . env . source = src ;
this . env . group = group ;
// truncate groups listing stack
$ . each ( this . env . address _group _stack , function ( i , v ) {
if ( ref . env . group == v . id ) {
index = i ;
return false ;
}
} ) ;
this . env . address _group _stack = index < 0 ? [ ] : this . env . address _group _stack . slice ( 0 , index ) ;
// remove cached contact group selector
this . destroy _entity _selector ( 'contactgroup-selector' ) ;
// make sure the current group is on top of the stack
if ( this . env . group ) {
if ( ! search ) search = { } ;
search . id = this . env . group ;
this . env . address _group _stack . push ( search ) ;
// mark the first group on the stack as selected in the directory list
folder = 'G' + src + this . env . address _group _stack [ 0 ] . id ;
}
else if ( this . gui _objects . addresslist _title ) {
$ ( this . gui _objects . addresslist _title ) . text ( this . get _label ( 'contacts' ) ) ;
}
if ( ! this . env . search _id )
this . select _folder ( folder , '' , true ) ;
// load contacts remotely
if ( this . gui _objects . contactslist ) {
this . list _contacts _remote ( src , group , page ) ;
return ;
}
if ( win = this . get _frame _window ( this . env . contentframe ) ) {
target = win ;
url . _framed = 1 ;
}
if ( group )
url . _gid = group ;
if ( page )
url . _page = page ;
if ( src )
url . _source = src ;
// also send search request to get the correct listing
if ( this . env . search _request )
url . _search = this . env . search _request ;
this . set _busy ( true , 'loading' ) ;
this . location _href ( url , target ) ;
} ;
// send remote request to load contacts list
this . list _contacts _remote = function ( src , group , page )
{
// clear message list first
this . list _contacts _clear ( ) ;
// send request to server
var url = { } , lock = this . set _busy ( true , 'loading' ) ;
if ( src )
url . _source = src ;
if ( page )
url . _page = page ;
if ( group )
url . _gid = group ;
this . env . source = src ;
this . env . group = group ;
// also send search request to get the right records
if ( this . env . search _request )
url . _search = this . env . search _request ;
this . http _request ( this . env . task == 'mail' ? 'list-contacts' : 'list' , url , lock ) ;
if ( this . env . task != 'mail' )
this . update _state ( { _source : src , _page : page && page > 1 ? page : null , _gid : group } ) ;
} ;
this . list _contacts _clear = function ( )
{
this . contact _list . data = { } ;
this . contact _list . clear ( true ) ;
this . show _contentframe ( false ) ;
this . enable _command ( 'delete' , 'move' , 'copy' , 'print' , false ) ;
} ;
this . set _group _prop = function ( prop )
{
if ( this . gui _objects . addresslist _title ) {
var boxtitle = $ ( this . gui _objects . addresslist _title ) . html ( '' ) ; // clear contents
// add link to pop back to parent group
if ( this . env . address _group _stack . length > 1
|| ( this . env . address _group _stack . length == 1 && this . env . address _group _stack [ 0 ] . search _request )
) {
$ ( '<a href="#list">...</a>' )
. attr ( 'title' , this . get _label ( 'uponelevel' ) )
. addClass ( 'poplink' )
. appendTo ( boxtitle )
. click ( function ( e ) { return ref . command ( 'popgroup' , '' , this ) ; } ) ;
boxtitle . append ( ' » ' ) ;
}
boxtitle . append ( $ ( '<span>' ) . text ( prop ? prop . name : this . get _label ( 'contacts' ) ) ) ;
}
} ;
// load contact record
this . load _contact = function ( cid , action , framed )
{
var win , url = { } , target = window ,
rec = this . contact _list ? this . contact _list . data [ cid ] : null ;
if ( win = this . get _frame _window ( this . env . contentframe ) ) {
url . _framed = 1 ;
target = win ;
this . show _contentframe ( true ) ;
// load dummy content, unselect selected row(s)
if ( ! cid )
this . contact _list . clear _selection ( ) ;
this . enable _command ( 'export-selected' , 'print' , rec && rec . _type != 'group' ) ;
}
else if ( framed )
return false ;
if ( action && ( cid || action == 'add' ) && ! this . drag _active ) {
if ( this . env . group )
url . _gid = this . env . group ;
if ( this . env . search _request )
url . _search = this . env . search _request ;
if ( cid )
url . _cid = this . preview _id = cid ;
url . _action = action ;
url . _source = this . env . source ;
this . location _href ( url , target , true ) ;
}
return true ;
} ;
// add/delete member to/from the group
this . group _member _change = function ( what , cid , source , gid )
{
if ( what != 'add' )
what = 'del' ;
var lock = this . display _message ( what == 'add' ? 'addingmember' : 'removingmember' , 'loading' ) ,
post _data = { _cid : cid , _source : source , _gid : gid } ;
this . http _post ( 'group-' + what + 'members' , post _data , lock ) ;
} ;
this . contacts _drag _menu = function ( e , to )
{
var dest = to . type == 'group' ? to . source : to . id ,
source = this . env . source ;
if ( ! this . env . address _sources [ dest ] || this . env . address _sources [ dest ] . readonly )
return true ;
// search result may contain contacts from many sources, but if there is only one...
if ( source == '' && this . env . selection _sources . length == 1 )
source = this . env . selection _sources [ 0 ] ;
if ( to . type == 'group' && dest == source ) {
var cid = this . contact _list . get _selection ( ) . join ( ',' ) ;
this . group _member _change ( 'add' , cid , dest , to . id ) ;
return true ;
}
// move action is not possible, "redirect" to copy if menu wasn't requested
else if ( ! this . commands . move && rcube _event . get _modifier ( e ) != SHIFT _KEY ) {
this . copy _contacts ( to ) ;
return true ;
}
return this . drag _menu ( e , to ) ;
} ;
// copy contact(s) to the specified target (group or directory)
this . copy _contacts = function ( to , event , cid )
{
if ( ! to ) {
cid = this . contact _list . get _selection ( ) ;
return this . addressbook _selector ( event , function ( to , obj ) {
var to = $ ( obj ) . data ( 'source' ) ? ref . env . contactgroups [ 'G' + $ ( obj ) . data ( 'source' ) + $ ( obj ) . data ( 'gid' ) ] : ref . env . address _sources [ to ] ;
ref . copy _contacts ( to , null , cid ) ;
} ) ;
}
var dest = to . type == 'group' ? to . source : to . id ,
source = this . env . source ,
group = this . env . group ? this . env . group : '' ;
cid = cid ? cid . join ( ',' ) : this . contact _list . get _selection ( ) . join ( ',' ) ;
if ( ! cid || ! this . env . address _sources [ dest ] || this . env . address _sources [ dest ] . readonly )
return ;
// search result may contain contacts from many sources, but if there is only one...
if ( source == '' && this . env . selection _sources . length == 1 )
source = this . env . selection _sources [ 0 ] ;
// tagret is a group
if ( to . type == 'group' ) {
if ( dest == source )
return ;
var lock = this . display _message ( 'copyingcontact' , 'loading' ) ,
post _data = { _cid : cid , _source : this . env . source , _to : dest , _togid : to . id , _gid : group } ;
this . http _post ( 'copy' , post _data , lock ) ;
}
// target is an addressbook
else if ( to . id != source ) {
var lock = this . display _message ( 'copyingcontact' , 'loading' ) ,
post _data = { _cid : cid , _source : this . env . source , _to : to . id , _gid : group } ;
this . http _post ( 'copy' , post _data , lock ) ;
}
} ;
// move contact(s) to the specified target (group or directory)
this . move _contacts = function ( to , event , cid )
{
if ( ! to ) {
cid = this . contact _list . get _selection ( ) ;
return this . addressbook _selector ( event , function ( to , obj ) {
var to = $ ( obj ) . data ( 'source' ) ? ref . env . contactgroups [ 'G' + $ ( obj ) . data ( 'source' ) + $ ( obj ) . data ( 'gid' ) ] : ref . env . address _sources [ to ] ;
ref . move _contacts ( to , null , cid ) ;
} ) ;
}
var dest = to . type == 'group' ? to . source : to . id ,
source = this . env . source ,
group = this . env . group ? this . env . group : '' ;
if ( ! this . env . address _sources [ dest ] || this . env . address _sources [ dest ] . readonly )
return ;
// search result may contain contacts from many sources, but if there is only one...
if ( source == '' && this . env . selection _sources . length == 1 )
source = this . env . selection _sources [ 0 ] ;
if ( to . type == 'group' ) {
if ( dest == source )
return ;
this . _with _selected _contacts ( 'move' , { _to : dest , _togid : to . id , _cid : cid } ) ;
}
// target is an addressbook
else if ( to . id != source )
this . _with _selected _contacts ( 'move' , { _to : to . id , _cid : cid } ) ;
} ;
// delete contact(s)
this . delete _contacts = function ( )
{
var undelete = this . env . source && this . env . address _sources [ this . env . source ] . undelete ;
if ( undelete ) {
this . _with _selected _contacts ( 'delete' , { _cid : this . contact _list . get _selection ( ) } ) ;
}
else {
var cid = this . contact _list . get _selection ( ) ;
this . confirm _dialog ( this . get _label ( 'deletecontactconfirm' ) , 'delete' , function ( ) {
ref . _with _selected _contacts ( 'delete' , { _cid : cid } ) ;
} ) ;
}
} ;
this . _with _selected _contacts = function ( action , post _data )
{
var selection = post _data . _cid ;
// exit if no contact specified or if selection is empty
if ( ! selection . length && ! this . env . cid )
return ;
var n , a _cids = [ ] ,
label = action == 'delete' ? 'contactdeleting' : 'movingcontact' ,
lock = this . display _message ( label , 'loading' ) ,
display _next = this . check _display _next ( ) ;
if ( this . env . cid )
a _cids . push ( this . env . cid ) ;
else {
for ( n = 0 ; n < selection . length ; n ++ ) {
id = selection [ n ] ;
a _cids . push ( id ) ;
this . contact _list . remove _row ( id , display _next && n == selection . length - 1 ) ;
}
if ( ! display _next )
this . contact _list . clear _selection ( ) ;
}
if ( ! post _data )
post _data = { } ;
post _data . _source = this . env . source ;
post _data . _from = this . env . action ;
post _data . _cid = a _cids . join ( ',' ) ;
if ( this . env . group )
post _data . _gid = this . env . group ;
// also send search request to get the right records from the next page
if ( this . env . search _request )
post _data . _search = this . env . search _request ;
// send request to server
this . http _post ( action , post _data , lock )
return true ;
} ;
// update a contact record in the list
this . update _contact _row = function ( cid , cols _arr , newcid , source , data )
{
var list = this . contact _list ;
cid = this . html _identifier ( cid ) ;
// when in searching mode, concat cid with the source name
if ( ! list . rows [ cid ] ) {
cid = cid + '-' + source ;
if ( newcid )
newcid = newcid + '-' + source ;
}
list . update _row ( cid , cols _arr , newcid , true ) ;
list . data [ cid ] = data ;
} ;
// add row to contacts list
this . add _contact _row = function ( cid , cols , classes , data )
{
if ( ! this . gui _objects . contactslist )
return false ;
var c , col , list = this . contact _list ,
row = { cols : [ ] } ;
row . id = 'rcmrow' + this . html _identifier ( cid ) ;
row . className = 'contact ' + ( classes || '' ) ;
if ( list . in _selection ( cid ) )
row . className += ' selected' ;
// add each submitted col
for ( c in cols ) {
col = { } ;
col . className = String ( c ) . toLowerCase ( ) ;
col . innerHTML = cols [ c ] ;
row . cols . push ( col ) ;
}
// store data in list member
list . data [ cid ] = data ;
list . insert _row ( row ) ;
this . enable _command ( 'export' , list . rowcount > 0 ) ;
} ;
this . init _contact _form = function ( )
{
var col ;
if ( this . env . coltypes ) {
this . set _photo _actions ( $ ( '#ff_photo' ) . val ( ) ) ;
for ( col in this . env . coltypes )
this . init _edit _field ( col , null ) ;
}
$ ( '.contactfieldgroup .row a.deletebutton' ) . click ( function ( ) {
ref . delete _edit _field ( this ) ;
return false ;
} ) ;
$ ( 'select.addfieldmenu' ) . change ( function ( ) {
ref . insert _edit _field ( $ ( this ) . val ( ) , $ ( this ) . attr ( 'rel' ) , this ) ;
this . selectedIndex = 0 ;
} ) ;
// enable date pickers on date fields
if ( $ . datepicker && this . env . date _format ) {
$ . datepicker . setDefaults ( {
dateFormat : this . env . date _format ,
changeMonth : true ,
changeYear : true ,
yearRange : '-120:+10' ,
showOtherMonths : true ,
selectOtherMonths : true
} ) ;
$ ( 'input.datepicker' ) . datepicker ( ) ;
}
// Submit search form on Enter
if ( this . env . action == 'search' )
$ ( this . gui _objects . editform ) . append ( $ ( '<input type="submit">' ) . hide ( ) )
. submit ( function ( ) { $ ( 'input.mainaction' ) . click ( ) ; return false ; } ) ;
} ;
// group creation dialog
this . group _create = function ( )
{
var input = $ ( '<input>' ) . attr ( 'type' , 'text' ) ,
content = $ ( '<label>' ) . text ( this . get _label ( 'namex' ) ) . append ( input ) ,
source = this . env . source ;
this . simple _dialog ( content , 'newgroup' , function ( ) {
var name ;
if ( name = input . val ( ) ) {
ref . http _post ( 'group-create' , { _source : source , _name : name } ,
ref . set _busy ( true , 'loading' ) ) ;
return true ;
}
} ) ;
} ;
// group rename dialog
this . group _rename = function ( )
{
if ( ! this . env . group )
return ;
var group _name = this . env . contactgroups [ 'G' + this . env . source + this . env . group ] . name ,
input = $ ( '<input>' ) . attr ( 'type' , 'text' ) . val ( group _name ) ,
content = $ ( '<label>' ) . text ( this . get _label ( 'namex' ) ) . append ( input ) ,
source = this . env . source ,
group = this . env . group ;
this . simple _dialog ( content , 'grouprename' , function ( ) {
var name ;
if ( ( name = input . val ( ) ) && name != group _name ) {
ref . http _post ( 'group-rename' , { _source : source , _gid : group , _name : name } ,
ref . set _busy ( true , 'loading' ) ) ;
return true ;
}
} ) ;
} ;
this . group _delete = function ( )
{
if ( this . env . group ) {
var group = this . env . group ;
this . confirm _dialog ( this . get _label ( 'deletegroupconfirm' ) , 'delete' , function ( ) {
var lock = ref . set _busy ( true , 'groupdeleting' ) ;
ref . http _post ( 'group-delete' , { _source : ref . env . source , _gid : group } , lock ) ;
} ) ;
}
} ;
// callback from server upon group-delete command
this . remove _group _item = function ( prop )
{
var key = 'G' + prop . source + prop . id ;
if ( this . treelist . remove ( key ) ) {
// make sure there is no cached address book or contact group selectors
this . destroy _entity _selector ( 'addressbook-selector' ) ;
this . destroy _entity _selector ( 'contactgroup-selector' ) ;
this . triggerEvent ( 'group_delete' , { source : prop . source , id : prop . id } ) ;
delete this . env . contactfolders [ key ] ;
delete this . env . contactgroups [ key ] ;
}
if ( prop . source == this . env . source && prop . id == this . env . group )
this . list _contacts ( prop . source , 0 ) ;
} ;
//assign selected contacts to a group
this . group _assign _selected = function ( props , obj , event )
{
var cid = ref . contact _list . get _selection ( ) ;
var source = ref . env . source ;
this . contactgroup _selector ( event , function ( to ) { ref . group _member _change ( 'add' , cid , source , to ) ; } ) ;
} ;
//remove selected contacts from current active group
this . group _remove _selected = function ( )
{
this . http _post ( 'group-delmembers' , { _cid : this . contact _list . get _selection ( ) ,
_source : this . env . source , _gid : this . env . group } ) ;
} ;
//callback after deleting contact(s) from current group
this . remove _group _contacts = function ( props )
{
if ( this . env . group !== undefined && ( this . env . group === props . gid ) ) {
var n , selection = this . contact _list . get _selection ( ) ,
display _next = this . check _display _next ( ) ;
for ( n = 0 ; n < selection . length ; n ++ ) {
id = selection [ n ] ;
this . contact _list . remove _row ( id , display _next && n == selection . length - 1 ) ;
}
if ( ! display _next )
this . contact _list . clear _selection ( ) ;
}
} ;
// callback for creating a new contact group
this . insert _contact _group = function ( prop )
{
prop . type = 'group' ;
var key = 'G' + prop . source + prop . id ,
link = $ ( '<a>' ) . attr ( { href : '#' , rel : prop . source + ':' + prop . id } )
. click ( function ( ) { return ref . command ( 'listgroup' , prop , this ) ; } )
. text ( prop . name ) ;
this . env . contactfolders [ key ] = this . env . contactgroups [ key ] = prop ;
this . treelist . insert ( { id : key , html : link , classes : [ 'contactgroup' ] } , prop . source , 'contactgroup' ) ;
// make sure there is no cached address book or contact group selectors
this . destroy _entity _selector ( 'addressbook-selector' ) ;
this . destroy _entity _selector ( 'contactgroup-selector' ) ;
this . triggerEvent ( 'group_insert' , { id : prop . id , source : prop . source , name : prop . name , li : this . treelist . get _item ( key ) } ) ;
} ;
// callback for renaming a contact group
this . update _contact _group = function ( prop )
{
var key = 'G' + prop . source + prop . id ,
newnode = { } ;
// group ID has changed, replace link node and identifiers
if ( prop . newid ) {
var newkey = 'G' + prop . source + prop . newid ,
newprop = $ . extend ( { } , prop ) ;
this . env . contactfolders [ newkey ] = this . env . contactfolders [ key ] ;
this . env . contactfolders [ newkey ] . id = prop . newid ;
this . env . group = prop . newid ;
delete this . env . contactfolders [ key ] ;
delete this . env . contactgroups [ key ] ;
newprop . id = prop . newid ;
newprop . type = 'group' ;
newnode . id = newkey ;
newnode . html = $ ( '<a>' ) . attr ( { href : '#' , rel : prop . source + ':' + prop . newid } )
. click ( function ( ) { return ref . command ( 'listgroup' , newprop , this ) ; } )
. text ( prop . name ) ;
}
// update displayed group name
else {
$ ( this . treelist . get _item ( key ) ) . children ( ) . first ( ) . text ( prop . name ) ;
this . env . contactfolders [ key ] . name = this . env . contactgroups [ key ] . name = prop . name ;
if ( prop . source == this . env . source && prop . id == this . env . group )
this . set _group _prop ( prop ) ;
}
// update list node and re-sort it
this . treelist . update ( key , newnode , true ) ;
// make sure there is no cached address book or contact group selectors
this . destroy _entity _selector ( 'addressbook-selector' ) ;
this . destroy _entity _selector ( 'contactgroup-selector' ) ;
this . triggerEvent ( 'group_update' , { id : prop . id , source : prop . source , name : prop . name , li : this . treelist . get _item ( key ) , newid : prop . newid } ) ;
} ;
this . update _group _commands = function ( )
{
var source = this . env . source != '' ? this . env . address _sources [ this . env . source ] : null ,
supported = source && source . groups && ! source . readonly ;
this . enable _command ( 'group-create' , supported ) ;
this . enable _command ( 'group-rename' , 'group-delete' , supported && this . env . group ) ;
} ;
this . init _edit _field = function ( col , elem )
{
var label = this . env . coltypes [ col ] . label ;
if ( ! elem )
elem = $ ( '.ff_' + col ) ;
if ( label && ! $ ( 'label[for="ff_' + col + '"]' ) . length )
elem . placeholder ( label ) ;
} ;
this . insert _edit _field = function ( col , section , menu )
{
// just make pre-defined input field visible
var elem = $ ( '#ff_' + col ) ;
if ( elem . length ) {
$ ( 'label[for="ff_' + col + '"]' ) . parent ( ) . show ( ) ;
elem . show ( ) . focus ( ) ;
$ ( menu ) . children ( 'option[value="' + col + '"]' ) . prop ( 'disabled' , true ) ;
}
else {
var lastelem = $ ( '.ff_' + col ) ,
appendcontainer = $ ( '#contactsection' + section + ' .contactcontroller' + col ) ;
if ( ! appendcontainer . length ) {
var sect = $ ( '#contactsection' + section ) ,
lastgroup = $ ( '.contactfieldgroup' , sect ) . last ( ) ;
appendcontainer = $ ( '<fieldset>' ) . addClass ( 'contactfieldgroup contactcontroller' + col ) ;
if ( lastgroup . length )
appendcontainer . insertAfter ( lastgroup ) ;
else
sect . prepend ( appendcontainer ) ;
}
if ( appendcontainer . get ( 0 ) . nodeName == 'FIELDSET' ) {
var label , input ,
colprop = this . env . coltypes [ col ] ,
name _suffix = colprop . limit != 1 ? '[]' : '' ,
compact = $ ( menu ) . data ( 'compact' ) ? true : false ,
input _id = 'ff_' + col + ( colprop . count || 0 ) ,
row = $ ( '<div>' ) . addClass ( 'row input-group' ) ,
cell = $ ( '<div>' ) . addClass ( 'contactfieldcontent ' + colprop . type ) ;
// Field label
if ( colprop . subtypes _select ) {
label = $ ( colprop . subtypes _select ) ;
if ( ! compact )
label = $ ( '<div>' ) . addClass ( 'contactfieldlabel label' ) . append ( label ) ;
else
label . addClass ( 'input-group-prepend' ) ;
}
else {
label = $ ( '<label>' ) . addClass ( 'contactfieldlabel label input-group-text' )
. attr ( 'for' , input _id ) . text ( colprop . label ) ;
if ( compact )
label = $ ( '<span class="input-group-prepend">' ) . append ( label ) ;
}
// Field input
if ( colprop . type == 'text' || colprop . type == 'date' ) {
input = $ ( '<input>' )
. addClass ( 'form-control ff_' + col )
. attr ( { type : 'text' , name : '_' + col + name _suffix , size : colprop . size , id : input _id } ) ;
this . init _edit _field ( col , input ) ;
if ( colprop . type == 'date' && $ . datepicker )
input . addClass ( 'datepicker' ) . datepicker ( ) ;
}
else if ( colprop . type == 'textarea' ) {
input = $ ( '<textarea>' )
. addClass ( 'form-control ff_' + col )
. attr ( { name : '_' + col + name _suffix , cols : colprop . size , rows : colprop . rows , id : input _id } ) ;
this . init _edit _field ( col , input ) ;
}
else if ( colprop . type == 'composite' ) {
var i , childcol , cp , first , templ , cols = [ ] , suffices = [ ] , content = cell ;
row . addClass ( 'composite' ) ;
if ( compact )
content = $ ( '<div class="content input-group-text">' ) ;
// read template for composite field order
if ( templ = this . env [ col + '_template' ] ) {
for ( i = 0 ; i < templ . length ; i ++ ) {
cols . push ( templ [ i ] [ 1 ] ) ;
suffices . push ( templ [ i ] [ 2 ] ) ;
}
}
else { // list fields according to appearance in colprop
for ( childcol in colprop . childs )
cols . push ( childcol ) ;
}
for ( i = 0 ; i < cols . length ; i ++ ) {
childcol = cols [ i ] ;
cp = colprop . childs [ childcol ] ;
input = $ ( '<input>' )
. addClass ( 'form-control ff_' + childcol )
. attr ( { type : 'text' , name : '_' + childcol + name _suffix , size : cp . size } )
. appendTo ( content ) ;
if ( ! compact )
content . append ( suffices [ i ] || " " ) ;
this . init _edit _field ( childcol , input ) ;
if ( ! first ) first = input ;
}
if ( compact )
input = content ;
else
input = first ; // set focus to the first of this composite fields
}
else if ( colprop . type == 'select' ) {
input = $ ( '<select>' )
. addClass ( 'custom-select ff_' + col )
. attr ( { name : '_' + col + name _suffix , id : input _id } ) ;
var options = input . attr ( 'options' ) ;
options [ options . length ] = new Option ( '---' , '' ) ;
if ( colprop . options )
$ . each ( colprop . options , function ( i , val ) { options [ options . length ] = new Option ( val , i ) ; } ) ;
}
if ( input ) {
var delbutton = $ ( '<a href="#del"></a>' )
. addClass ( 'contactfieldbutton deletebutton input-group-text icon delete' )
. attr ( { title : this . get _label ( 'delete' ) , rel : col } )
. html ( this . env . delbutton )
. click ( function ( ) { ref . delete _edit _field ( this ) ; return false ; } ) ;
row . append ( label ) ;
if ( ! compact ) {
if ( colprop . type != 'composite' )
cell . append ( input ) ;
row . append ( cell . append ( delbutton ) ) ;
}
else {
row . append ( input ) . append ( delbutton ) ;
delbutton . wrap ( '<span class="input-group-append">' ) ;
}
row . appendTo ( appendcontainer . show ( ) ) ;
if ( input . is ( 'div' ) )
input . find ( 'input' ) . first ( ) . focus ( ) ;
else
input . first ( ) . focus ( ) ;
// disable option if limit reached
if ( ! colprop . count ) colprop . count = 0 ;
if ( ++ colprop . count == colprop . limit && colprop . limit )
$ ( menu ) . children ( 'option[value="' + col + '"]' ) . prop ( 'disabled' , true ) ;
this . triggerEvent ( 'insert-edit-field' , input ) ;
}
}
}
} ;
this . delete _edit _field = function ( elem )
{
var col = $ ( elem ) . attr ( 'rel' ) ,
colprop = this . env . coltypes [ col ] ,
input _group = $ ( elem ) . parents ( 'div.row' ) ,
fieldset = $ ( elem ) . parents ( 'fieldset.contactfieldgroup' ) ,
addmenu = fieldset . parent ( ) . find ( 'select.addfieldmenu' ) ;
// just clear input but don't hide the last field
if ( -- colprop . count <= 0 && colprop . visible )
input _group . find ( 'input' ) . val ( '' ) . blur ( ) ;
else {
input _group . remove ( ) ;
// hide entire fieldset if no more rows
if ( ! fieldset . children ( 'div.row' ) . length )
fieldset . hide ( ) ;
}
// enable option in add-field selector or insert it if necessary
if ( addmenu . length ) {
var option = addmenu . children ( 'option[value="' + col + '"]' ) ;
if ( option . length )
option . prop ( 'disabled' , false ) ;
else
option = $ ( '<option>' ) . attr ( 'value' , col ) . html ( colprop . label ) . appendTo ( addmenu ) ;
addmenu . show ( ) ;
}
} ;
this . upload _contact _photo = function ( form )
{
if ( form && form . elements . _photo . value ) {
this . async _upload _form ( form , 'upload-photo' , function ( e ) {
ref . set _busy ( false , null , ref . file _upload _id ) ;
} ) ;
// display upload indicator
this . file _upload _id = this . set _busy ( true , 'uploading' ) ;
}
} ;
this . replace _contact _photo = function ( id )
{
var img _src = id == '-del-' ? this . env . photo _placeholder :
this . env . comm _path + '&_action=photo&_source=' + this . env . source + '&_cid=' + ( this . env . cid || 0 ) + '&_photo=' + id ;
this . set _photo _actions ( id ) ;
$ ( this . gui _objects . contactphoto ) . children ( 'img' ) . attr ( 'src' , img _src ) ;
} ;
this . photo _upload _end = function ( )
{
this . set _busy ( false , null , this . file _upload _id ) ;
delete this . file _upload _id ;
} ;
this . set _photo _actions = function ( id )
{
var n , buttons = this . buttons [ 'upload-photo' ] ;
for ( n = 0 ; buttons && n < buttons . length ; n ++ )
$ ( 'a#' + buttons [ n ] . id ) . html ( this . get _label ( id == '-del-' ? 'addphoto' : 'replacephoto' ) ) ;
$ ( '#ff_photo' ) . val ( id ) ;
this . enable _command ( 'upload-photo' , this . env . coltypes . photo ? true : false ) ;
this . enable _command ( 'delete-photo' , this . env . coltypes . photo && id != '-del-' ) ;
} ;
// load advanced search page
this . advanced _search = function ( )
{
var dialog = $ ( '<iframe>' ) . attr ( 'src' , this . url ( 'search' , { _form : 1 , _framed : 1 } ) ) ,
search _func = function ( ) {
var valid = false , form = { _adv : 1 } ;
$ . each ( $ ( dialog [ 0 ] . contentWindow . rcmail . gui _objects . editform ) . serializeArray ( ) , function ( ) {
if ( this . name . match ( /^_search/ ) && this . value != '' ) {
form [ this . name ] = this . value ;
valid = true ;
}
} ) ;
if ( valid ) {
ref . http _post ( 'search' , form , ref . set _busy ( true , 'searching' ) ) ;
return true ;
}
} ;
this . simple _dialog ( dialog , this . gettext ( 'advsearch' ) , search _func , {
button : 'search' ,
width : 600 ,
height : 500
} ) ;
return true ;
} ;
// unselect directory/group
this . unselect _directory = function ( )
{
this . select _folder ( '' ) ;
this . enable _command ( 'search-delete' , false ) ;
} ;
// callback for creating a new saved search record
this . insert _saved _search = function ( name , id )
{
var key = 'S' + id ,
link = $ ( '<a>' ) . attr ( { href : '#' , rel : id } )
. click ( function ( ) { return ref . command ( 'listsearch' , id , this ) ; } )
. html ( name ) ,
prop = { name : name , id : id } ;
this . savedsearchlist . insert ( { id : key , html : link , classes : [ 'contactsearch' ] } , null , 'contactsearch' ) ;
this . select _folder ( key , '' , true ) ;
this . enable _command ( 'search-delete' , true ) ;
this . env . search _id = id ;
this . triggerEvent ( 'abook_search_insert' , prop ) ;
} ;
// creates a dialog for saved search
this . search _create = function ( )
{
var input = $ ( '<input>' ) . attr ( 'type' , 'text' ) ,
content = $ ( '<label>' ) . text ( this . get _label ( 'namex' ) ) . append ( input ) ;
this . simple _dialog ( content , 'searchsave' ,
function ( ) {
var name ;
if ( name = input . val ( ) ) {
ref . http _post ( 'search-create' , { _search : ref . env . search _request , _name : name } ,
ref . set _busy ( true , 'loading' ) ) ;
return true ;
}
}
) ;
} ;
this . search _delete = function ( )
{
if ( this . env . search _request ) {
var lock = this . set _busy ( true , 'savedsearchdeleting' ) ;
this . http _post ( 'search-delete' , { _sid : this . env . search _id } , lock ) ;
}
} ;
// callback from server upon search-delete command
this . remove _search _item = function ( id )
{
if ( this . savedsearchlist . remove ( 'S' + id ) ) {
this . triggerEvent ( 'search_delete' , { id : id } ) ;
}
this . env . search _id = null ;
this . env . search _request = null ;
this . list _contacts _clear ( ) ;
this . reset _qsearch ( ) ;
this . enable _command ( 'search-delete' , 'search-create' , false ) ;
} ;
this . listsearch = function ( id )
{
var lock = this . set _busy ( true , 'searching' ) ;
if ( this . contact _list ) {
this . list _contacts _clear ( ) ;
}
this . reset _qsearch ( ) ;
if ( this . savedsearchlist ) {
this . treelist . select ( '' ) ;
this . savedsearchlist . select ( 'S' + id ) ;
}
else
this . select _folder ( 'S' + id , '' , true ) ;
// reset vars
this . env . current _page = 1 ;
this . http _request ( 'search' , { _sid : id } , lock ) ;
} ;
// display a dialog with QR code image
this . qrcode = function ( )
{
var title = this . get _label ( 'qrcode' ) ,
options = { button : false , cancel _button : 'close' , width : 300 , height : 300 } ,
img = new Image ( 300 , 300 ) ;
img . src = this . url ( 'addressbook/qrcode' , { _source : this . env . source , _cid : this . get _single _cid ( ) } ) ;
return this . simple _dialog ( img , title , null , options ) ;
} ;
/*********************************************************/
/********* user settings methods *********/
/*********************************************************/
// preferences section select and load options frame
this . section _select = function ( list )
{
var win , id = list . get _single _selection ( ) ;
if ( id && ( win = this . get _frame _window ( this . env . contentframe ) ) ) {
this . location _href ( { _action : 'edit-prefs' , _section : id , _framed : 1 } , win , true ) ;
}
} ;
this . response _select = function ( list )
{
var id = list . get _single _selection ( ) ;
this . enable _command ( 'delete' , ! ! id && $ . inArray ( id , this . env . readonly _responses ) < 0 ) ;
if ( id ) {
this . load _response ( id , 'edit-response' ) ;
}
} ;
// load response record
this . load _response = function ( id , action )
{
var win ;
if ( win = this . get _frame _window ( this . env . contentframe ) ) {
if ( id || action == 'add-response' ) {
if ( ! id )
this . responses _list . clear _selection ( ) ;
this . location _href ( { _action : action , _key : id , _framed : 1 } , win , true ) ;
}
}
} ;
this . identity _select = function ( list )
{
var id = list . get _single _selection ( ) ;
this . enable _command ( 'delete' , ! ! id && list . rowcount > 1 && this . env . identities _level < 2 ) ;
if ( id ) {
this . load _identity ( id , 'edit-identity' ) ;
}
} ;
// load identity record
this . load _identity = function ( id , action )
{
var win ;
if ( win = this . get _frame _window ( this . env . contentframe ) ) {
if ( id || action == 'add-identity' ) {
if ( ! id )
this . identity _list . clear _selection ( ) ;
this . location _href ( { _action : action , _iid : id , _framed : 1 } , win , true ) ;
}
}
} ;
this . delete _identity = function ( id )
{
// exit if no identity is specified or if selection is empty
var selection = this . identity _list . get _selection ( ) ;
if ( ! ( selection . length || this . env . iid ) )
return ;
if ( ! id )
id = this . env . iid ? this . env . iid : selection [ 0 ] ;
// submit request with appended token
if ( id ) {
this . confirm _dialog ( this . get _label ( 'deleteidentityconfirm' ) , 'delete' , function ( ) {
ref . http _post ( 'settings/delete-identity' , { _iid : id } , true ) ;
} ) ;
}
} ;
this . update _identity _row = function ( id , name , add )
{
var list = this . identity _list ,
rid = this . html _identifier ( id ) ;
if ( add ) {
list . insert _row ( { id : 'rcmrow' + rid , cols : [ { className : 'mail' , innerHTML : name } ] } ) ;
list . select ( rid ) ;
}
else {
list . update _row ( rid , [ name ] ) ;
}
} ;
this . update _response _row = function ( response , oldkey )
{
var list = this . responses _list ;
if ( list && oldkey ) {
list . update _row ( oldkey , [ response . name ] , response . key , true ) ;
}
else if ( list ) {
list . insert _row ( { id : 'rcmrow' + response . key , cols : [ { className : 'name' , innerHTML : response . name } ] } ) ;
list . select ( response . key ) ;
}
} ;
this . remove _response = function ( key )
{
var frame ;
if ( this . env . textresponses ) {
delete this . env . textresponses [ key ] ;
}
if ( this . responses _list ) {
this . responses _list . remove _row ( key ) ;
this . show _contentframe ( false ) ;
}
this . enable _command ( 'delete' , false ) ;
} ;
this . remove _identity = function ( id )
{
var frame , list = this . identity _list ,
rid = this . html _identifier ( id ) ;
if ( list && id ) {
list . remove _row ( rid ) ;
this . show _contentframe ( false ) ;
}
this . enable _command ( 'delete' , false ) ;
} ;
/*********************************************************/
/********* folder manager methods *********/
/*********************************************************/
this . init _subscription _list = function ( )
{
var delim = RegExp . escape ( this . env . delimiter ) ;
this . last _sub _rx = RegExp ( '[' + delim + ']?[^' + delim + ']+$' ) ;
this . subscription _list = new rcube _treelist _widget ( this . gui _objects . subscriptionlist , {
selectable : true ,
tabexit : false ,
parent _focus : true ,
id _prefix : 'rcmli' ,
id _encode : this . html _identifier _encode ,
id _decode : this . html _identifier _decode ,
searchbox : '#foldersearch'
} ) ;
this . subscription _list
. addEventListener ( 'select' , function ( node ) { ref . subscription _select ( node . id ) ; } )
. addEventListener ( 'collapse' , function ( node ) { ref . folder _collapsed ( node ) } )
. addEventListener ( 'expand' , function ( node ) { ref . folder _collapsed ( node ) } )
. addEventListener ( 'search' , function ( p ) { if ( p . query ) ref . subscription _select ( ) ; } )
. draggable ( { cancel : 'li.mailbox.root,input,div.treetoggle,.custom-control' } )
. droppable ( {
// @todo: find better way, accept callback is executed for every folder
// on the list when dragging starts (and stops), this is slow, but
// I didn't find a method to check droptarget on over event
accept : function ( node ) {
if ( ! node . is ( '.mailbox' ) )
return false ;
var source _folder = ref . folder _id2name ( node . attr ( 'id' ) ) ,
dest _folder = ref . folder _id2name ( this . id ) ,
source = ref . env . subscriptionrows [ source _folder ] ,
dest = ref . env . subscriptionrows [ dest _folder ] ;
return source && ! source [ 2 ]
&& dest _folder != source _folder . replace ( ref . last _sub _rx , '' )
&& ! dest _folder . startsWith ( source _folder + ref . env . delimiter ) ;
} ,
drop : function ( e , ui ) {
var source = ref . folder _id2name ( ui . draggable . attr ( 'id' ) ) ,
dest = ref . folder _id2name ( this . id ) ;
ref . subscription _move _folder ( source , dest ) ;
}
} ) ;
} ;
this . folder _id2name = function ( id )
{
return id ? ref . html _identifier _decode ( id . replace ( /^rcmli/ , '' ) ) : null ;
} ;
this . subscription _select = function ( id )
{
var folder ;
if ( id && id != '*' && ( folder = this . env . subscriptionrows [ id ] ) ) {
this . env . mailbox = id ;
this . show _folder ( id ) ;
this . enable _command ( 'delete-folder' , ! folder [ 2 ] ) ;
}
else {
this . env . mailbox = null ;
this . show _contentframe ( false ) ;
this . enable _command ( 'delete-folder' , 'purge' , false ) ;
}
} ;
this . subscription _move _folder = function ( from , to )
{
if ( from && to !== null && from != to && to != from . replace ( this . last _sub _rx , '' ) ) {
var path = from . split ( this . env . delimiter ) ,
basename = path . pop ( ) ,
newname = to === '' || to === '*' ? basename : to + this . env . delimiter + basename ;
if ( newname != from ) {
this . confirm _dialog ( this . get _label ( 'movefolderconfirm' ) , 'move' , function ( ) {
ref . http _post ( 'rename-folder' , { _folder _oldname : from , _folder _newname : newname } ,
ref . set _busy ( true , 'foldermoving' ) ) ;
} , { button _class : 'save move' } ) ;
}
}
} ;
// tell server to create and subscribe a new mailbox
this . create _folder = function ( )
{
this . show _folder ( '' , this . env . mailbox ) ;
} ;
// delete a specific mailbox with all its messages
this . delete _folder = function ( name )
{
if ( ! name )
name = this . env . mailbox ;
if ( name ) {
this . confirm _dialog ( this . get _label ( 'deletefolderconfirm' ) , 'delete' , function ( ) {
ref . http _post ( 'delete-folder' , { _mbox : name } , ref . set _busy ( true , 'folderdeleting' ) ) ;
} ) ;
}
} ;
// Add folder row to the table and initialize it
this . add _folder _row = function ( id , name , display _name , is _protected , subscribed , class _name , refrow , subfolders )
{
if ( ! this . gui _objects . subscriptionlist )
return false ;
// reset searching
if ( this . subscription _list . is _search ( ) ) {
this . subscription _select ( ) ;
this . subscription _list . reset _search ( ) ;
}
// disable drag-n-drop temporarily
// some skins disable dragging in mobile mode, so we have to check if it is still draggable
if ( this . subscription _list . is _draggable ( ) )
this . subscription _list . draggable ( 'destroy' ) . droppable ( 'destroy' ) ;
var row , n , tmp , tmp _name , rowid , collator , pos , p , parent = '' ,
folders = [ ] , list = [ ] , slist = [ ] ,
list _element = $ ( this . gui _objects . subscriptionlist ) ;
row = refrow ? refrow : $ ( $ ( 'li' , list _element ) . get ( 1 ) ) . clone ( true ) ;
if ( ! row . length ) {
// Refresh page if we don't have a table row to clone
this . goto _url ( 'folders' ) ;
return false ;
}
// set ID, reset css class
row . attr ( { id : 'rcmli' + this . html _identifier _encode ( id ) , 'class' : class _name } ) ;
if ( ! refrow || ! refrow . length ) {
// remove old data, subfolders and toggle
$ ( 'ul,div.treetoggle' , row ) . remove ( ) ;
row . removeData ( 'filtered' ) ;
}
// set folder name
$ ( 'a' , row ) . first ( ) . text ( display _name ) . removeAttr ( 'title' ) ;
// update subscription checkbox
$ ( 'input[name="_subscribed[]"]' , row ) . first ( ) . val ( id )
. prop ( { checked : subscribed ? true : false , disabled : is _protected ? true : false } ) ;
// add to folder/row-ID map
this . env . subscriptionrows [ id ] = [ name , display _name , false ] ;
// copy folders data to an array for sorting
$ . each ( this . env . subscriptionrows , function ( k , v ) { v [ 3 ] = k ; folders . push ( v ) ; } ) ;
try {
// use collator if supported (FF29, IE11, Opera15, Chrome24)
collator = new Intl . Collator ( this . env . locale . replace ( '_' , '-' ) ) ;
}
catch ( e ) { } ;
// sort folders
folders . sort ( function ( a , b ) {
var i , f1 , f2 ,
path1 = a [ 0 ] . split ( ref . env . delimiter ) ,
path2 = b [ 0 ] . split ( ref . env . delimiter ) ,
len = path1 . length ;
for ( i = 0 ; i < len ; i ++ ) {
f1 = path1 [ i ] ;
f2 = path2 [ i ] ;
if ( f1 !== f2 ) {
if ( f2 === undefined )
return 1 ;
if ( collator )
return collator . compare ( f1 , f2 ) ;
else
return f1 < f2 ? - 1 : 1 ;
}
else if ( i == len - 1 ) {
return - 1
}
}
} ) ;
for ( n in folders ) {
p = folders [ n ] [ 3 ] ;
// protected folder
if ( folders [ n ] [ 2 ] ) {
tmp _name = p + this . env . delimiter ;
// prefix namespace cannot have subfolders (#1488349)
if ( tmp _name == this . env . prefix _ns )
continue ;
slist . push ( p ) ;
tmp = tmp _name ;
}
// protected folder's child
else if ( tmp && p . startsWith ( tmp ) )
slist . push ( p ) ;
// other
else {
list . push ( p ) ;
tmp = null ;
}
}
// check if subfolder of a protected folder
for ( n = 0 ; n < slist . length ; n ++ ) {
if ( id . startsWith ( slist [ n ] + this . env . delimiter ) )
rowid = slist [ n ] ;
}
// find folder position after sorting
for ( n = 0 ; ! rowid && n < list . length ; n ++ ) {
if ( n && list [ n ] == id )
rowid = list [ n - 1 ] ;
}
// add row to the table
if ( rowid && ( n = this . subscription _list . get _item ( rowid , true ) ) ) {
// find parent folder
if ( pos = id . lastIndexOf ( this . env . delimiter ) ) {
parent = id . substring ( 0 , pos ) ;
parent = this . subscription _list . get _item ( parent , true ) ;
// add required tree elements to the parent if not already there
if ( ! $ ( 'div.treetoggle' , parent ) . length ) {
$ ( '<div> </div>' ) . addClass ( 'treetoggle collapsed' ) . appendTo ( parent ) ;
}
if ( ! $ ( 'ul' , parent ) . length ) {
$ ( '<ul>' ) . css ( 'display' , 'none' ) . appendTo ( parent ) ;
}
}
if ( parent && n == parent ) {
$ ( 'ul' , parent ) . first ( ) . append ( row ) ;
}
else {
while ( p = $ ( n ) . parent ( ) . parent ( ) . get ( 0 ) ) {
if ( parent && p == parent )
break ;
if ( ! $ ( p ) . is ( 'li.mailbox' ) )
break ;
n = p ;
}
$ ( n ) . after ( row ) ;
}
}
else {
list _element . append ( row ) ;
}
// add subfolders
$ . extend ( this . env . subscriptionrows , subfolders || { } ) ;
// update list widget
this . subscription _list . reset ( true ) ;
this . subscription _select ( ) ;
// expand parent
if ( parent ) {
this . subscription _list . expand ( this . folder _id2name ( parent . id ) ) ;
}
row = row . show ( ) . get ( 0 ) ;
if ( row . scrollIntoView )
row . scrollIntoView ( false ) ;
// Let skins to do their magic, e.g. Elastic will fix pretty checkbox
if ( ! refrow )
this . triggerEvent ( 'clonerow' , { row : row , id : id } ) ;
return row ;
} ;
// replace an existing table row with a new folder line (with subfolders)
this . replace _folder _row = function ( oldid , id , name , display _name , is _protected , class _name )
{
if ( ! this . gui _objects . subscriptionlist ) {
if ( this . is _framed ( ) ) {
// @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.'
return window . parent . rcmail . replace _folder _row ( oldid , id , name , display _name , is _protected , class _name ) ;
}
return false ;
}
// reset searching
if ( this . subscription _list . is _search ( ) ) {
this . subscription _select ( ) ;
this . subscription _list . reset _search ( ) ;
}
var subfolders = { } ,
row = this . subscription _list . get _item ( oldid , true ) ,
parent = $ ( row ) . parent ( ) ,
old _folder = this . env . subscriptionrows [ oldid ] ,
prefix _len _id = oldid . length ,
prefix _len _name = old _folder [ 0 ] . length ,
subscribed = $ ( 'input[name="_subscribed[]"]' , row ) . first ( ) . prop ( 'checked' ) ;
// no renaming, only update class_name
if ( oldid == id ) {
$ ( row ) . attr ( 'class' , class _name || '' ) ;
return ;
}
// update subfolders
$ ( 'li' , row ) . each ( function ( ) {
var fname = ref . folder _id2name ( this . id ) ,
folder = ref . env . subscriptionrows [ fname ] ,
newid = id + fname . slice ( prefix _len _id ) ;
this . id = 'rcmli' + ref . html _identifier _encode ( newid ) ;
$ ( 'input[name="_subscribed[]"]' , this ) . first ( ) . val ( newid ) ;
folder [ 0 ] = name + folder [ 0 ] . slice ( prefix _len _name ) ;
subfolders [ newid ] = folder ;
delete ref . env . subscriptionrows [ fname ] ;
} ) ;
// get row off the list
row = $ ( row ) . detach ( ) ;
delete this . env . subscriptionrows [ oldid ] ;
// remove parent list/toggle elements if not needed
if ( parent . get ( 0 ) != this . gui _objects . subscriptionlist && ! $ ( 'li' , parent ) . length ) {
$ ( 'ul,div.treetoggle' , parent . parent ( ) ) . remove ( ) ;
}
// move the existing table row
this . add _folder _row ( id , name , display _name , is _protected , subscribed , class _name , row , subfolders ) ;
} ;
// remove the table row of a specific mailbox from the table
this . remove _folder _row = function ( folder )
{
// reset searching
if ( this . subscription _list . is _search ( ) ) {
this . subscription _select ( ) ;
this . subscription _list . reset _search ( ) ;
}
var list = [ ] , row = this . subscription _list . get _item ( folder , true ) ;
// get subfolders if any
$ ( 'li' , row ) . each ( function ( ) { list . push ( ref . folder _id2name ( this . id ) ) ; } ) ;
// remove folder row (and subfolders)
this . subscription _list . remove ( folder ) ;
// update local list variable
list . push ( folder ) ;
$ . each ( list , function ( i , v ) { delete ref . env . subscriptionrows [ v ] ; } ) ;
} ;
this . subscribe = function ( folder )
{
if ( folder ) {
var lock = this . display _message ( 'foldersubscribing' , 'loading' ) ;
this . http _post ( 'subscribe' , { _mbox : folder } , lock ) ;
}
} ;
this . unsubscribe = function ( folder )
{
if ( folder ) {
var lock = this . display _message ( 'folderunsubscribing' , 'loading' ) ;
this . http _post ( 'unsubscribe' , { _mbox : folder } , lock ) ;
}
} ;
// when user select a folder in manager
this . show _folder = function ( folder , path , force )
{
var win , target = window ,
action = folder === '' ? 'add' : 'edit' ,
url = '&_action=' + action + '-folder&_mbox=' + urlencode ( folder ) ;
if ( path )
url += '&_path=' + urlencode ( path ) ;
if ( win = this . get _frame _window ( this . env . contentframe ) ) {
target = win ;
url += '&_framed=1' ;
}
if ( String ( target . location . href ) . indexOf ( url ) >= 0 && ! force )
this . show _contentframe ( true ) ;
else
this . location _href ( this . env . comm _path + url , target , true ) ;
} ;
// disables subscription checkbox (for protected folder)
this . disable _subscription = function ( folder )
{
var row = this . subscription _list . get _item ( folder , true ) ;
if ( row )
$ ( 'input[name="_subscribed[]"]' , row ) . first ( ) . prop ( 'disabled' , true ) ;
} ;
// resets state of subscription checkbox (e.g. on error)
this . reset _subscription = function ( folder , state )
{
var row = this . subscription _list . get _item ( folder , true ) ;
if ( row )
$ ( 'input[name="_subscribed[]"]' , row ) . first ( ) . prop ( 'checked' , state ) ;
} ;
this . folder _size = function ( folder )
{
var lock = this . set _busy ( true , 'loading' ) ;
this . http _post ( 'folder-size' , { _mbox : folder } , lock ) ;
} ;
this . folder _size _update = function ( size )
{
$ ( '#folder-size' ) . replaceWith ( size ) ;
} ;
// filter folders by namespace
this . folder _filter = function ( prefix )
{
this . subscription _list . reset _search ( ) ;
this . subscription _list . container . children ( 'li' ) . each ( function ( ) {
var i , folder = ref . folder _id2name ( this . id ) ;
// show all folders
if ( prefix == '---' ) {
}
// got namespace prefix
else if ( prefix ) {
if ( folder !== prefix ) {
$ ( this ) . data ( 'filtered' , true ) . hide ( ) ;
return
}
}
// no namespace prefix, filter out all other namespaces
else {
// first get all namespace roots
for ( i in ref . env . ns _roots ) {
if ( folder === ref . env . ns _roots [ i ] ) {
$ ( this ) . data ( 'filtered' , true ) . hide ( ) ;
return ;
}
}
}
$ ( this ) . removeData ( 'filtered' ) . show ( ) ;
} ) ;
} ;
/*********************************************************/
/********* GUI functionality *********/
/*********************************************************/
this . init _button = function ( cmd , prop )
{
var elm = document . getElementById ( prop . id ) ;
if ( ! elm )
return ;
var preload = false ;
if ( prop . type == 'image' ) {
elm = elm . parentNode ;
preload = true ;
}
elm . _command = cmd ;
elm . _id = prop . id ;
if ( prop . sel ) {
elm . onmousedown = function ( e ) { return ref . button _sel ( this . _command , this . _id ) ; } ;
elm . onmouseup = function ( e ) { return ref . button _out ( this . _command , this . _id ) ; } ;
if ( preload )
new Image ( ) . src = prop . sel ;
}
if ( prop . over ) {
elm . onmouseover = function ( e ) { return ref . button _over ( this . _command , this . _id ) ; } ;
elm . onmouseout = function ( e ) { return ref . button _out ( this . _command , this . _id ) ; } ;
if ( preload )
new Image ( ) . src = prop . over ;
}
} ;
// set event handlers on registered buttons
this . init _buttons = function ( )
{
for ( var cmd in this . buttons ) {
if ( typeof cmd !== 'string' )
continue ;
for ( var i = 0 ; i < this . buttons [ cmd ] . length ; i ++ ) {
this . init _button ( cmd , this . buttons [ cmd ] [ i ] ) ;
}
}
} ;
// set button to a specific state
this . set _button = function ( command , state )
{
var n , button , obj , a _buttons = this . buttons [ command ] ,
len = a _buttons ? a _buttons . length : 0 ;
for ( n = 0 ; n < len ; n ++ ) {
button = a _buttons [ n ] ;
obj = document . getElementById ( button . id ) ;
if ( ! obj || button . status === state )
continue ;
// get default/passive setting of the button
if ( button . type == 'image' && ! button . status ) {
button . pas = obj . _original _src ? obj . _original _src : obj . src ;
// respect PNG fix on IE browsers
if ( obj . runtimeStyle && obj . runtimeStyle . filter && obj . runtimeStyle . filter . match ( /src=['"]([^'"]+)['"]/ ) )
button . pas = RegExp . $1 ;
}
else if ( ! button . status )
button . pas = String ( obj . className ) ;
button . status = state ;
// set image according to button state
if ( button . type == 'image' && button [ state ] ) {
obj . src = button [ state ] ;
}
// set class name according to button state
else if ( button [ state ] !== undefined ) {
obj . className = button [ state ] ;
}
// disable/enable input buttons
if ( button . type == 'input' || button . type == 'button' ) {
obj . disabled = state == 'pas' ;
}
else {
$ ( obj ) . attr ( {
tabindex : state == 'pas' || state == 'sel' ? '-1' : ( $ ( obj ) . attr ( 'data-tabindex' ) || '0' ) ,
'aria-disabled' : state == 'pas' || state == 'sel' ? 'true' : 'false'
} ) ;
}
}
} ;
// display a specific alttext
this . set _alttext = function ( command , label )
{
var n , button , obj , link , label ,
a _buttons = this . buttons [ command ] ,
len = a _buttons ? a _buttons . length : 0 ;
for ( n = 0 ; n < len ; n ++ ) {
button = a _buttons [ n ] ;
obj = document . getElementById ( button . id ) ;
label = this . get _label ( label ) ;
if ( obj && button . type == 'image' ) {
obj . setAttribute ( 'alt' , label ) ;
if ( ( link = obj . parentNode ) && link . tagName . toLowerCase ( ) == 'a' )
link . setAttribute ( 'title' , label ) ;
}
else if ( obj )
obj . setAttribute ( 'title' , label ) ;
}
} ;
// mouse over button
this . button _over = function ( command , id )
{
this . button _event ( command , id , 'over' ) ;
} ;
// mouse down on button
this . button _sel = function ( command , id )
{
this . button _event ( command , id , 'sel' ) ;
} ;
// mouse out of button
this . button _out = function ( command , id )
{
this . button _event ( command , id , 'act' ) ;
} ;
// event of button
this . button _event = function ( command , id , event )
{
var n , button , obj , a _buttons = this . buttons [ command ] ,
len = a _buttons ? a _buttons . length : 0 ;
for ( n = 0 ; n < len ; n ++ ) {
button = a _buttons [ n ] ;
if ( button . id == id && button . status == 'act' ) {
if ( button [ event ] && ( obj = document . getElementById ( button . id ) ) ) {
obj [ button . type == 'image' ? 'src' : 'className' ] = button [ event ] ;
}
if ( event == 'sel' ) {
this . buttons _sel [ id ] = command ;
}
}
}
} ;
// write to the document/window title
this . set _pagetitle = function ( title )
{
if ( title && document . title )
document . title = title ;
} ;
// display a system message, list of types in common.css (below #message definition)
this . display _message = function ( msg , type , timeout , key )
{
if ( msg && msg . length && /^[a-z._]+$/ . test ( msg ) )
msg = this . get _label ( msg ) ;
// pass command to parent window
if ( this . is _framed ( ) )
return parent . rcmail . display _message ( msg , type , timeout ) ;
if ( ! this . gui _objects . message ) {
// save message in order to display after page loaded
if ( type != 'loading' )
this . pending _message = [ msg , type , timeout , key ] ;
return 1 ;
}
if ( ! type )
type = 'notice' ;
else if ( type == 'loading' ) {
if ( ! key )
key = 'loading' ;
if ( ! timeout )
timeout = this . env . request _timeout * 1000 ;
if ( ! msg )
msg = this . get _label ( 'loading' ) ;
}
if ( ! key )
key = this . html _identifier ( msg ) ;
var date = new Date ( ) ,
id = type + date . getTime ( ) ;
if ( ! timeout ) {
switch ( type ) {
case 'error' :
case 'warning' :
timeout = this . message _time * 2 ;
break ;
case 'uploading' :
timeout = 0 ;
break ;
default :
timeout = this . message _time ;
}
}
// The same message is already displayed
if ( this . messages [ key ] ) {
// replace label
if ( this . messages [ key ] . obj )
$ ( 'div.content' , this . messages [ key ] . obj ) . html ( msg ) ;
// store label in stack
if ( type == 'loading' ) {
this . messages [ key ] . labels . push ( { 'id' : id , 'msg' : msg } ) ;
}
// add element and set timeout
this . messages [ key ] . elements . push ( id ) ;
setTimeout ( function ( ) { ref . hide _message ( id , type == 'loading' ) ; } , timeout ) ;
return id ;
}
// create DOM object and display it
var obj = $ ( '<div>' ) . addClass ( type + ' content' ) . html ( msg ) . data ( 'key' , key ) ,
cont = $ ( this . gui _objects . message ) . append ( obj ) . show ( ) ;
this . messages [ key ] = { 'obj' : obj , 'elements' : [ id ] } ;
if ( type == 'loading' ) {
this . messages [ key ] . labels = [ { 'id' : id , 'msg' : msg } ] ;
}
else if ( type != 'uploading' ) {
obj . click ( function ( ) { return ref . hide _message ( obj ) ; } )
. attr ( 'role' , 'alert' ) ;
}
this . triggerEvent ( 'message' , { message : msg , type : type , timeout : timeout , object : obj } ) ;
if ( timeout > 0 )
setTimeout ( function ( ) { ref . hide _message ( id , type != 'loading' ) ; } , timeout ) ;
return id ;
} ;
// make a message to disapear
this . hide _message = function ( obj , fade )
{
// pass command to parent window
if ( this . is _framed ( ) )
return parent . rcmail . hide _message ( obj , fade ) ;
if ( ! this . gui _objects . message )
return ;
var k , n , i , o , m = this . messages ;
// Hide message by object, don't use for 'loading'!
if ( typeof obj === 'object' ) {
o = $ ( obj ) ;
k = o . data ( 'key' ) ;
this . hide _message _object ( o , fade ) ;
if ( m [ k ] )
delete m [ k ] ;
}
// Hide message by id
else {
for ( k in m ) {
for ( n in m [ k ] . elements ) {
if ( m [ k ] && m [ k ] . elements [ n ] == obj ) {
m [ k ] . elements . splice ( n , 1 ) ;
// hide DOM element if last instance is removed
if ( ! m [ k ] . elements . length ) {
this . hide _message _object ( m [ k ] . obj , fade ) ;
delete m [ k ] ;
}
// set pending action label for 'loading' message
else if ( k == 'loading' ) {
for ( i in m [ k ] . labels ) {
if ( m [ k ] . labels [ i ] . id == obj ) {
delete m [ k ] . labels [ i ] ;
}
else {
o = m [ k ] . labels [ i ] . msg ;
$ ( 'div.content' , m [ k ] . obj ) . html ( o ) ;
}
}
}
}
}
}
}
} ;
// hide message object and remove from the DOM
this . hide _message _object = function ( o , fade )
{
if ( fade )
o . fadeOut ( 600 , function ( ) { $ ( this ) . remove ( ) ; } ) ;
else
o . hide ( ) . remove ( ) ;
} ;
// remove all messages immediately
this . clear _messages = function ( )
{
// pass command to parent window
if ( this . is _framed ( ) )
return parent . rcmail . clear _messages ( ) ;
var k , n , m = this . messages ;
for ( k in m )
for ( n in m [ k ] . elements )
if ( m [ k ] . obj )
this . hide _message _object ( m [ k ] . obj ) ;
this . messages = { } ;
} ;
// display uploading message with progress indicator
// data should contain: name, total, current, percent, text
this . display _progress = function ( data )
{
if ( ! data || ! data . name )
return ;
var msg = this . messages [ 'progress' + data . name ] ;
if ( ! data . label )
data . label = this . get _label ( 'uploadingmany' ) ;
if ( ! msg ) {
if ( ! data . percent || data . percent < 100 )
this . display _message ( data . label , 'uploading' , 0 , 'progress' + data . name ) ;
return ;
}
if ( ! data . total || data . percent >= 100 ) {
this . hide _message ( msg . obj ) ;
return ;
}
if ( data . text )
data . label += ' ' + data . text ;
msg . obj . text ( data . label ) ;
} ;
// open a jquery UI dialog with the given content
this . show _popup _dialog = function ( content , title , buttons , options )
{
// forward call to parent window
if ( this . is _framed ( ) ) {
return parent . rcmail . show _popup _dialog ( content , title , buttons , options ) ;
}
var popup = $ ( '<div class="popup">' ) ;
if ( typeof content == 'object' ) {
popup . append ( content ) ;
if ( $ ( content ) . is ( 'iframe' ) )
popup . addClass ( 'iframe' ) ;
}
else
popup . html ( content ) ;
// assign special classes to dialog buttons
var i = 0 , fn = function ( button , classes , idx ) {
if ( typeof button == 'function' ) {
button = {
click : button ,
text : idx ,
'class' : classes
} ;
}
else {
buttons [ 'class' ] = classes ;
}
return button ;
} ;
if ( options && options . button _classes )
$ . each ( buttons , function ( idx , button ) {
var cl = options . button _classes [ i ] ;
if ( cl )
buttons [ idx ] = fn ( button , cl , idx ) ;
i ++ ;
} ) ;
options = $ . extend ( {
title : title ,
buttons : buttons ,
modal : true ,
resizable : true ,
width : 500 ,
close : function ( event , ui ) { $ ( this ) . remove ( ) ; }
} , options || { } ) ;
popup . dialog ( options ) ;
if ( options . width )
popup . width ( options . width ) ;
if ( options . height )
popup . height ( options . height ) ;
var dialog = popup . parent ( ) ;
if ( ! options . noresize ) {
// resize and center popup
var win = $ ( window ) , w = win . width ( ) , h = win . height ( ) ,
width = popup . width ( ) ,
height = options . height || ( popup [ 0 ] . scrollHeight + 20 ) ,
titlebar _height = $ ( '.ui-dialog-titlebar' , dialog ) . outerHeight ( ) || 0 ,
buttonpane _height = $ ( '.ui-dialog-buttonpane' , dialog ) . outerHeight ( ) || 0 ,
padding = ( parseInt ( dialog . css ( 'padding-top' ) ) + parseInt ( popup . css ( 'padding-top' ) ) ) * 2 ;
popup . dialog ( 'option' , {
height : Math . min ( h - 40 , height + titlebar _height + buttonpane _height + padding + 2 ) ,
width : Math . min ( w - 20 , width + 24 )
} ) ;
}
else {
popup . css ( 'width' , 'auto' ) ;
}
// Don't propagate keyboard events to the UI below the dialog (#6055)
dialog . on ( 'keydown keyup' , function ( e ) { e . stopPropagation ( ) ; } ) ;
this . triggerEvent ( 'dialog-open' , { obj : popup } ) ;
return popup ;
} ;
// show_popup_dialog() wrapper for simple dialogs with action and Cancel buttons
this . simple _dialog = function ( content , title , action _func , options )
{
if ( ! options )
options = { } ;
var title = this . get _label ( title ) ,
save _label = options . button || 'save' ,
save _class = options . button _class || save _label . replace ( /^[^\.]+\./i , '' ) ,
cancel _label = options . cancel _button || 'cancel' ,
cancel _class = options . cancel _class || cancel _label . replace ( /^[^\.]+\./i , '' ) ,
close _func = function ( e , ui , dialog ) {
( ref . is _framed ( ) ? parent . $ : $ ) ( dialog || this ) . dialog ( 'close' ) ;
if ( options . cancel _func ) options . cancel _func ( e , ref ) ;
} ,
buttons = [ {
text : this . get _label ( cancel _label ) ,
'class' : cancel _class . replace ( /close/i , 'cancel' ) ,
click : close _func
} ] ;
if ( ! action _func )
buttons [ 0 ] [ 'class' ] += ' mainaction' ;
else
buttons . unshift ( {
text : this . get _label ( save _label ) ,
'class' : 'mainaction ' + save _class ,
click : function ( e , ui ) { if ( action _func ( e , ref ) ) close _func ( e , ui , this ) ; }
} ) ;
return this . show _popup _dialog ( content , title , buttons , options ) ;
} ;
// show_popup_dialog() wrapper for alert() type dialogs
this . alert _dialog = function ( content , action , options )
{
options = $ . extend ( options || { } , {
cancel _button : 'ok' ,
cancel _class : 'save' ,
cancel _func : action ,
noresize : true
} ) ;
return this . simple _dialog ( content , options . title || 'alerttitle' , null , options ) ;
} ;
// simple_dialog() wrapper for confirm() type dialogs
this . confirm _dialog = function ( content , button _label , action , options )
{
var action _func = function ( e , ref ) { action ( e , ref ) ; return true ; } ;
options = $ . extend ( options || { } , {
button : button _label || 'continue' ,
noresize : true
} ) ;
return this . simple _dialog ( content , options . title || 'confirmationtitle' , action _func , options ) ;
} ;
// enable/disable buttons for page shifting
this . set _page _buttons = function ( )
{
this . enable _command ( 'nextpage' , 'lastpage' , this . env . pagecount > this . env . current _page ) ;
this . enable _command ( 'previouspage' , 'firstpage' , this . env . current _page > 1 ) ;
this . update _pagejumper ( ) ;
} ;
// mark a mailbox as selected and set environment variable
this . select _folder = function ( name , prefix , encode )
{
if ( this . savedsearchlist ) {
this . savedsearchlist . select ( '' ) ;
}
if ( this . treelist ) {
this . treelist . select ( name ) ;
}
else if ( this . gui _objects . folderlist ) {
$ ( 'li.selected' , this . gui _objects . folderlist ) . removeClass ( 'selected' ) ;
$ ( this . get _folder _li ( name , prefix , encode ) ) . addClass ( 'selected' ) ;
// trigger event hook
this . triggerEvent ( 'selectfolder' , { folder : name , prefix : prefix } ) ;
}
} ;
// adds a class to selected folder
this . mark _folder = function ( name , class _name , prefix , encode )
{
$ ( this . get _folder _li ( name , prefix , encode ) ) . addClass ( class _name ) ;
this . triggerEvent ( 'markfolder' , { folder : name , mark : class _name , status : true } ) ;
} ;
// adds a class to selected folder
this . unmark _folder = function ( name , class _name , prefix , encode )
{
$ ( this . get _folder _li ( name , prefix , encode ) ) . removeClass ( class _name ) ;
this . triggerEvent ( 'markfolder' , { folder : name , mark : class _name , status : false } ) ;
} ;
// helper method to find a folder list item
this . get _folder _li = function ( name , prefix , encode )
{
if ( ! prefix )
prefix = 'rcmli' ;
if ( this . gui _objects . folderlist ) {
name = this . html _identifier ( name , encode ) ;
return document . getElementById ( prefix + name ) ;
}
} ;
// for reordering column array (Konqueror workaround)
// and for setting some message list global variables
this . set _message _coltypes = function ( listcols , repl , smart _col )
{
var list = this . message _list ,
thead = list ? list . thead : null ,
repl , cell , col , n , len , tr ;
this . env . listcols = listcols ;
if ( ! this . env . coltypes )
this . env . coltypes = { } ;
// replace old column headers
if ( thead ) {
if ( repl ) {
thead . innerHTML = '' ;
tr = document . createElement ( 'tr' ) ;
for ( c = 0 , len = repl . length ; c < len ; c ++ ) {
cell = document . createElement ( 'th' ) ;
cell . innerHTML = repl [ c ] . html || '' ;
if ( repl [ c ] . id ) cell . id = repl [ c ] . id ;
if ( repl [ c ] . className ) cell . className = repl [ c ] . className ;
tr . appendChild ( cell ) ;
}
if ( list . checkbox _selection )
list . insert _checkbox ( tr , 'thead' ) ;
thead . appendChild ( tr ) ;
}
for ( n = 0 , len = this . env . listcols . length ; n < len ; n ++ ) {
col = this . env . listcols [ list . checkbox _selection ? n - 1 : n ] ;
if ( ( cell = thead . rows [ 0 ] . cells [ n ] ) && ( col == 'from' || col == 'to' || col == 'fromto' ) ) {
$ ( cell ) . attr ( 'rel' , col ) . find ( 'span,a' ) . text ( this . get _label ( col == 'fromto' ? smart _col : col ) ) ;
}
}
}
this . env . subject _col = null ;
this . env . flagged _col = null ;
this . env . status _col = null ;
if ( this . env . coltypes . folder )
this . env . coltypes . folder . hidden = ! ( this . env . search _request || this . env . search _id ) || this . env . search _scope == 'base' ;
if ( ( n = $ . inArray ( 'subject' , this . env . listcols ) ) >= 0 ) {
this . env . subject _col = n ;
if ( list )
list . subject _col = n ;
}
if ( ( n = $ . inArray ( 'flag' , this . env . listcols ) ) >= 0 )
this . env . flagged _col = n ;
if ( ( n = $ . inArray ( 'status' , this . env . listcols ) ) >= 0 )
this . env . status _col = n ;
if ( list ) {
list . hide _column ( 'folder' , ( this . env . coltypes . folder && this . env . coltypes . folder . hidden ) || $ . inArray ( 'folder' , this . env . listcols ) < 0 ) ;
list . init _header ( ) ;
}
} ;
// replace content of row count display
this . set _rowcount = function ( text , mbox )
{
// #1487752
if ( mbox && mbox != this . env . mailbox )
return false ;
$ ( this . gui _objects . countdisplay ) . html ( text ) ;
// update page navigation buttons
this . set _page _buttons ( ) ;
} ;
// replace content of mailboxname display
this . set _mailboxname = function ( content )
{
if ( this . gui _objects . mailboxname && content )
this . gui _objects . mailboxname . innerHTML = content ;
} ;
// replace content of quota display
this . set _quota = function ( content )
{
if ( this . gui _objects . quotadisplay && content && content . type == 'text' )
$ ( this . gui _objects . quotadisplay ) . text ( ( content . percent || 0 ) + '%' ) . attr ( 'title' , content . title || '' ) ;
this . triggerEvent ( 'setquota' , content ) ;
this . env . quota _content = content ;
} ;
// update trash folder state
this . set _trash _count = function ( count )
{
this [ ( count ? 'un' : '' ) + 'mark_folder' ] ( this . env . trash _mailbox , 'empty' , '' , true ) ;
} ;
// update the mailboxlist
this . set _unread _count = function ( mbox , count , set _title , mark )
{
if ( ! this . gui _objects . mailboxlist )
return false ;
this . env . unread _counts [ mbox ] = count ;
this . set _unread _count _display ( mbox , set _title ) ;
if ( mark )
this . mark _folder ( mbox , mark , '' , true ) ;
else if ( ! count )
this . unmark _folder ( mbox , 'recent' , '' , true ) ;
this . mark _all _read _state ( ) ;
} ;
// update the mailbox count display
this . set _unread _count _display = function ( mbox , set _title )
{
var reg , link , text _obj , item , mycount , childcount , div ;
if ( item = this . get _folder _li ( mbox , '' , true ) ) {
mycount = this . env . unread _counts [ mbox ] ? this . env . unread _counts [ mbox ] : 0 ;
link = $ ( item ) . children ( 'a' ) . eq ( 0 ) ;
text _obj = link . children ( 'span.unreadcount' ) ;
if ( ! text _obj . length && mycount )
text _obj = $ ( '<span>' ) . addClass ( 'unreadcount skip-content' ) . appendTo ( link ) ;
reg = /\s+\([0-9]+\)$/i ;
childcount = 0 ;
if ( ( div = item . getElementsByTagName ( 'div' ) [ 0 ] ) &&
div . className . match ( /collapsed/ ) ) {
// add children's counters
for ( var k in this . env . unread _counts )
if ( k . startsWith ( mbox + this . env . delimiter ) )
childcount += this . env . unread _counts [ k ] ;
}
if ( mycount && text _obj . length )
text _obj . html ( this . env . unreadwrap . replace ( /%[sd]/ , mycount ) ) ;
else if ( text _obj . length )
text _obj . remove ( ) ;
// set parent's display
reg = new RegExp ( RegExp . escape ( this . env . delimiter ) + '[^' + RegExp . escape ( this . env . delimiter ) + ']+$' ) ;
if ( mbox . match ( reg ) )
this . set _unread _count _display ( mbox . replace ( reg , '' ) , false ) ;
// set the right classes
if ( ( mycount + childcount ) > 0 )
$ ( item ) . addClass ( 'unread' ) ;
else
$ ( item ) . removeClass ( 'unread' ) ;
}
// set unread count to window title
reg = /^\([0-9]+\)\s+/i ;
if ( set _title && document . title ) {
var new _title = '' ,
doc _title = String ( document . title ) ;
if ( mycount && doc _title . match ( reg ) )
new _title = doc _title . replace ( reg , '(' + mycount + ') ' ) ;
else if ( mycount )
new _title = '(' + mycount + ') ' + doc _title ;
else
new _title = doc _title . replace ( reg , '' ) ;
this . set _pagetitle ( new _title ) ;
}
} ;
// display fetched raw headers
this . set _headers = function ( content )
{
if ( this . gui _objects . all _headers _box && content )
$ ( this . gui _objects . all _headers _box ) . html ( content ) . show ( ) ;
} ;
// display all-headers row and fetch raw message headers
this . show _headers = function ( props , elem )
{
if ( ! this . gui _objects . all _headers _row || ! this . gui _objects . all _headers _box || ! this . env . uid )
return ;
$ ( elem ) . removeClass ( 'show-headers' ) . addClass ( 'hide-headers' ) ;
$ ( this . gui _objects . all _headers _row ) . show ( ) ;
elem . onclick = function ( ) { ref . command ( 'hide-headers' , '' , elem ) ; } ;
// fetch headers only once
if ( ! this . gui _objects . all _headers _box . innerHTML ) {
this . http _request ( 'headers' , { _uid : this . env . uid , _mbox : this . env . mailbox } ,
this . display _message ( '' , 'loading' )
) ;
}
} ;
// hide all-headers row
this . hide _headers = function ( props , elem )
{
if ( ! this . gui _objects . all _headers _row || ! this . gui _objects . all _headers _box )
return ;
$ ( elem ) . removeClass ( 'hide-headers' ) . addClass ( 'show-headers' ) ;
$ ( this . gui _objects . all _headers _row ) . hide ( ) ;
elem . onclick = function ( ) { ref . command ( 'show-headers' , '' , elem ) ; } ;
} ;
// create folder selector popup
this . folder _selector = function ( event , callback )
{
this . entity _selector ( 'folder-selector' , callback , this . env . mailboxes _list , function ( obj , a ) {
var n = 0 , s = 0 ,
delim = ref . env . delimiter ,
folder = ref . env . mailboxes [ obj ] ,
id = folder . id ,
row = $ ( '<li>' ) ;
if ( folder . virtual )
a . addClass ( 'virtual' ) . attr ( { 'aria-disabled' : 'true' , tabindex : '-1' } ) ;
else
a . addClass ( 'active' ) . data ( 'id' , folder . id ) ;
if ( folder [ 'class' ] )
row . addClass ( folder [ 'class' ] ) ;
// calculate/set indentation level
while ( ( s = id . indexOf ( delim , s ) ) >= 0 ) {
n ++ ; s ++ ;
}
a . css ( 'padding-left' , n ? ( n * 16 ) + 'px' : 0 ) ;
// add folder name element
a . append ( $ ( '<span>' ) . text ( folder . name ) ) ;
return row . append ( a ) ;
} , event ) ;
} ;
// create addressbook selector popup
this . addressbook _selector = function ( event , callback )
{
// build addressbook + groups list
var combined _sources = [ ] ;
// check we really need it before processing
if ( ! this . entity _selectors [ 'addressbook-selector' ] ) {
$ . each ( this . env . address _sources , function ( ) {
if ( ! this . readonly ) {
var source = this ;
combined _sources . push ( source ) ;
$ . each ( ref . env . contactgroups , function ( ) {
if ( source . id === this . source ) {
combined _sources . push ( this ) ;
}
} ) ;
}
} ) ;
}
this . entity _selector ( 'addressbook-selector' , callback , combined _sources , function ( obj , a ) {
if ( obj . type == 'group' ) {
a . attr ( 'rel' , obj . source + ':' + obj . id )
. addClass ( 'contactgroup active' )
. data ( { source : obj . source , gid : obj . id , id : obj . source + ':' + obj . id } )
. css ( 'padding-left' , '16px' ) ;
}
else {
a . addClass ( 'addressbook active' ) . data ( 'id' , obj . id ) ;
}
a . append ( $ ( '<span>' ) . text ( obj . name ) ) ;
return $ ( '<li>' ) . append ( a ) ;
} , event ) ;
} ;
// create contactgroup selector popup
this . contactgroup _selector = function ( event , callback )
{
this . entity _selector ( 'contactgroup-selector' , callback , this . env . contactgroups , function ( obj , a ) {
if ( ref . env . source === obj . source ) {
a . addClass ( 'contactgroup active' )
. data ( { id : obj . id } )
. append ( $ ( '<span>' ) . text ( obj . name ) ) ;
return $ ( '<li>' ) . append ( a ) ;
}
} , event ) ;
} ;
// create selector popup (eg for folders or address books), position and display it
this . entity _selector = function ( name , click _callback , entity _list , list _callback , event )
{
var container = this . entity _selectors [ name ] ;
if ( ! container ) {
var rows = [ ] ,
container = $ ( '<div>' ) . attr ( 'id' , name ) . addClass ( 'popupmenu' ) ,
ul = $ ( '<ul>' ) . addClass ( 'toolbarmenu menu' ) ,
link = document . createElement ( 'a' ) ;
link . href = '#' ;
link . className = 'icon' ;
// loop over entity list
$ . each ( entity _list , function ( i ) {
var a = $ ( link . cloneNode ( false ) ) . attr ( 'rel' , this . id ) ;
rows . push ( list _callback ( this , a , i ) ) ;
} ) ;
ul . append ( rows ) . appendTo ( container ) ;
// temporarily show element to calculate its size
container . css ( { left : '-1000px' , top : '-1000px' } )
. appendTo ( document . body ) . show ( ) ;
// set max-height if the list is long
if ( rows . length > 10 )
container . css ( 'max-height' , $ ( 'li' , container ) [ 0 ] . offsetHeight * 10 + 9 ) ;
// register delegate event handler for folder item clicks
container . on ( 'click' , 'a.active' , function ( e ) {
container . data ( 'callback' ) ( $ ( this ) . data ( 'id' ) , this ) ;
} ) ;
this . entity _selectors [ name ] = container ;
}
container . data ( 'callback' , click _callback ) ;
// position menu on the screen
this . show _menu ( name , true , event ) ;
} ;
this . destroy _entity _selector = function ( name )
{
$ ( "#" + name ) . remove ( ) ;
delete this . entity _selectors [ name ] ;
} ;
/***********************************************/
/********* popup menu functions *********/
/***********************************************/
// Show/hide a specific popup menu
this . show _menu = function ( prop , show , event )
{
var name = typeof prop == 'object' ? prop . menu : prop ,
obj = $ ( '#' + name ) ,
ref = event && event . target ? $ ( event . target ) : $ ( obj . attr ( 'rel' ) || '#' + name + 'link' ) ,
keyboard = rcube _event . is _keyboard ( event ) ,
align = obj . attr ( 'data-align' ) || '' ,
stack = false ;
// find "real" button element
if ( ref . get ( 0 ) . tagName != 'A' && ref . closest ( 'a' ) . length )
ref = ref . closest ( 'a' ) ;
if ( typeof prop == 'string' )
prop = { menu : name } ;
// let plugins or skins provide the menu element
if ( ! obj . length ) {
obj = this . triggerEvent ( 'menu-get' , { name : name , props : prop , originalEvent : event } ) ;
}
if ( ! obj || ! obj . length ) {
// just delegate the action to subscribers
return this . triggerEvent ( show === false ? 'menu-close' : 'menu-open' , { name : name , props : prop , originalEvent : event } ) ;
}
// move element to top for proper absolute positioning
obj . appendTo ( document . body ) ;
if ( typeof show == 'undefined' )
show = obj . is ( ':visible' ) ? false : true ;
if ( show && ref . length ) {
var win = $ ( window ) ,
pos = ref . offset ( ) ,
above = align . indexOf ( 'bottom' ) >= 0 ;
stack = ref . attr ( 'role' ) == 'menuitem' || ref . closest ( '[role=menuitem]' ) . length > 0 ;
ref . offsetWidth = ref . outerWidth ( ) ;
ref . offsetHeight = ref . outerHeight ( ) ;
if ( ! above && pos . top + ref . offsetHeight + obj . height ( ) > win . height ( ) ) {
above = true ;
}
if ( align . indexOf ( 'right' ) >= 0 ) {
pos . left = pos . left + ref . outerWidth ( ) - obj . width ( ) ;
}
else if ( stack ) {
pos . left = pos . left + ref . offsetWidth - 5 ;
pos . top -= ref . offsetHeight ;
}
if ( pos . left + obj . width ( ) > win . width ( ) ) {
pos . left = win . width ( ) - obj . width ( ) - 12 ;
}
pos . top = Math . max ( 0 , pos . top + ( above ? - obj . height ( ) : ref . offsetHeight ) ) ;
obj . css ( { left : pos . left + 'px' , top : pos . top + 'px' } ) ;
}
// add menu to stack
if ( show ) {
// truncate stack down to the one containing the ref link
for ( var i = this . menu _stack . length - 1 ; stack && i >= 0 ; i -- ) {
if ( ! $ ( ref ) . parents ( '#' + this . menu _stack [ i ] ) . length && $ ( event . target ) . parent ( ) . attr ( 'role' ) != 'menuitem' )
this . hide _menu ( this . menu _stack [ i ] , event ) ;
}
if ( stack && this . menu _stack . length ) {
obj . data ( 'parent' , $ . last ( this . menu _stack ) ) ;
obj . css ( 'z-index' , ( $ ( '#' + $ . last ( this . menu _stack ) ) . css ( 'z-index' ) || 0 ) + 1 ) ;
}
else if ( ! stack && this . menu _stack . length ) {
this . hide _menu ( this . menu _stack [ 0 ] , event ) ;
}
obj . show ( ) . attr ( 'aria-hidden' , 'false' ) . data ( 'opener' , ref . attr ( 'aria-expanded' , 'true' ) . get ( 0 ) ) ;
this . triggerEvent ( 'menu-open' , { name : name , obj : obj , props : prop , originalEvent : event } ) ;
this . menu _stack . push ( name ) ;
this . menu _keyboard _active = show && keyboard ;
if ( this . menu _keyboard _active ) {
this . focused _menu = name ;
obj . find ( 'a,input:not(:disabled)' ) . not ( '[aria-disabled=true]' ) . first ( ) . focus ( ) ;
}
}
else { // close menu
this . hide _menu ( name , event ) ;
}
return show ;
} ;
// hide the given popup menu (and it's childs)
this . hide _menu = function ( name , event )
{
if ( ! this . menu _stack . length ) {
// delegate to subscribers
this . triggerEvent ( 'menu-close' , { name : name , props : { menu : name } , originalEvent : event } ) ;
return ;
}
var obj , keyboard = rcube _event . is _keyboard ( event ) ;
for ( var j = this . menu _stack . length - 1 ; j >= 0 ; j -- ) {
obj = $ ( '#' + this . menu _stack [ j ] ) . hide ( ) . attr ( 'aria-hidden' , 'true' ) . data ( 'parent' , false ) ;
this . triggerEvent ( 'menu-close' , { name : this . menu _stack [ j ] , obj : obj , props : { menu : this . menu _stack [ j ] } , originalEvent : event } ) ;
if ( this . menu _stack [ j ] == name ) {
j = - 1 ; // stop loop
if ( obj . data ( 'opener' ) ) {
$ ( obj . data ( 'opener' ) ) . attr ( 'aria-expanded' , 'false' ) ;
if ( keyboard )
obj . data ( 'opener' ) . focus ( ) ;
}
}
this . menu _stack . pop ( ) ;
}
// focus previous menu in stack
if ( this . menu _stack . length && keyboard ) {
this . menu _keyboard _active = true ;
this . focused _menu = $ . last ( this . menu _stack ) ;
if ( ! obj || ! obj . data ( 'opener' ) )
$ ( '#' + this . focused _menu ) . find ( 'a,input:not(:disabled)' ) . not ( '[aria-disabled=true]' ) . first ( ) . focus ( ) ;
}
else {
this . focused _menu = null ;
this . menu _keyboard _active = false ;
}
} ;
// position a menu element on the screen in relation to other object
this . element _position = function ( element , obj )
{
var obj = $ ( obj ) , win = $ ( window ) ,
width = obj . outerWidth ( ) ,
height = obj . outerHeight ( ) ,
menu _pos = obj . data ( 'menu-pos' ) ,
win _height = win . height ( ) ,
elem _height = $ ( element ) . height ( ) ,
elem _width = $ ( element ) . width ( ) ,
pos = obj . offset ( ) ,
top = pos . top ,
left = pos . left + width ;
if ( menu _pos == 'bottom' ) {
top += height ;
left -= width ;
}
else
left -= 5 ;
if ( top + elem _height > win _height ) {
top -= elem _height - height ;
if ( top < 0 )
top = Math . max ( 0 , ( win _height - elem _height ) / 2 ) ;
}
if ( left + elem _width > win . width ( ) )
left -= elem _width + width ;
element . css ( { left : left + 'px' , top : top + 'px' } ) ;
} ;
// initialize HTML editor
this . editor _init = function ( config , id )
{
this . editor = new rcube _text _editor ( config , id ) ;
} ;
/********************************************************/
/********* html to text conversion functions *********/
/********************************************************/
this . html2plain = function ( html , func )
{
return this . format _converter ( html , 'html' , func ) ;
} ;
this . plain2html = function ( plain , func )
{
return this . format _converter ( plain , 'plain' , func ) ;
} ;
this . format _converter = function ( text , format , func )
{
// warn the user (if converted content is not empty)
if ( ! text
|| ( format == 'html' && ! ( text . replace ( /<[^>]+>| |\xC2\xA0|\s/g , '' ) ) . length )
|| ( format != 'html' && ! ( text . replace ( /\xC2\xA0|\s/g , '' ) ) . length )
) {
// without setTimeout() here, textarea is filled with initial (onload) content
if ( func )
setTimeout ( function ( ) { func ( '' ) ; } , 50 ) ;
return true ;
}
var confirmed = this . env . editor _warned || confirm ( this . get _label ( 'editorwarning' ) ) ;
this . env . editor _warned = true ;
if ( ! confirmed )
return false ;
var url = '?_task=utils&_action=' + ( format == 'html' ? 'html2text' : 'text2html' ) ,
lock = this . set _busy ( true , 'converting' ) ;
$ . ajax ( { type : 'POST' , url : url , data : text , contentType : 'application/octet-stream' ,
error : function ( o , status , err ) { ref . http _error ( o , status , err , lock ) ; } ,
success : function ( data ) {
ref . set _busy ( false , null , lock ) ;
if ( func ) func ( data ) ;
}
} ) ;
return true ;
} ;
/********************************************************/
/********* remote request methods *********/
/********************************************************/
// compose a valid url with the given parameters
this . url = function ( action , query )
{
var querystring = typeof query === 'string' ? query : '' ;
if ( typeof action !== 'string' )
query = action ;
else if ( ! query || typeof query !== 'object' )
query = { } ;
if ( action )
query . _action = action ;
else if ( this . env . action )
query . _action = this . env . action ;
var url = this . env . comm _path , k , param = { } ;
// overwrite task name
if ( action && action . match ( /([a-z0-9_-]+)\/([a-z0-9-_.]+)/ ) ) {
query . _action = RegExp . $2 ;
url = url . replace ( /\_task=[a-z0-9_-]+/ , '_task=' + RegExp . $1 ) ;
}
// force _framed=0
if ( query . _framed === 0 ) {
url = url . replace ( '&_framed=1' , '' ) ;
query . _framed = null ;
}
// remove undefined values
for ( k in query ) {
if ( query [ k ] !== undefined && query [ k ] !== null )
param [ k ] = query [ k ] ;
}
if ( param = $ . param ( param ) )
url += ( url . indexOf ( '?' ) > - 1 ? '&' : '?' ) + param ;
if ( querystring )
url += ( url . indexOf ( '?' ) > - 1 ? '&' : '?' ) + querystring ;
return url ;
} ;
this . redirect = function ( url , lock )
{
if ( lock !== false )
this . set _busy ( true , 'loading' ) ;
if ( this . is _framed ( ) ) {
url = url . replace ( /&_framed=1/ , '' ) ;
parent . rcmail . redirect ( url , lock ) ;
}
else {
if ( this . env . extwin ) {
if ( typeof url == 'string' )
url += ( url . indexOf ( '?' ) < 0 ? '?' : '&' ) + '_extwin=1' ;
else
url . _extwin = 1 ;
}
this . location _href ( url , window ) ;
}
} ;
this . goto _url = function ( action , query , lock , secure )
{
var url = this . url ( action , query )
if ( secure ) url = this . secure _url ( url ) ;
this . redirect ( url , lock ) ;
} ;
this . location _href = function ( url , target , frame )
{
if ( frame )
this . lock _frame ( target ) ;
if ( typeof url == 'object' )
url = this . env . comm _path + '&' + $ . param ( url ) ;
// simulate real link click to force IE to send referer header
if ( bw . ie && target == window )
$ ( '<a>' ) . attr ( 'href' , url ) . appendTo ( document . body ) . get ( 0 ) . click ( ) ;
else
target . location . href = url ;
// reset keep-alive interval
this . start _keepalive ( ) ;
} ;
// update browser location to remember current view
this . update _state = function ( query )
{
if ( window . history . replaceState )
try {
// This may throw security exception in Firefox (#5400)
window . history . replaceState ( { } , document . title , rcmail . url ( '' , query ) ) ;
}
catch ( e ) { /* ignore */ } ;
} ;
// send a http request to the server
this . http _request = function ( action , data , lock , type )
{
if ( type != 'POST' )
type = 'GET' ;
if ( typeof data !== 'object' )
data = rcube _parse _query ( data ) ;
data . _remote = 1 ;
data . _unlock = lock ? lock : 0 ;
// trigger plugin hook
var result = this . triggerEvent ( 'request' + action , data ) ;
// abort if one of the handlers returned false
if ( result === false ) {
if ( data . _unlock )
this . set _busy ( false , null , data . _unlock ) ;
return false ;
}
else if ( result && result . getResponseHeader ) {
return result ;
}
else if ( result !== undefined ) {
data = result ;
if ( data . _action ) {
action = data . _action ;
delete data . _action ;
}
}
var url = this . url ( action ) ;
// reset keep-alive interval
this . start _keepalive ( ) ;
// send request
return $ . ajax ( {
type : type , url : url , data : data , dataType : 'json' ,
success : function ( data ) { ref . http _response ( data ) ; } ,
error : function ( o , status , err ) { ref . http _error ( o , status , err , lock , action ) ; }
} ) ;
} ;
// send a http GET request to the server
this . http _get = this . http _request ;
// send a http POST request to the server
this . http _post = function ( action , data , lock )
{
return this . http _request ( action , data , lock , 'POST' ) ;
} ;
// aborts ajax request
this . abort _request = function ( r )
{
if ( r . request )
r . request . abort ( ) ;
if ( r . lock )
this . set _busy ( false , null , r . lock ) ;
} ;
// handle HTTP response
this . http _response = function ( response )
{
if ( ! response )
return ;
if ( response . unlock )
this . set _busy ( false , null , response . unlock ) ;
this . triggerEvent ( 'responsebefore' , { response : response } ) ;
this . triggerEvent ( 'responsebefore' + response . action , { response : response } ) ;
// set env vars
if ( response . env )
this . set _env ( response . env ) ;
var i ;
// we have labels to add
if ( typeof response . texts === 'object' ) {
for ( i in response . texts )
if ( typeof response . texts [ i ] === 'string' )
this . add _label ( i , response . texts [ i ] ) ;
}
// if we get javascript code from server -> execute it
if ( response . exec ) {
eval ( response . exec ) ;
}
// execute callback functions of plugins
if ( response . callbacks && response . callbacks . length ) {
for ( i = 0 ; i < response . callbacks . length ; i ++ )
this . triggerEvent ( response . callbacks [ i ] [ 0 ] , response . callbacks [ i ] [ 1 ] ) ;
}
// process the response data according to the sent action
switch ( response . action ) {
case 'mark' :
// Mark the message as Seen also in the opener/parent
if ( ( this . env . action == 'show' || this . env . action == 'preview' ) && this . env . last _flag == 'SEEN' )
this . set _unread _message ( this . env . uid , this . env . mailbox ) ;
break ;
case 'delete' :
if ( this . task == 'addressbook' ) {
var sid , uid = this . contact _list . get _selection ( ) , writable = false ;
if ( uid && this . contact _list . rows [ uid ] ) {
// search results, get source ID from record ID
if ( this . env . source == '' ) {
sid = String ( uid ) . replace ( /^[^-]+-/ , '' ) ;
writable = sid && this . env . address _sources [ sid ] && ! this . env . address _sources [ sid ] . readonly ;
}
else {
writable = ! this . env . address _sources [ this . env . source ] . readonly ;
}
}
this . enable _command ( 'delete' , 'edit' , writable ) ;
this . enable _command ( 'export' , ( this . contact _list && this . contact _list . rowcount > 0 ) ) ;
this . enable _command ( 'export-selected' , 'print' , false ) ;
}
case 'move' :
if ( this . env . action == 'show' ) {
// re-enable commands on move/delete error
this . enable _command ( this . env . message _commands , true ) ;
if ( ! this . env . list _post )
this . enable _command ( 'reply-list' , false ) ;
}
else if ( this . task == 'addressbook' ) {
this . triggerEvent ( 'listupdate' , { list : this . contact _list , folder : this . env . source , rowcount : this . contact _list . rowcount } ) ;
}
case 'purge' :
case 'expunge' :
if ( this . task == 'mail' ) {
if ( ! this . env . exists ) {
// clear preview pane content
if ( this . env . contentframe )
this . show _contentframe ( false ) ;
// disable commands useless when mailbox is empty
this . enable _command ( this . env . message _commands , 'purge' , 'expunge' ,
'select-all' , 'select-none' , 'expand-all' , 'expand-unread' , 'collapse-all' , false ) ;
}
if ( this . message _list )
this . triggerEvent ( 'listupdate' , { list : this . message _list , folder : this . env . mailbox , rowcount : this . message _list . rowcount } ) ;
}
break ;
case 'refresh' :
case 'check-recent' :
// update message flags
$ . each ( this . env . recent _flags || { } , function ( uid , flags ) {
ref . set _message ( uid , 'deleted' , flags . deleted ) ;
ref . set _message ( uid , 'replied' , flags . answered ) ;
ref . set _message ( uid , 'unread' , ! flags . seen ) ;
ref . set _message ( uid , 'forwarded' , flags . forwarded ) ;
ref . set _message ( uid , 'flagged' , flags . flagged ) ;
} ) ;
delete this . env . recent _flags ;
case 'getunread' :
case 'search' :
this . env . qsearch = null ;
case 'list' :
if ( this . task == 'mail' ) {
var is _multifolder = this . is _multifolder _listing ( ) ,
list = this . message _list ,
uid = this . env . list _uid ;
this . enable _command ( 'show' , 'select-all' , 'select-none' , this . env . messagecount > 0 ) ;
this . enable _command ( 'expunge' , this . env . exists && ! is _multifolder ) ;
this . enable _command ( 'purge' , this . purge _mailbox _test ( ) && ! is _multifolder ) ;
this . enable _command ( 'import-messages' , ! is _multifolder ) ;
this . enable _command ( 'expand-all' , 'expand-unread' , 'collapse-all' , this . env . threading && this . env . messagecount && ! is _multifolder ) ;
if ( list ) {
if ( response . action == 'list' || response . action == 'search' ) {
// highlight message row when we're back from message page
if ( uid ) {
if ( uid === 'FIRST' ) {
uid = list . get _first _row ( ) ;
}
else if ( uid === 'LAST' ) {
uid = list . get _last _row ( ) ;
}
else if ( ! list . rows [ uid ] ) {
uid += '-' + this . env . mailbox ;
}
if ( uid && list . rows [ uid ] ) {
list . select ( uid ) ;
}
delete this . env . list _uid ;
}
this . enable _command ( 'set-listmode' , this . env . threads && ! is _multifolder ) ;
if ( list . rowcount > 0 && ! $ ( document . activeElement ) . is ( 'input,textarea' ) )
list . focus ( ) ;
// trigger 'select' so all dependent actions update its state
// e.g. plugins use this event to activate buttons (#1490647)
list . triggerEvent ( 'select' ) ;
}
if ( response . action != 'getunread' )
this . triggerEvent ( 'listupdate' , { list : list , folder : this . env . mailbox , rowcount : list . rowcount } ) ;
}
}
else if ( this . task == 'addressbook' ) {
var list = this . contact _list ,
uid = this . env . list _uid ;
this . enable _command ( 'export' , 'select-all' , 'select-none' , ( list && list . rowcount > 0 ) ) ;
if ( response . action == 'list' || response . action == 'search' ) {
this . enable _command ( 'search-create' , this . env . source == '' ) ;
this . enable _command ( 'search-delete' , this . env . search _id ) ;
this . update _group _commands ( ) ;
if ( list && uid ) {
if ( uid === 'FIRST' ) {
uid = list . get _first _row ( ) ;
}
else if ( uid === 'LAST' ) {
uid = list . get _last _row ( ) ;
}
if ( uid && list . rows [ uid ] ) {
list . select ( uid ) ;
}
delete this . env . list _uid ;
// trigger 'select' so all dependent actions update its state
list . triggerEvent ( 'select' ) ;
}
if ( list . rowcount > 0 && ! $ ( document . activeElement ) . is ( 'input,textarea' ) )
list . focus ( ) ;
this . triggerEvent ( 'listupdate' , { list : list , folder : this . env . source , rowcount : list . rowcount } ) ;
}
}
break ;
case 'list-contacts' :
case 'search-contacts' :
if ( this . contact _list ) {
if ( this . contact _list . rowcount > 0 )
this . contact _list . focus ( ) ;
this . triggerEvent ( 'listupdate' , { list : this . contact _list , rowcount : this . contact _list . rowcount } ) ;
}
break ;
}
if ( response . unlock )
this . hide _message ( response . unlock ) ;
this . triggerEvent ( 'responseafter' , { response : response } ) ;
this . triggerEvent ( 'responseafter' + response . action , { response : response } ) ;
// reset keep-alive interval
this . start _keepalive ( ) ;
} ;
// handle HTTP request errors
this . http _error = function ( request , status , err , lock , action )
{
var errmsg = request . statusText ;
this . set _busy ( false , null , lock ) ;
request . abort ( ) ;
// don't display error message on page unload (#1488547)
if ( this . unload )
return ;
if ( request . status && errmsg )
this . display _message ( this . get _label ( 'servererror' ) + ' (' + errmsg + ')' , 'error' ) ;
else if ( status == 'timeout' )
this . display _message ( 'requesttimedout' , 'error' ) ;
else if ( request . status == 0 && status != 'abort' )
this . display _message ( 'connerror' , 'error' ) ;
// redirect to url specified in location header if not empty
var location _url = request . getResponseHeader ( "Location" ) ;
if ( location _url && this . env . action != 'compose' ) // don't redirect on compose screen, contents might get lost (#1488926)
this . redirect ( location _url ) ;
// 403 Forbidden response (CSRF prevention) - reload the page.
// In case there's a new valid session it will be used, otherwise
// login form will be presented (#1488960).
if ( request . status == 403 ) {
( this . is _framed ( ) ? parent : window ) . location . reload ( ) ;
return ;
}
// re-send keep-alive requests after 30 seconds
if ( action == 'keep-alive' )
setTimeout ( function ( ) { ref . keep _alive ( ) ; ref . start _keepalive ( ) ; } , 30000 ) ;
} ;
// handler for session errors detected on the server
this . session _error = function ( redirect _url )
{
this . env . server _error = 401 ;
// save message in local storage and do not redirect
if ( this . env . action == 'compose' ) {
this . save _compose _form _local ( ) ;
this . compose _skip _unsavedcheck = true ;
// stop keep-alive and refresh processes
this . env . session _lifetime = 0 ;
if ( this . _keepalive )
clearInterval ( this . _keepalive ) ;
if ( this . _refresh )
clearInterval ( this . _refresh ) ;
}
else if ( redirect _url ) {
setTimeout ( function ( ) { ref . redirect ( redirect _url , true ) ; } , 2000 ) ;
}
} ;
// callback when an iframe finished loading
this . iframe _loaded = function ( unlock )
{
if ( ! unlock )
unlock = this . env . frame _lock ;
this . set _busy ( false , null , unlock ) ;
if ( this . submit _timer )
clearTimeout ( this . submit _timer ) ;
} ;
/ * *
Send multi - threaded parallel HTTP requests to the server for a list if items .
The string '%' in either a GET query or POST parameters will be replaced with the respective item value .
This is the argument object expected : {
items : [ 'foo' , 'bar' , 'gna' ] , // list of items to send requests for
action : 'task/some-action' , // Roudncube action to call
query : { q : '%s' } , // GET query parameters
postdata : { source : '%s' } , // POST data (sends a POST request if present)
threads : 3 , // max. number of concurrent requests
onresponse : function ( data ) { } , // Callback function called for every response received from server
whendone : function ( alldata ) { } // Callback function called when all requests have been sent
}
* /
this . multi _thread _http _request = function ( prop )
{
var i , item , reqid = new Date ( ) . getTime ( ) ,
threads = prop . threads || 1 ;
prop . reqid = reqid ;
prop . running = 0 ;
prop . requests = [ ] ;
prop . result = [ ] ;
prop . _items = $ . extend ( [ ] , prop . items ) ; // copy items
if ( ! prop . lock )
prop . lock = this . display _message ( '' , 'loading' ) ;
// add the request arguments to the jobs pool
this . http _request _jobs [ reqid ] = prop ;
// start n threads
for ( i = 0 ; i < threads ; i ++ ) {
item = prop . _items . shift ( ) ;
if ( item === undefined )
break ;
prop . running ++ ;
prop . requests . push ( this . multi _thread _send _request ( prop , item ) ) ;
}
return reqid ;
} ;
// helper method to send an HTTP request with the given iterator value
this . multi _thread _send _request = function ( prop , item )
{
var k , postdata , query ;
// replace %s in post data
if ( prop . postdata ) {
postdata = { } ;
for ( k in prop . postdata ) {
postdata [ k ] = String ( prop . postdata [ k ] ) . replace ( '%s' , item ) ;
}
postdata . _reqid = prop . reqid ;
}
// replace %s in query
else if ( typeof prop . query == 'string' ) {
query = prop . query . replace ( '%s' , item ) ;
query += '&_reqid=' + prop . reqid ;
}
else if ( typeof prop . query == 'object' && prop . query ) {
query = { } ;
for ( k in prop . query ) {
query [ k ] = String ( prop . query [ k ] ) . replace ( '%s' , item ) ;
}
query . _reqid = prop . reqid ;
}
// send HTTP GET or POST request
return postdata ? this . http _post ( prop . action , postdata ) : this . http _request ( prop . action , query ) ;
} ;
// callback function for multi-threaded http responses
this . multi _thread _http _response = function ( data , reqid )
{
var prop = this . http _request _jobs [ reqid ] ;
if ( ! prop || prop . running <= 0 || prop . cancelled )
return ;
prop . running -- ;
// trigger response callback
if ( prop . onresponse && typeof prop . onresponse == 'function' ) {
prop . onresponse ( data ) ;
}
prop . result = $ . extend ( prop . result , data ) ;
// send next request if prop.items is not yet empty
var item = prop . _items . shift ( ) ;
if ( item !== undefined ) {
prop . running ++ ;
prop . requests . push ( this . multi _thread _send _request ( prop , item ) ) ;
}
// trigger whendone callback and mark this request as done
else if ( prop . running == 0 ) {
if ( prop . whendone && typeof prop . whendone == 'function' ) {
prop . whendone ( prop . result ) ;
}
this . set _busy ( false , '' , prop . lock ) ;
// remove from this.http_request_jobs pool
delete this . http _request _jobs [ reqid ] ;
}
} ;
// abort a running multi-thread request with the given identifier
this . multi _thread _request _abort = function ( reqid )
{
var prop = this . http _request _jobs [ reqid ] ;
if ( prop ) {
for ( var i = 0 ; prop . running > 0 && i < prop . requests . length ; i ++ ) {
if ( prop . requests [ i ] . abort )
prop . requests [ i ] . abort ( ) ;
}
prop . running = 0 ;
prop . cancelled = true ;
this . set _busy ( false , '' , prop . lock ) ;
}
} ;
// post the given form to a hidden iframe
this . async _upload _form = function ( form , action , onload )
{
// create hidden iframe
var ts = new Date ( ) . getTime ( ) ,
frame _name = 'rcmupload' + ts ,
frame = this . dummy _iframe ( frame _name ) ;
// handle upload errors by parsing iframe content in onload
frame . on ( 'load' , { ts : ts } , onload ) ;
$ ( form ) . attr ( {
target : frame _name ,
action : this . url ( action , { _id : this . env . compose _id || '' , _uploadid : ts , _from : this . env . action } ) ,
method : 'POST' ,
enctype : 'multipart/form-data'
} )
. submit ( ) ;
return frame _name ;
} ;
// create hidden iframe element
this . dummy _iframe = function ( name , src )
{
return $ ( '<iframe>' ) . attr ( {
name : name ,
src : src ,
style : 'width:0;height:0;visibility:hidden' ,
'aria-hidden' : 'true'
} )
. appendTo ( document . body ) ;
} ;
// html5 file-drop API
this . document _drag _hover = function ( e , over )
{
// don't e.preventDefault() here to not block text dragging on the page (#1490619)
$ ( this . gui _objects . filedrop ) [ ( over ? 'addClass' : 'removeClass' ) ] ( 'active' ) ;
} ;
this . file _drag _hover = function ( e , over )
{
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
$ ( this . gui _objects . filedrop ) [ ( over ? 'addClass' : 'removeClass' ) ] ( 'hover' ) ;
} ;
// handler when files are dropped to a designated area.
// compose a multipart form data and submit it to the server
this . file _dropped = function ( e )
{
// abort event and reset UI
this . file _drag _hover ( e , false ) ;
// prepare multipart form data composition
var uri ,
files = e . target . files || e . dataTransfer . files ,
args = { _id : this . env . compose _id || this . env . cid || '' , _remote : 1 , _from : this . env . action } ;
if ( ! files || ! files . length ) {
// Roundcube attachment, pass its uri to the backend and attach
if ( uri = e . dataTransfer . getData ( 'roundcube-uri' ) ) {
var ts = 'upload' + new Date ( ) . getTime ( ) ,
// jQuery way to escape filename (#1490530)
content = $ ( '<span>' ) . text ( e . dataTransfer . getData ( 'roundcube-name' ) || this . get _label ( 'attaching' ) ) . html ( ) ;
args . _uri = uri ;
args . _uploadid = ts ;
// add to attachments list
if ( ! this . add2attachment _list ( ts , { name : '' , html : content , classname : 'uploading' , complete : false } ) )
this . file _upload _id = this . set _busy ( true , 'attaching' ) ;
this . http _post ( this . env . filedrop . action || 'upload' , args ) ;
}
return ;
}
this . file _upload ( files , args , {
name : ( this . env . filedrop . fieldname || '_file' ) + ( this . env . filedrop . single ? '' : '[]' ) ,
single : this . env . filedrop . single ,
filter : this . env . filedrop . filter ,
action : ref . env . filedrop . action
} ) ;
} ;
// Files upload using ajax
this . file _upload = function ( files , post _args , props )
{
if ( ! window . FormData || ! files || ! files . length )
return false ;
var f , i , fname , size = 0 , numfiles = 0 ,
formdata = new FormData ( ) ,
fieldname = props . name || '_file[]' ,
limit = props . single ? 1 : files . length ;
args = $ . extend ( { _remote : 1 , _from : this . env . action } , post _args || { } ) ;
// add files to form data
for ( i = 0 ; numfiles < limit && ( f = files [ i ] ) ; i ++ ) {
// filter by file type if requested
if ( props . filter && ! f . type . match ( new RegExp ( props . filter ) ) ) {
// TODO: show message to user
continue ;
}
formdata . append ( fieldname , f ) ;
size += f . size ;
fname = f . name ;
numfiles ++ ;
}
if ( numfiles ) {
if ( this . env . max _filesize && this . env . filesizeerror && size > this . env . max _filesize ) {
this . display _message ( this . env . filesizeerror , 'error' ) ;
return false ;
}
if ( this . env . max _filecount && this . env . filecounterror && numfiles > this . env . max _filecount ) {
this . display _message ( this . env . filecounterror , 'error' ) ;
return false ;
}
var ts = 'upload' + new Date ( ) . getTime ( ) ,
label = numfiles > 1 ? this . get _label ( 'uploadingmany' ) : fname ,
// jQuery way to escape filename (#1490530)
content = $ ( '<span>' ) . text ( label ) . html ( ) ;
// add to attachments list
if ( ! this . add2attachment _list ( ts , { name : '' , html : content , classname : 'uploading' , complete : false } ) && ! props . lock )
props . lock = this . file _upload _id = this . set _busy ( true , 'uploading' ) ;
args . _uploadid = ts ;
args . _unlock = props . lock ;
this . uploads [ ts ] = $ . ajax ( {
type : 'POST' ,
dataType : 'json' ,
url : this . url ( props . action || 'upload' , args ) ,
contentType : false ,
processData : false ,
timeout : 0 , // disable default timeout set in ajaxSetup()
data : formdata ,
headers : { 'X-Roundcube-Request' : this . env . request _token } ,
xhr : function ( ) {
var xhr = $ . ajaxSettings . xhr ( ) ;
if ( xhr . upload && ref . labels . uploadprogress ) {
xhr . upload . onprogress = function ( e ) {
var msg = ref . file _upload _msg ( e . loaded , e . total ) ;
if ( msg ) {
$ ( '#' + ts ) . find ( '.uploading' ) . text ( msg ) ;
}
} ;
}
return xhr ;
} ,
success : function ( data ) {
delete ref . uploads [ ts ] ;
ref . http _response ( data ) ;
} ,
error : function ( o , status , err ) {
delete ref . uploads [ ts ] ;
ref . remove _from _attachment _list ( ts ) ;
ref . http _error ( o , status , err , props . lock , 'attachment' ) ;
}
} ) ;
}
return true ;
} ;
this . file _upload _msg = function ( current , total )
{
if ( total && current < total ) {
var percent = Math . round ( current / total * 100 ) ,
label = ref . get _label ( 'uploadprogress' ) ;
if ( total >= 1073741824 ) {
total = parseFloat ( total / 1073741824 ) . toFixed ( 1 ) + ' ' . this . get _label ( 'GB' ) ;
current = parseFloat ( current / 1073741824 ) . toFixed ( 1 ) ;
}
else if ( total >= 1048576 ) {
total = parseFloat ( total / 1048576 ) . toFixed ( 1 ) + ' ' + this . get _label ( 'MB' ) ;
current = parseFloat ( current / 1048576 ) . toFixed ( 1 ) ;
}
else if ( total >= 1024 ) {
total = parseInt ( total / 1024 ) + ' ' + this . get _label ( 'KB' ) ;
current = parseInt ( current / 1024 ) ;
}
else {
total = total + ' ' + this . get _label ( 'B' ) ;
}
return label . replace ( '$percent' , percent + '%' ) . replace ( '$current' , current ) . replace ( '$total' , total ) ;
}
} ;
// starts interval for keep-alive signal
this . start _keepalive = function ( )
{
if ( ! this . env . session _lifetime || this . env . framed || this . env . extwin || this . task == 'login' || this . env . action == 'print' )
return ;
if ( this . _keepalive )
clearInterval ( this . _keepalive ) ;
// use Math to prevent from an integer overflow (#5273)
// maximum interval is 15 minutes, minimum is 30 seconds
var interval = Math . min ( 1800 , this . env . session _lifetime ) * 0.5 * 1000 ;
this . _keepalive = setInterval ( function ( ) { ref . keep _alive ( ) ; } , interval < 30000 ? 30000 : interval ) ;
} ;
// starts interval for refresh signal
this . start _refresh = function ( )
{
if ( ! this . env . refresh _interval || this . env . framed || this . env . extwin || this . task == 'login' || this . env . action == 'print' )
return ;
if ( this . _refresh )
clearInterval ( this . _refresh ) ;
this . _refresh = setInterval ( function ( ) { ref . refresh ( ) ; } , this . env . refresh _interval * 1000 ) ;
} ;
// sends keep-alive signal
this . keep _alive = function ( )
{
if ( ! this . busy )
this . http _request ( 'keep-alive' ) ;
} ;
// sends refresh signal
this . refresh = function ( )
{
if ( this . busy ) {
// try again after 10 seconds
setTimeout ( function ( ) { ref . refresh ( ) ; ref . start _refresh ( ) ; } , 10000 ) ;
return ;
}
var params = { } , lock = this . set _busy ( true , 'refreshing' ) ;
if ( this . task == 'mail' && this . gui _objects . mailboxlist )
params = this . check _recent _params ( ) ;
params . _last = Math . floor ( this . env . lastrefresh . getTime ( ) / 1000 ) ;
this . env . lastrefresh = new Date ( ) ;
// plugins should bind to 'requestrefresh' event to add own params
this . http _post ( 'refresh' , params , lock ) ;
} ;
// returns check-recent request parameters
this . check _recent _params = function ( )
{
var params = { _mbox : this . env . mailbox } ;
if ( this . gui _objects . mailboxlist )
params . _folderlist = 1 ;
if ( this . gui _objects . quotadisplay )
params . _quota = 1 ;
if ( this . env . search _request )
params . _search = this . env . search _request ;
if ( this . gui _objects . messagelist ) {
params . _list = 1 ;
// message uids for flag updates check
params . _uids = $ . map ( this . message _list . rows , function ( row , uid ) { return uid ; } ) . join ( ',' ) ;
}
return params ;
} ;
/********************************************************/
/********* helper methods *********/
/********************************************************/
/ * *
* Quote html entities
* /
this . quote _html = function ( str )
{
return String ( str ) . replace ( /</g , '<' ) . replace ( />/g , '>' ) . replace ( /"/g , '"' ) ;
} ;
// get window.opener.rcmail if available
this . opener = function ( deep , filter )
{
var i , win = window . opener ;
// catch Error: Permission denied to access property rcmail
try {
if ( win && ! win . closed && win !== window ) {
// try parent of the opener window, e.g. preview frame
if ( deep && ( ! win . rcmail || win . rcmail . env . framed ) && win . parent && win . parent . rcmail )
win = win . parent ;
if ( win . rcmail && filter )
for ( i in filter )
if ( win . rcmail . env [ i ] != filter [ i ] )
return ;
return win . rcmail ;
}
}
catch ( e ) { }
} ;
// check if we're in show mode or if we have a unique selection
// and return the message uid
this . get _single _uid = function ( )
{
var uid = this . env . uid || ( this . message _list ? this . message _list . get _single _selection ( ) : null ) ;
var result = ref . triggerEvent ( 'get_single_uid' , { uid : uid } ) ;
return result || uid ;
} ;
// same as above but for contacts
this . get _single _cid = function ( )
{
var cid = this . env . cid || ( this . contact _list ? this . contact _list . get _single _selection ( ) : null ) ;
var result = ref . triggerEvent ( 'get_single_cid' , { cid : cid } ) ;
return result || cid ;
} ;
// get the IMP mailbox of the message with the given UID
this . get _message _mailbox = function ( uid )
{
var msg ;
if ( this . env . messages && uid && ( msg = this . env . messages [ uid ] ) && msg . mbox )
return msg . mbox ;
if ( /^[0-9]+-(.*)$/ . test ( uid ) )
return RegExp . $1 ;
return this . env . mailbox ;
} ;
// build request parameters from single message id (maybe with mailbox name)
this . params _from _uid = function ( uid , params )
{
if ( ! params )
params = { } ;
params . _uid = String ( uid ) . split ( '-' ) [ 0 ] ;
params . _mbox = this . get _message _mailbox ( uid ) ;
return params ;
} ;
// gets cursor position
this . get _caret _pos = function ( obj )
{
if ( obj . selectionEnd !== undefined )
return obj . selectionEnd ;
return obj . value . length ;
} ;
// moves cursor to specified position
this . set _caret _pos = function ( obj , pos )
{
try {
if ( obj . setSelectionRange )
obj . setSelectionRange ( pos , pos ) ;
}
catch ( e ) { } // catch Firefox exception if obj is hidden
} ;
// get selected text from an input field
this . get _input _selection = function ( obj )
{
var start = 0 , end = 0 , normalizedValue = '' ;
if ( typeof obj . selectionStart == "number" && typeof obj . selectionEnd == "number" ) {
normalizedValue = obj . value ;
start = obj . selectionStart ;
end = obj . selectionEnd ;
}
return { start : start , end : end , text : normalizedValue . substr ( start , end - start ) } ;
} ;
// disable/enable all fields of a form
this . lock _form = function ( form , lock )
{
if ( ! form || ! form . elements )
return ;
if ( lock )
this . disabled _form _elements = [ ] ;
$ . each ( form . elements , function ( ) {
if ( this . type == 'hidden' )
return ;
// remember which elem was disabled before lock
if ( lock && this . disabled )
ref . disabled _form _elements . push ( this ) ;
else if ( lock || $ . inArray ( this , ref . disabled _form _elements ) < 0 )
this . disabled = lock ;
} ) ;
} ;
this . mailto _handler _uri = function ( )
{
return location . href . split ( '?' ) [ 0 ] + '?_task=mail&_action=compose&_to=%s' ;
} ;
this . register _protocol _handler = function ( name )
{
try {
window . navigator . registerProtocolHandler ( 'mailto' , this . mailto _handler _uri ( ) , name ) ;
}
catch ( e ) {
this . display _message ( String ( e ) , 'error' ) ;
}
} ;
this . check _protocol _handler = function ( name , elem )
{
var nav = window . navigator ;
if ( ! nav || ( typeof nav . registerProtocolHandler != 'function' ) ) {
$ ( elem ) . addClass ( 'disabled' ) . click ( function ( ) {
ref . display _message ( 'nosupporterror' , 'error' ) ;
return false ;
} ) ;
}
else if ( typeof nav . isProtocolHandlerRegistered == 'function' ) {
var status = nav . isProtocolHandlerRegistered ( 'mailto' , this . mailto _handler _uri ( ) ) ;
if ( status )
$ ( elem ) . parent ( ) . find ( '.mailtoprotohandler-status' ) . html ( status ) ;
}
else {
$ ( elem ) . click ( function ( ) { ref . register _protocol _handler ( name ) ; return false ; } ) ;
}
} ;
// Checks browser capabilities eg. PDF support, TIF support
this . browser _capabilities _check = function ( )
{
if ( ! this . env . browser _capabilities )
this . env . browser _capabilities = { } ;
$ . each ( [ 'pdf' , 'flash' , 'tiff' , 'webp' ] , function ( ) {
if ( ref . env . browser _capabilities [ this ] === undefined )
ref . env . browser _capabilities [ this ] = ref [ this + '_support_check' ] ( ) ;
} ) ;
} ;
// Returns browser capabilities string
this . browser _capabilities = function ( )
{
if ( ! this . env . browser _capabilities )
return '' ;
var n , ret = [ ] ;
for ( n in this . env . browser _capabilities )
ret . push ( n + '=' + this . env . browser _capabilities [ n ] ) ;
return ret . join ( ) ;
} ;
this . tiff _support _check = function ( )
{
this . image _support _check ( 'tiff' ) ;
return 0 ;
} ;
this . webp _support _check = function ( )
{
this . image _support _check ( 'webp' ) ;
return 0 ;
} ;
this . image _support _check = function ( type )
{
window . setTimeout ( function ( ) {
var img = new Image ( ) ;
img . onload = function ( ) { ref . env . browser _capabilities [ type ] = 1 ; } ;
img . onerror = function ( ) { ref . env . browser _capabilities [ type ] = 0 ; } ;
img . src = ref . assets _path ( 'program/resources/blank.' + type ) ;
} , 10 ) ;
} ;
this . pdf _support _check = function ( )
{
var i , plugin = navigator . mimeTypes ? navigator . mimeTypes [ "application/pdf" ] : { } ,
plugins = navigator . plugins ,
len = plugins . length ,
regex = /Adobe Reader|PDF|Acrobat/i ;
if ( plugin && plugin . enabledPlugin )
return 1 ;
if ( 'ActiveXObject' in window ) {
try {
if ( plugin = new ActiveXObject ( "AcroPDF.PDF" ) )
return 1 ;
}
catch ( e ) { }
try {
if ( plugin = new ActiveXObject ( "PDF.PdfCtrl" ) )
return 1 ;
}
catch ( e ) { }
}
for ( i = 0 ; i < len ; i ++ ) {
plugin = plugins [ i ] ;
if ( typeof plugin === 'string' ) {
if ( regex . test ( plugin ) )
return 1 ;
}
else if ( plugin . name && regex . test ( plugin . name ) )
return 1 ;
}
window . setTimeout ( function ( ) {
$ ( '<object>' ) . attr ( {
data : ref . assets _path ( 'program/resources/dummy.pdf' ) ,
type : 'application/pdf' ,
style : 'position: "absolute"; top: -1000px; height: 1px; width: 1px'
} )
. on ( 'load error' , function ( e ) {
ref . env . browser _capabilities . pdf = e . type == 'load' ? 1 : 0 ;
$ ( this ) . remove ( ) ;
} )
. appendTo ( document . body ) ;
} , 10 ) ;
return 0 ;
} ;
this . flash _support _check = function ( )
{
var plugin = navigator . mimeTypes ? navigator . mimeTypes [ "application/x-shockwave-flash" ] : { } ;
if ( plugin && plugin . enabledPlugin )
return 1 ;
if ( 'ActiveXObject' in window ) {
try {
if ( plugin = new ActiveXObject ( "ShockwaveFlash.ShockwaveFlash" ) )
return 1 ;
}
catch ( e ) { }
}
return 0 ;
} ;
this . assets _path = function ( path )
{
if ( this . env . assets _path && ! path . startsWith ( this . env . assets _path ) ) {
path = this . env . assets _path + path ;
}
return path ;
} ;
// Cookie setter
this . set _cookie = function ( name , value , expires )
{
setCookie ( name , value , expires , this . env . cookie _path , this . env . cookie _domain , this . env . cookie _secure ) ;
} ;
this . get _local _storage _prefix = function ( )
{
if ( ! this . local _storage _prefix )
this . local _storage _prefix = 'roundcube.' + ( this . env . user _id || 'anonymous' ) + '.' ;
return this . local _storage _prefix ;
} ;
// wrapper for localStorage.getItem(key)
this . local _storage _get _item = function ( key , deflt , encrypted )
{
var item , result ;
// TODO: add encryption
try {
item = localStorage . getItem ( this . get _local _storage _prefix ( ) + key ) ;
result = JSON . parse ( item ) ;
}
catch ( e ) { }
return result || deflt || null ;
} ;
// wrapper for localStorage.setItem(key, data)
this . local _storage _set _item = function ( key , data , encrypted )
{
// try/catch to handle no localStorage support, but also error
// in Safari-in-private-browsing-mode where localStorage exists
// but can't be used (#1489996)
try {
// TODO: add encryption
localStorage . setItem ( this . get _local _storage _prefix ( ) + key , JSON . stringify ( data ) ) ;
return true ;
}
catch ( e ) {
return false ;
}
} ;
// wrapper for localStorage.removeItem(key)
this . local _storage _remove _item = function ( key )
{
try {
localStorage . removeItem ( this . get _local _storage _prefix ( ) + key ) ;
return true ;
}
catch ( e ) {
return false ;
}
} ;
this . print _dialog = function ( )
{
if ( bw . safari )
setTimeout ( 'window.print()' , 10 ) ;
else
window . print ( ) ;
} ;
} // end object rcube_webmail
// some static methods
rcube _webmail . long _subject _title = function ( elem , indent , text _elem )
{
if ( ! elem . title ) {
var $elem = $ ( text _elem || elem ) ;
if ( $elem . width ( ) + ( indent || 0 ) * 15 > $elem . parent ( ) . width ( ) )
elem . title = rcube _webmail . subject _text ( $elem [ 0 ] ) ;
}
} ;
rcube _webmail . long _subject _title _ex = function ( elem )
{
if ( ! elem . title ) {
var $elem = $ ( elem ) ,
txt = $elem . text ( ) . trim ( ) ,
indent = $ ( 'span.branch' , $elem ) . width ( ) || 0 ,
tmp = $ ( '<span>' ) . text ( txt )
. css ( { position : 'absolute' , 'float' : 'left' , visibility : 'hidden' ,
'font-size' : $elem . css ( 'font-size' ) , 'font-weight' : $elem . css ( 'font-weight' ) } )
. appendTo ( document . body ) ,
w = tmp . width ( ) ;
tmp . remove ( ) ;
if ( w + indent * 15 > $elem . width ( ) )
elem . title = rcube _webmail . subject _text ( elem ) ;
}
} ;
rcube _webmail . subject _text = function ( elem )
{
var t = $ ( elem ) . clone ( ) ;
t . find ( '.skip-on-drag,.skip-content,.voice' ) . remove ( ) ;
return t . text ( ) . trim ( ) ;
} ;
// set event handlers on all iframe elements (and their contents)
rcube _webmail . set _iframe _events = function ( events )
{
$ ( 'iframe' ) . each ( function ( ) {
var frame = $ ( this ) ;
$ . each ( events , function ( event _name , event _handler ) {
frame . on ( 'load' , function ( e ) {
try { $ ( this ) . contents ( ) . on ( event _name , event _handler ) ; }
catch ( e ) { /* catch possible permission error in IE */ }
} ) ;
try { frame . contents ( ) . on ( event _name , event _handler ) ; }
catch ( e ) { /* catch possible permission error in IE */ }
} ) ;
} ) ;
} ;
rcube _webmail . prototype . get _cookie = getCookie ;
// copy event engine prototype
rcube _webmail . prototype . addEventListener = rcube _event _engine . prototype . addEventListener ;
rcube _webmail . prototype . removeEventListener = rcube _event _engine . prototype . removeEventListener ;
rcube _webmail . prototype . triggerEvent = rcube _event _engine . prototype . triggerEvent ;