Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

app.js 275KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392439343944395439643974398439944004401440244034404440544064407440844094410441144124413441444154416441744184419442044214422442344244425442644274428442944304431443244334434443544364437443844394440444144424443444444454446444744484449445044514452445344544455445644574458445944604461446244634464446544664467446844694470447144724473447444754476447744784479448044814482448344844485448644874488448944904491449244934494449544964497449844994500450145024503450445054506450745084509451045114512451345144515451645174518451945204521452245234524452545264527452845294530453145324533453445354536453745384539454045414542454345444545454645474548454945504551455245534554455545564557455845594560456145624563456445654566456745684569457045714572457345744575457645774578457945804581458245834584458545864587458845894590459145924593459445954596459745984599460046014602460346044605460646074608460946104611461246134614461546164617461846194620462146224623462446254626462746284629463046314632463346344635463646374638463946404641464246434644464546464647464846494650465146524653465446554656465746584659466046614662466346644665466646674668466946704671467246734674467546764677467846794680468146824683468446854686468746884689469046914692469346944695469646974698469947004701470247034704470547064707470847094710471147124713471447154716471747184719472047214722472347244725472647274728472947304731473247334734473547364737473847394740474147424743474447454746474747484749475047514752475347544755475647574758475947604761476247634764476547664767476847694770477147724773477447754776477747784779478047814782478347844785478647874788478947904791479247934794479547964797479847994800480148024803480448054806480748084809481048114812481348144815481648174818481948204821482248234824482548264827482848294830483148324833483448354836483748384839484048414842484348444845484648474848484948504851485248534854485548564857485848594860486148624863486448654866486748684869487048714872487348744875487648774878487948804881488248834884488548864887488848894890489148924893489448954896489748984899490049014902490349044905490649074908490949104911491249134914491549164917491849194920492149224923492449254926492749284929493049314932493349344935493649374938493949404941494249434944494549464947494849494950495149524953495449554956495749584959496049614962496349644965496649674968496949704971497249734974497549764977497849794980498149824983498449854986498749884989499049914992499349944995499649974998499950005001500250035004500550065007500850095010501150125013501450155016501750185019502050215022502350245025502650275028502950305031503250335034503550365037503850395040504150425043504450455046504750485049505050515052505350545055505650575058505950605061506250635064506550665067506850695070507150725073507450755076507750785079508050815082508350845085508650875088508950905091509250935094509550965097509850995100510151025103510451055106510751085109511051115112511351145115511651175118511951205121512251235124512551265127512851295130513151325133513451355136513751385139514051415142514351445145514651475148514951505151515251535154515551565157515851595160516151625163516451655166516751685169517051715172517351745175517651775178517951805181518251835184518551865187518851895190519151925193519451955196519751985199520052015202520352045205520652075208520952105211521252135214521552165217521852195220522152225223522452255226522752285229523052315232523352345235523652375238523952405241524252435244524552465247524852495250525152525253525452555256525752585259526052615262526352645265526652675268526952705271527252735274527552765277527852795280528152825283528452855286528752885289529052915292529352945295529652975298529953005301530253035304530553065307530853095310531153125313531453155316531753185319532053215322532353245325532653275328532953305331533253335334533553365337533853395340534153425343534453455346534753485349535053515352535353545355535653575358535953605361536253635364536553665367536853695370537153725373537453755376537753785379538053815382538353845385538653875388538953905391539253935394539553965397539853995400540154025403540454055406540754085409541054115412541354145415541654175418541954205421542254235424542554265427542854295430543154325433543454355436543754385439544054415442544354445445544654475448544954505451545254535454545554565457545854595460546154625463546454655466546754685469547054715472547354745475547654775478547954805481548254835484548554865487548854895490549154925493549454955496549754985499550055015502550355045505550655075508550955105511551255135514551555165517551855195520552155225523552455255526552755285529553055315532553355345535553655375538553955405541554255435544554555465547554855495550555155525553555455555556555755585559556055615562556355645565556655675568556955705571557255735574557555765577557855795580558155825583558455855586558755885589559055915592559355945595559655975598559956005601560256035604560556065607560856095610561156125613561456155616561756185619562056215622562356245625562656275628562956305631563256335634563556365637563856395640564156425643564456455646564756485649565056515652565356545655565656575658565956605661566256635664566556665667566856695670567156725673567456755676567756785679568056815682568356845685568656875688568956905691569256935694569556965697569856995700570157025703570457055706570757085709571057115712571357145715571657175718571957205721572257235724572557265727572857295730573157325733573457355736573757385739574057415742574357445745574657475748574957505751575257535754575557565757575857595760576157625763576457655766576757685769577057715772577357745775577657775778577957805781578257835784578557865787578857895790579157925793579457955796579757985799580058015802580358045805580658075808580958105811581258135814581558165817581858195820582158225823582458255826582758285829583058315832583358345835583658375838583958405841584258435844584558465847584858495850585158525853585458555856585758585859586058615862586358645865586658675868586958705871587258735874587558765877587858795880588158825883588458855886588758885889589058915892589358945895589658975898589959005901590259035904590559065907590859095910591159125913591459155916591759185919592059215922592359245925592659275928592959305931593259335934593559365937593859395940594159425943594459455946594759485949595059515952595359545955595659575958595959605961596259635964596559665967596859695970597159725973597459755976597759785979598059815982598359845985598659875988598959905991599259935994599559965997599859996000600160026003600460056006600760086009601060116012601360146015601660176018601960206021602260236024602560266027602860296030603160326033603460356036603760386039604060416042604360446045604660476048604960506051605260536054605560566057605860596060606160626063606460656066606760686069607060716072607360746075607660776078607960806081608260836084608560866087608860896090609160926093609460956096609760986099610061016102610361046105610661076108610961106111611261136114611561166117611861196120612161226123612461256126612761286129613061316132613361346135613661376138613961406141614261436144614561466147614861496150615161526153615461556156615761586159616061616162616361646165616661676168616961706171617261736174617561766177617861796180618161826183618461856186618761886189619061916192619361946195619661976198619962006201620262036204620562066207620862096210621162126213621462156216621762186219622062216222622362246225622662276228622962306231623262336234623562366237623862396240624162426243624462456246624762486249625062516252625362546255625662576258625962606261626262636264626562666267626862696270627162726273627462756276627762786279628062816282628362846285628662876288628962906291629262936294629562966297629862996300630163026303630463056306630763086309631063116312631363146315631663176318631963206321632263236324632563266327632863296330633163326333633463356336633763386339634063416342634363446345634663476348634963506351635263536354635563566357635863596360636163626363636463656366636763686369637063716372637363746375637663776378637963806381638263836384638563866387638863896390639163926393639463956396639763986399640064016402640364046405640664076408640964106411641264136414641564166417641864196420642164226423642464256426642764286429643064316432643364346435643664376438643964406441644264436444644564466447644864496450645164526453645464556456645764586459646064616462646364646465646664676468646964706471647264736474647564766477647864796480648164826483648464856486648764886489649064916492649364946495649664976498649965006501650265036504650565066507650865096510651165126513651465156516651765186519652065216522652365246525652665276528652965306531653265336534653565366537653865396540654165426543654465456546654765486549655065516552655365546555655665576558655965606561656265636564656565666567656865696570657165726573657465756576657765786579658065816582658365846585658665876588658965906591659265936594659565966597659865996600660166026603660466056606660766086609661066116612661366146615661666176618661966206621662266236624662566266627662866296630663166326633663466356636663766386639664066416642664366446645664666476648664966506651665266536654665566566657665866596660666166626663666466656666666766686669667066716672667366746675667666776678667966806681668266836684668566866687668866896690669166926693669466956696669766986699670067016702670367046705670667076708670967106711671267136714671567166717671867196720672167226723672467256726672767286729673067316732673367346735673667376738673967406741674267436744674567466747674867496750675167526753675467556756675767586759676067616762676367646765676667676768676967706771677267736774677567766777677867796780678167826783678467856786678767886789679067916792679367946795679667976798679968006801680268036804680568066807680868096810681168126813681468156816681768186819682068216822682368246825682668276828682968306831683268336834683568366837683868396840684168426843684468456846684768486849685068516852685368546855685668576858685968606861686268636864686568666867686868696870687168726873687468756876687768786879688068816882688368846885688668876888688968906891689268936894689568966897689868996900690169026903690469056906690769086909691069116912691369146915691669176918691969206921692269236924692569266927692869296930693169326933693469356936693769386939694069416942694369446945694669476948694969506951695269536954695569566957695869596960696169626963696469656966696769686969697069716972697369746975697669776978697969806981698269836984698569866987698869896990699169926993699469956996699769986999700070017002700370047005700670077008700970107011701270137014701570167017701870197020702170227023702470257026702770287029703070317032703370347035703670377038703970407041704270437044704570467047704870497050705170527053705470557056705770587059706070617062706370647065706670677068706970707071707270737074707570767077707870797080708170827083708470857086708770887089709070917092709370947095709670977098709971007101710271037104710571067107710871097110711171127113711471157116711771187119712071217122712371247125712671277128712971307131713271337134713571367137713871397140714171427143714471457146714771487149715071517152715371547155715671577158715971607161716271637164716571667167716871697170717171727173717471757176717771787179718071817182718371847185718671877188718971907191719271937194719571967197719871997200720172027203720472057206720772087209721072117212721372147215721672177218721972207221722272237224722572267227722872297230723172327233723472357236723772387239724072417242724372447245724672477248724972507251725272537254725572567257725872597260726172627263726472657266726772687269727072717272727372747275727672777278727972807281728272837284728572867287728872897290729172927293729472957296729772987299730073017302730373047305730673077308730973107311731273137314731573167317731873197320732173227323732473257326732773287329733073317332733373347335733673377338733973407341734273437344734573467347734873497350735173527353735473557356735773587359736073617362736373647365736673677368736973707371737273737374737573767377737873797380738173827383738473857386738773887389739073917392739373947395739673977398739974007401740274037404740574067407740874097410741174127413741474157416741774187419742074217422742374247425742674277428742974307431743274337434743574367437743874397440744174427443744474457446744774487449745074517452745374547455745674577458745974607461746274637464746574667467746874697470747174727473747474757476747774787479748074817482748374847485748674877488748974907491749274937494749574967497749874997500750175027503750475057506750775087509751075117512751375147515751675177518751975207521752275237524752575267527752875297530753175327533753475357536753775387539754075417542754375447545754675477548754975507551755275537554755575567557755875597560756175627563756475657566756775687569757075717572757375747575757675777578757975807581758275837584758575867587758875897590759175927593759475957596759775987599760076017602760376047605760676077608760976107611761276137614761576167617761876197620762176227623762476257626762776287629763076317632763376347635763676377638763976407641764276437644764576467647764876497650765176527653765476557656765776587659766076617662766376647665766676677668766976707671767276737674767576767677767876797680768176827683768476857686768776887689769076917692769376947695769676977698769977007701770277037704770577067707770877097710771177127713771477157716771777187719772077217722772377247725772677277728772977307731773277337734773577367737773877397740774177427743774477457746774777487749775077517752775377547755775677577758775977607761776277637764776577667767776877697770777177727773777477757776777777787779778077817782778377847785778677877788778977907791779277937794779577967797779877997800780178027803780478057806780778087809781078117812781378147815781678177818781978207821782278237824782578267827782878297830783178327833783478357836783778387839784078417842784378447845784678477848784978507851785278537854785578567857785878597860786178627863786478657866786778687869787078717872787378747875787678777878787978807881788278837884788578867887788878897890789178927893789478957896789778987899790079017902790379047905790679077908790979107911791279137914791579167917791879197920792179227923792479257926792779287929793079317932793379347935793679377938793979407941794279437944794579467947794879497950795179527953795479557956795779587959796079617962796379647965796679677968796979707971797279737974797579767977797879797980798179827983798479857986798779887989799079917992799379947995799679977998799980008001800280038004800580068007800880098010801180128013801480158016801780188019802080218022802380248025802680278028802980308031803280338034803580368037803880398040804180428043804480458046804780488049805080518052805380548055805680578058805980608061806280638064806580668067806880698070807180728073807480758076807780788079808080818082808380848085808680878088808980908091809280938094809580968097809880998100810181028103810481058106810781088109811081118112811381148115811681178118811981208121812281238124812581268127812881298130813181328133813481358136813781388139814081418142814381448145814681478148814981508151815281538154815581568157815881598160816181628163816481658166816781688169817081718172817381748175817681778178817981808181818281838184818581868187818881898190819181928193819481958196819781988199820082018202820382048205820682078208820982108211821282138214821582168217821882198220822182228223822482258226822782288229823082318232823382348235823682378238823982408241824282438244824582468247824882498250825182528253825482558256825782588259826082618262826382648265826682678268826982708271827282738274827582768277827882798280828182828283828482858286828782888289829082918292829382948295829682978298829983008301830283038304830583068307830883098310831183128313831483158316831783188319832083218322832383248325832683278328832983308331833283338334833583368337833883398340834183428343834483458346834783488349835083518352835383548355835683578358835983608361836283638364836583668367836883698370837183728373837483758376837783788379838083818382838383848385838683878388838983908391839283938394839583968397839883998400840184028403840484058406840784088409841084118412841384148415841684178418841984208421842284238424842584268427842884298430843184328433843484358436843784388439844084418442844384448445844684478448844984508451845284538454845584568457845884598460846184628463846484658466846784688469847084718472847384748475847684778478847984808481848284838484848584868487848884898490849184928493849484958496849784988499850085018502850385048505850685078508850985108511851285138514851585168517851885198520852185228523852485258526852785288529853085318532853385348535853685378538853985408541854285438544854585468547854885498550855185528553855485558556855785588559856085618562856385648565856685678568856985708571857285738574857585768577857885798580858185828583858485858586858785888589859085918592859385948595859685978598859986008601860286038604860586068607860886098610861186128613861486158616861786188619862086218622862386248625862686278628862986308631863286338634863586368637863886398640864186428643864486458646864786488649865086518652865386548655865686578658865986608661866286638664866586668667866886698670867186728673867486758676867786788679868086818682868386848685868686878688868986908691869286938694869586968697869886998700870187028703870487058706870787088709871087118712871387148715871687178718871987208721872287238724872587268727872887298730873187328733873487358736873787388739874087418742874387448745874687478748874987508751875287538754875587568757875887598760876187628763876487658766876787688769877087718772877387748775877687778778877987808781878287838784878587868787878887898790879187928793879487958796879787988799880088018802880388048805880688078808880988108811881288138814881588168817881888198820882188228823882488258826882788288829883088318832883388348835883688378838883988408841884288438844884588468847884888498850885188528853885488558856885788588859886088618862886388648865886688678868886988708871887288738874887588768877887888798880888188828883888488858886888788888889889088918892889388948895889688978898889989008901890289038904890589068907890889098910891189128913891489158916891789188919892089218922892389248925892689278928892989308931893289338934893589368937893889398940894189428943894489458946894789488949895089518952895389548955895689578958895989608961896289638964896589668967896889698970897189728973897489758976897789788979898089818982898389848985898689878988898989908991899289938994899589968997899889999000900190029003900490059006
  1. /**
  2. * Roundcube Webmail Client Script
  3. *
  4. * This file is part of the Roundcube Webmail client
  5. *
  6. * @licstart The following is the entire license notice for the
  7. * JavaScript code in this file.
  8. *
  9. * Copyright (C) 2005-2015, The Roundcube Dev Team
  10. * Copyright (C) 2011-2015, Kolab Systems AG
  11. *
  12. * The JavaScript code in this page is free software: you can
  13. * redistribute it and/or modify it under the terms of the GNU
  14. * General Public License (GNU GPL) as published by the Free Software
  15. * Foundation, either version 3 of the License, or (at your option)
  16. * any later version. The code is distributed WITHOUT ANY WARRANTY;
  17. * without even the implied warranty of MERCHANTABILITY or FITNESS
  18. * FOR A PARTICULAR PURPOSE. See the GNU GPL for more details.
  19. *
  20. * As additional permission under GNU GPL version 3 section 7, you
  21. * may distribute non-source (e.g., minimized or compacted) forms of
  22. * that code without the copy of the GNU GPL normally required by
  23. * section 4, provided you include this license notice and a URL
  24. * through which recipients can access the Corresponding Source.
  25. *
  26. * @licend The above is the entire license notice
  27. * for the JavaScript code in this file.
  28. *
  29. * @author Thomas Bruederli <roundcube@gmail.com>
  30. * @author Aleksander 'A.L.E.C' Machniak <alec@alec.pl>
  31. * @author Charles McNulty <charles@charlesmcnulty.com>
  32. *
  33. * @requires jquery.js, common.js, list.js
  34. */
  35. function rcube_webmail()
  36. {
  37. this.labels = {};
  38. this.buttons = {};
  39. this.buttons_sel = {};
  40. this.gui_objects = {};
  41. this.gui_containers = {};
  42. this.commands = {};
  43. this.command_handlers = {};
  44. this.onloads = [];
  45. this.messages = {};
  46. this.group2expand = {};
  47. this.http_request_jobs = {};
  48. this.menu_stack = [];
  49. // webmail client settings
  50. this.dblclick_time = 500;
  51. this.message_time = 5000;
  52. this.identifier_expr = /[^0-9a-z_-]/gi;
  53. // environment defaults
  54. this.env = {
  55. request_timeout: 180, // seconds
  56. draft_autosave: 0, // seconds
  57. comm_path: './',
  58. recipients_separator: ',',
  59. recipients_delimiter: ', ',
  60. popup_width: 1150,
  61. popup_width_small: 900
  62. };
  63. // create protected reference to myself
  64. this.ref = 'rcmail';
  65. var ref = this;
  66. // set jQuery ajax options
  67. $.ajaxSetup({
  68. cache: false,
  69. timeout: this.env.request_timeout * 1000,
  70. error: function(request, status, err){ ref.http_error(request, status, err); },
  71. beforeSend: function(xmlhttp){ xmlhttp.setRequestHeader('X-Roundcube-Request', ref.env.request_token); }
  72. });
  73. // unload fix
  74. $(window).on('beforeunload', function() { ref.unload = true; });
  75. // set environment variable(s)
  76. this.set_env = function(p, value)
  77. {
  78. if (p != null && typeof p === 'object' && !value)
  79. for (var n in p)
  80. this.env[n] = p[n];
  81. else
  82. this.env[p] = value;
  83. };
  84. // add a localized label to the client environment
  85. this.add_label = function(p, value)
  86. {
  87. if (typeof p == 'string')
  88. this.labels[p] = value;
  89. else if (typeof p == 'object')
  90. $.extend(this.labels, p);
  91. };
  92. // add a button to the button list
  93. this.register_button = function(command, id, type, act, sel, over)
  94. {
  95. var button_prop = {id:id, type:type};
  96. if (act) button_prop.act = act;
  97. if (sel) button_prop.sel = sel;
  98. if (over) button_prop.over = over;
  99. if (!this.buttons[command])
  100. this.buttons[command] = [];
  101. this.buttons[command].push(button_prop);
  102. if (this.loaded)
  103. init_button(command, button_prop);
  104. };
  105. // register a specific gui object
  106. this.gui_object = function(name, id)
  107. {
  108. this.gui_objects[name] = this.loaded ? rcube_find_object(id) : id;
  109. };
  110. // register a container object
  111. this.gui_container = function(name, id)
  112. {
  113. this.gui_containers[name] = id;
  114. };
  115. // add a GUI element (html node) to a specified container
  116. this.add_element = function(elm, container)
  117. {
  118. if (this.gui_containers[container] && this.gui_containers[container].jquery)
  119. this.gui_containers[container].append(elm);
  120. };
  121. // register an external handler for a certain command
  122. this.register_command = function(command, callback, enable)
  123. {
  124. this.command_handlers[command] = callback;
  125. if (enable)
  126. this.enable_command(command, true);
  127. };
  128. // execute the given script on load
  129. this.add_onload = function(f)
  130. {
  131. this.onloads.push(f);
  132. };
  133. // initialize webmail client
  134. this.init = function()
  135. {
  136. var n;
  137. this.task = this.env.task;
  138. // check browser capabilities (never use version checks here)
  139. if (this.env.server_error != 409 && (!bw.dom || !bw.xmlhttp_test())) {
  140. this.goto_url('error', '_code=0x199');
  141. return;
  142. }
  143. if (!this.env.blankpage)
  144. this.env.blankpage = this.assets_path('program/resources/blank.gif');
  145. // find all registered gui containers
  146. for (n in this.gui_containers)
  147. this.gui_containers[n] = $('#'+this.gui_containers[n]);
  148. // find all registered gui objects
  149. for (n in this.gui_objects)
  150. this.gui_objects[n] = rcube_find_object(this.gui_objects[n]);
  151. // clickjacking protection
  152. if (this.env.x_frame_options) {
  153. try {
  154. // bust frame if not allowed
  155. if (this.env.x_frame_options == 'deny' && top.location.href != self.location.href)
  156. top.location.href = self.location.href;
  157. else if (top.location.hostname != self.location.hostname)
  158. throw 1;
  159. } catch (e) {
  160. // possible clickjacking attack: disable all form elements
  161. $('form').each(function(){ ref.lock_form(this, true); });
  162. this.display_message("Blocked: possible clickjacking attack!", 'error');
  163. return;
  164. }
  165. }
  166. // init registered buttons
  167. this.init_buttons();
  168. // tell parent window that this frame is loaded
  169. if (this.is_framed()) {
  170. parent.rcmail.set_busy(false, null, parent.rcmail.env.frame_lock);
  171. parent.rcmail.env.frame_lock = null;
  172. }
  173. // enable general commands
  174. this.enable_command('close', 'logout', 'mail', 'addressbook', 'settings', 'save-pref',
  175. 'compose', 'undo', 'about', 'switch-task', 'menu-open', 'menu-close', 'menu-save', true);
  176. // set active task button
  177. this.set_button(this.task, 'sel');
  178. if (this.env.permaurl)
  179. this.enable_command('permaurl', 'extwin', true);
  180. switch (this.task) {
  181. case 'mail':
  182. // enable mail commands
  183. this.enable_command('list', 'checkmail', 'add-contact', 'search', 'reset-search', 'collapse-folder', 'import-messages', true);
  184. if (this.gui_objects.messagelist) {
  185. this.message_list = new rcube_list_widget(this.gui_objects.messagelist, {
  186. multiselect:true, multiexpand:true, draggable:true, keyboard:true,
  187. column_movable:this.env.col_movable, dblclick_time:this.dblclick_time
  188. });
  189. this.message_list
  190. .addEventListener('initrow', function(o) { ref.init_message_row(o); })
  191. .addEventListener('dblclick', function(o) { ref.msglist_dbl_click(o); })
  192. .addEventListener('click', function(o) { ref.msglist_click(o); })
  193. .addEventListener('keypress', function(o) { ref.msglist_keypress(o); })
  194. .addEventListener('select', function(o) { ref.msglist_select(o); })
  195. .addEventListener('dragstart', function(o) { ref.drag_start(o); })
  196. .addEventListener('dragmove', function(e) { ref.drag_move(e); })
  197. .addEventListener('dragend', function(e) { ref.drag_end(e); })
  198. .addEventListener('expandcollapse', function(o) { ref.msglist_expand(o); })
  199. .addEventListener('column_replace', function(o) { ref.msglist_set_coltypes(o); })
  200. .addEventListener('listupdate', function(o) { ref.triggerEvent('listupdate', o); })
  201. .init();
  202. // TODO: this should go into the list-widget code
  203. $(this.message_list.thead).on('click', 'a.sortcol', function(e){
  204. return ref.command('sort', $(this).attr('rel'), this);
  205. });
  206. this.enable_command('toggle_status', 'toggle_flag', 'sort', true);
  207. this.enable_command('set-listmode', this.env.threads && !this.is_multifolder_listing());
  208. // load messages
  209. this.command('list');
  210. $(this.gui_objects.qsearchbox).val(this.env.search_text).focusin(function() { ref.message_list.blur(); });
  211. }
  212. this.set_button_titles();
  213. this.env.message_commands = ['show', 'reply', 'reply-all', 'reply-list',
  214. 'move', 'copy', 'delete', 'open', 'mark', 'edit', 'viewsource',
  215. 'print', 'load-attachment', 'download-attachment', 'show-headers', 'hide-headers', 'download',
  216. 'forward', 'forward-inline', 'forward-attachment', 'change-format'];
  217. if (this.env.action == 'show' || this.env.action == 'preview') {
  218. this.enable_command(this.env.message_commands, this.env.uid);
  219. this.enable_command('reply-list', this.env.list_post);
  220. if (this.env.action == 'show') {
  221. this.http_request('pagenav', {_uid: this.env.uid, _mbox: this.env.mailbox, _search: this.env.search_request},
  222. this.display_message('', 'loading'));
  223. }
  224. if (this.env.blockedobjects) {
  225. if (this.gui_objects.remoteobjectsmsg)
  226. this.gui_objects.remoteobjectsmsg.style.display = 'block';
  227. this.enable_command('load-images', 'always-load', true);
  228. }
  229. // make preview/message frame visible
  230. if (this.env.action == 'preview' && this.is_framed()) {
  231. this.enable_command('compose', 'add-contact', false);
  232. parent.rcmail.show_contentframe(true);
  233. }
  234. // initialize drag-n-drop on attachments, so they can e.g.
  235. // be dropped into mail compose attachments in another window
  236. if (this.gui_objects.attachments)
  237. $('li > a', this.gui_objects.attachments).not('.drop').on('dragstart', function(e) {
  238. var n, href = this.href, dt = e.originalEvent.dataTransfer;
  239. if (dt) {
  240. // inject username to the uri
  241. href = href.replace(/^https?:\/\//, function(m) { return m + urlencode(ref.env.username) + '@'});
  242. // cleanup the node to get filename without the size test
  243. n = $(this).clone();
  244. n.children().remove();
  245. dt.setData('roundcube-uri', href);
  246. dt.setData('roundcube-name', $.trim(n.text()));
  247. }
  248. });
  249. }
  250. else if (this.env.action == 'compose') {
  251. this.env.address_group_stack = [];
  252. this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel',
  253. 'toggle-editor', 'list-addresses', 'pushgroup', 'search', 'reset-search', 'extwin',
  254. 'insert-response', 'save-response', 'menu-open', 'menu-close'];
  255. if (this.env.drafts_mailbox)
  256. this.env.compose_commands.push('savedraft')
  257. this.enable_command(this.env.compose_commands, 'identities', 'responses', true);
  258. // add more commands (not enabled)
  259. $.merge(this.env.compose_commands, ['add-recipient', 'firstpage', 'previouspage', 'nextpage', 'lastpage']);
  260. if (window.googie) {
  261. this.env.editor_config.spellchecker = googie;
  262. this.env.editor_config.spellcheck_observer = function(s) { ref.spellcheck_state(); };
  263. this.env.compose_commands.push('spellcheck')
  264. this.enable_command('spellcheck', true);
  265. }
  266. // initialize HTML editor
  267. this.editor_init(this.env.editor_config, this.env.composebody);
  268. // init canned response functions
  269. if (this.gui_objects.responseslist) {
  270. $('a.insertresponse', this.gui_objects.responseslist)
  271. .attr('unselectable', 'on')
  272. .mousedown(function(e) { return rcube_event.cancel(e); })
  273. .on('mouseup keypress', function(e) {
  274. if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
  275. ref.command('insert-response', $(this).attr('rel'));
  276. $(document.body).trigger('mouseup'); // hides the menu
  277. return rcube_event.cancel(e);
  278. }
  279. });
  280. // avoid textarea loosing focus when hitting the save-response button/link
  281. $.each(this.buttons['save-response'] || [], function (i, v) {
  282. $('#' + v.id).mousedown(function(e){ return rcube_event.cancel(e); })
  283. });
  284. }
  285. // init message compose form
  286. this.init_messageform();
  287. }
  288. else if (this.env.action == 'get')
  289. this.enable_command('download', 'print', true);
  290. // show printing dialog
  291. else if (this.env.action == 'print' && this.env.uid
  292. && !this.env.is_pgp_content && !this.env.pgp_mime_part
  293. ) {
  294. this.print_dialog();
  295. }
  296. // get unread count for each mailbox
  297. if (this.gui_objects.mailboxlist) {
  298. this.env.unread_counts = {};
  299. this.gui_objects.folderlist = this.gui_objects.mailboxlist;
  300. this.http_request('getunread', {_page: this.env.current_page});
  301. }
  302. // init address book widget
  303. if (this.gui_objects.contactslist) {
  304. this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
  305. { multiselect:true, draggable:false, keyboard:true });
  306. this.contact_list
  307. .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
  308. .addEventListener('select', function(o) { ref.compose_recipient_select(o); })
  309. .addEventListener('dblclick', function(o) { ref.compose_add_recipient(); })
  310. .addEventListener('keypress', function(o) {
  311. if (o.key_pressed == o.ENTER_KEY) {
  312. if (!ref.compose_add_recipient()) {
  313. // execute link action on <enter> if not a recipient entry
  314. if (o.last_selected && String(o.last_selected).charAt(0) == 'G') {
  315. $(o.rows[o.last_selected].obj).find('a').first().click();
  316. }
  317. }
  318. }
  319. })
  320. .init();
  321. // remember last focused address field
  322. $('#_to,#_cc,#_bcc').focus(function() { ref.env.focused_field = this; });
  323. }
  324. if (this.gui_objects.addressbookslist) {
  325. this.gui_objects.folderlist = this.gui_objects.addressbookslist;
  326. this.enable_command('list-addresses', true);
  327. }
  328. // ask user to send MDN
  329. if (this.env.mdn_request && this.env.uid) {
  330. var postact = 'sendmdn',
  331. postdata = {_uid: this.env.uid, _mbox: this.env.mailbox};
  332. if (!confirm(this.get_label('mdnrequest'))) {
  333. postdata._flag = 'mdnsent';
  334. postact = 'mark';
  335. }
  336. this.http_post(postact, postdata);
  337. }
  338. this.check_mailvelope(this.env.action);
  339. // detect browser capabilities
  340. if (!this.is_framed() && !this.env.extwin)
  341. this.browser_capabilities_check();
  342. break;
  343. case 'addressbook':
  344. this.env.address_group_stack = [];
  345. if (this.gui_objects.folderlist)
  346. this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
  347. this.enable_command('add', 'import', this.env.writable_source);
  348. this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'search', 'reset-search', 'advanced-search', true);
  349. if (this.gui_objects.contactslist) {
  350. this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
  351. {multiselect:true, draggable:this.gui_objects.folderlist?true:false, keyboard:true});
  352. this.contact_list
  353. .addEventListener('initrow', function(o) { ref.triggerEvent('insertrow', { cid:o.uid, row:o }); })
  354. .addEventListener('keypress', function(o) { ref.contactlist_keypress(o); })
  355. .addEventListener('select', function(o) { ref.contactlist_select(o); })
  356. .addEventListener('dragstart', function(o) { ref.drag_start(o); })
  357. .addEventListener('dragmove', function(e) { ref.drag_move(e); })
  358. .addEventListener('dragend', function(e) { ref.drag_end(e); })
  359. .init();
  360. $(this.gui_objects.qsearchbox).focusin(function() { ref.contact_list.blur(); });
  361. this.update_group_commands();
  362. this.command('list');
  363. }
  364. if (this.gui_objects.savedsearchlist) {
  365. this.savedsearchlist = new rcube_treelist_widget(this.gui_objects.savedsearchlist, {
  366. id_prefix: 'rcmli',
  367. id_encode: this.html_identifier_encode,
  368. id_decode: this.html_identifier_decode
  369. });
  370. this.savedsearchlist.addEventListener('select', function(node) {
  371. ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }); });
  372. }
  373. this.set_page_buttons();
  374. if (this.env.cid) {
  375. this.enable_command('show', 'edit', true);
  376. // register handlers for group assignment via checkboxes
  377. if (this.gui_objects.editform) {
  378. $('input.groupmember').change(function() {
  379. ref.group_member_change(this.checked ? 'add' : 'del', ref.env.cid, ref.env.source, this.value);
  380. });
  381. }
  382. }
  383. if (this.gui_objects.editform) {
  384. this.enable_command('save', true);
  385. if (this.env.action == 'add' || this.env.action == 'edit' || this.env.action == 'search')
  386. this.init_contact_form();
  387. }
  388. else if (this.env.action == 'print') {
  389. this.print_dialog();
  390. }
  391. break;
  392. case 'settings':
  393. this.enable_command('preferences', 'identities', 'responses', 'save', 'folders', true);
  394. if (this.env.action == 'identities') {
  395. this.enable_command('add', this.env.identities_level < 2);
  396. }
  397. else if (this.env.action == 'edit-identity' || this.env.action == 'add-identity') {
  398. this.enable_command('save', 'edit', 'toggle-editor', true);
  399. this.enable_command('delete', this.env.identities_level < 2);
  400. // initialize HTML editor
  401. this.editor_init(this.env.editor_config, 'rcmfd_signature');
  402. }
  403. else if (this.env.action == 'folders') {
  404. this.enable_command('subscribe', 'unsubscribe', 'create-folder', 'rename-folder', true);
  405. }
  406. else if (this.env.action == 'edit-folder' && this.gui_objects.editform) {
  407. this.enable_command('save', 'folder-size', true);
  408. parent.rcmail.env.exists = this.env.messagecount;
  409. parent.rcmail.enable_command('purge', this.env.messagecount);
  410. }
  411. else if (this.env.action == 'responses') {
  412. this.enable_command('add', true);
  413. }
  414. if (this.gui_objects.identitieslist) {
  415. this.identity_list = new rcube_list_widget(this.gui_objects.identitieslist,
  416. {multiselect:false, draggable:false, keyboard:true});
  417. this.identity_list
  418. .addEventListener('select', function(o) { ref.identity_select(o); })
  419. .addEventListener('keypress', function(o) {
  420. if (o.key_pressed == o.ENTER_KEY) {
  421. ref.identity_select(o);
  422. }
  423. })
  424. .init()
  425. .focus();
  426. }
  427. else if (this.gui_objects.sectionslist) {
  428. this.sections_list = new rcube_list_widget(this.gui_objects.sectionslist, {multiselect:false, draggable:false, keyboard:true});
  429. this.sections_list
  430. .addEventListener('select', function(o) { ref.section_select(o); })
  431. .addEventListener('keypress', function(o) { if (o.key_pressed == o.ENTER_KEY) ref.section_select(o); })
  432. .init()
  433. .focus();
  434. }
  435. else if (this.gui_objects.subscriptionlist) {
  436. this.init_subscription_list();
  437. }
  438. else if (this.gui_objects.responseslist) {
  439. this.responses_list = new rcube_list_widget(this.gui_objects.responseslist, {multiselect:false, draggable:false, keyboard:true});
  440. this.responses_list
  441. .addEventListener('select', function(list) {
  442. var win, id = list.get_single_selection();
  443. ref.enable_command('delete', !!id && $.inArray(id, ref.env.readonly_responses) < 0);
  444. if (id && (win = ref.get_frame_window(ref.env.contentframe))) {
  445. ref.set_busy(true);
  446. ref.location_href({ _action:'edit-response', _key:id, _framed:1 }, win);
  447. }
  448. })
  449. .init()
  450. .focus();
  451. }
  452. break;
  453. case 'login':
  454. var tz, tz_name, jstz = window.jstz,
  455. input_user = $('#rcmloginuser'),
  456. input_tz = $('#rcmlogintz');
  457. input_user.keyup(function(e) { return ref.login_user_keyup(e); });
  458. if (input_user.val() == '')
  459. input_user.focus();
  460. else
  461. $('#rcmloginpwd').focus();
  462. // detect client timezone
  463. if (jstz && (tz = jstz.determine()))
  464. tz_name = tz.name();
  465. input_tz.val(tz_name ? tz_name : (new Date().getStdTimezoneOffset() / -60));
  466. // display 'loading' message on form submit, lock submit button
  467. $('form').submit(function () {
  468. $('input[type=submit]', this).prop('disabled', true);
  469. ref.clear_messages();
  470. ref.display_message('', 'loading');
  471. });
  472. this.enable_command('login', true);
  473. break;
  474. }
  475. // select first input field in an edit form
  476. if (this.gui_objects.editform)
  477. $("input,select,textarea", this.gui_objects.editform)
  478. .not(':hidden').not(':disabled').first().select().focus();
  479. // unset contentframe variable if preview_pane is enabled
  480. if (this.env.contentframe && !$('#' + this.env.contentframe).is(':visible'))
  481. this.env.contentframe = null;
  482. // prevent from form submit with Enter key in file input fields
  483. if (bw.ie)
  484. $('input[type=file]').keydown(function(e) { if (e.keyCode == '13') e.preventDefault(); });
  485. // flag object as complete
  486. this.loaded = true;
  487. this.env.lastrefresh = new Date();
  488. // show message
  489. if (this.pending_message)
  490. this.display_message.apply(this, this.pending_message);
  491. // init treelist widget
  492. if (this.gui_objects.folderlist && window.rcube_treelist_widget
  493. // some plugins may load rcube_treelist_widget and there's one case
  494. // when this will cause problems - addressbook widget in compose,
  495. // which already has been initialized using rcube_list_widget
  496. && this.gui_objects.folderlist != this.gui_objects.addressbookslist
  497. ) {
  498. this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, {
  499. selectable: true,
  500. id_prefix: 'rcmli',
  501. parent_focus: true,
  502. id_encode: this.html_identifier_encode,
  503. id_decode: this.html_identifier_decode,
  504. check_droptarget: function(node) { return !node.virtual && ref.check_droptarget(node.id) }
  505. });
  506. this.treelist
  507. .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
  508. .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
  509. .addEventListener('beforeselect', function(node) { return !ref.busy; })
  510. .addEventListener('select', function(node) { ref.triggerEvent('selectfolder', { folder:node.id, prefix:'rcmli' }) });
  511. }
  512. // activate html5 file drop feature (if browser supports it and if configured)
  513. if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) {
  514. $(document.body).on('dragover dragleave drop', function(e) { return ref.document_drag_hover(e, e.type == 'dragover'); });
  515. $(this.gui_objects.filedrop).addClass('droptarget')
  516. .on('dragover dragleave', function(e) { return ref.file_drag_hover(e, e.type == 'dragover'); })
  517. .get(0).addEventListener('drop', function(e) { return ref.file_dropped(e); }, false);
  518. }
  519. // catch document (and iframe) mouse clicks
  520. var body_mouseup = function(e) { return ref.doc_mouse_up(e); };
  521. $(document.body)
  522. .mouseup(body_mouseup)
  523. .keydown(function(e) { return ref.doc_keypress(e); });
  524. rcube_webmail.set_iframe_events({mouseup: body_mouseup});
  525. // trigger init event hook
  526. this.triggerEvent('init', { task:this.task, action:this.env.action });
  527. // execute all foreign onload scripts
  528. // @deprecated
  529. for (n in this.onloads) {
  530. if (typeof this.onloads[n] === 'string')
  531. eval(this.onloads[n]);
  532. else if (typeof this.onloads[n] === 'function')
  533. this.onloads[n]();
  534. }
  535. // start keep-alive and refresh intervals
  536. this.start_refresh();
  537. this.start_keepalive();
  538. };
  539. this.log = function(msg)
  540. {
  541. if (window.console && console.log)
  542. console.log(msg);
  543. };
  544. /*********************************************************/
  545. /********* client command interface *********/
  546. /*********************************************************/
  547. // execute a specific command on the web client
  548. this.command = function(command, props, obj, event)
  549. {
  550. var ret, uid, cid, url, flag, aborted = false;
  551. if (obj && obj.blur && !(event && rcube_event.is_keyboard(event)))
  552. obj.blur();
  553. // do nothing if interface is locked by another command
  554. // with exception for searching reset and menu
  555. if (this.busy && !(command == 'reset-search' && this.last_command == 'search') && !command.match(/^menu-/))
  556. return false;
  557. // let the browser handle this click (shift/ctrl usually opens the link in a new window/tab)
  558. if ((obj && obj.href && String(obj.href).indexOf('#') < 0) && rcube_event.get_modifier(event)) {
  559. return true;
  560. }
  561. // command not supported or allowed
  562. if (!this.commands[command]) {
  563. // pass command to parent window
  564. if (this.is_framed())
  565. parent.rcmail.command(command, props);
  566. return false;
  567. }
  568. // check input before leaving compose step
  569. if (this.task == 'mail' && this.env.action == 'compose' && !this.env.server_error && command != 'save-pref'
  570. && $.inArray(command, this.env.compose_commands) < 0
  571. ) {
  572. if (!this.env.is_sent && this.cmp_hash != this.compose_field_hash() && !confirm(this.get_label('notsentwarning')))
  573. return false;
  574. // remove copy from local storage if compose screen is left intentionally
  575. this.remove_compose_data(this.env.compose_id);
  576. this.compose_skip_unsavedcheck = true;
  577. }
  578. this.last_command = command;
  579. // process external commands
  580. if (typeof this.command_handlers[command] === 'function') {
  581. ret = this.command_handlers[command](props, obj, event);
  582. return ret !== undefined ? ret : (obj ? false : true);
  583. }
  584. else if (typeof this.command_handlers[command] === 'string') {
  585. ret = window[this.command_handlers[command]](props, obj, event);
  586. return ret !== undefined ? ret : (obj ? false : true);
  587. }
  588. // trigger plugin hooks
  589. this.triggerEvent('actionbefore', {props:props, action:command, originalEvent:event});
  590. ret = this.triggerEvent('before'+command, props || event);
  591. if (ret !== undefined) {
  592. // abort if one of the handlers returned false
  593. if (ret === false)
  594. return false;
  595. else
  596. props = ret;
  597. }
  598. ret = undefined;
  599. // process internal command
  600. switch (command) {
  601. case 'login':
  602. if (this.gui_objects.loginform)
  603. this.gui_objects.loginform.submit();
  604. break;
  605. // commands to switch task
  606. case 'logout':
  607. case 'mail':
  608. case 'addressbook':
  609. case 'settings':
  610. this.switch_task(command);
  611. break;
  612. case 'about':
  613. this.redirect('?_task=settings&_action=about', false);
  614. break;
  615. case 'permaurl':
  616. if (obj && obj.href && obj.target)
  617. return true;
  618. else if (this.env.permaurl)
  619. parent.location.href = this.env.permaurl;
  620. break;
  621. case 'extwin':
  622. if (this.env.action == 'compose') {
  623. var form = this.gui_objects.messageform,
  624. win = this.open_window('');
  625. if (win) {
  626. this.save_compose_form_local();
  627. this.compose_skip_unsavedcheck = true;
  628. $("input[name='_action']", form).val('compose');
  629. form.action = this.url('mail/compose', { _id: this.env.compose_id, _extwin: 1 });
  630. form.target = win.name;
  631. form.submit();
  632. }
  633. }
  634. else {
  635. this.open_window(this.env.permaurl, true);
  636. }
  637. break;
  638. case 'change-format':
  639. url = this.env.permaurl + '&_format=' + props;
  640. if (this.env.action == 'preview')
  641. url = url.replace(/_action=show/, '_action=preview') + '&_framed=1';
  642. if (this.env.extwin)
  643. url += '&_extwin=1';
  644. location.href = url;
  645. break;
  646. case 'menu-open':
  647. if (props && props.menu == 'attachmentmenu') {
  648. var mimetype = this.env.attachments[props.id];
  649. this.enable_command('open-attachment', mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0);
  650. }
  651. this.show_menu(props, props.show || undefined, event);
  652. break;
  653. case 'menu-close':
  654. this.hide_menu(props, event);
  655. break;
  656. case 'menu-save':
  657. this.triggerEvent(command, {props:props, originalEvent:event});
  658. return false;
  659. case 'open':
  660. if (uid = this.get_single_uid()) {
  661. obj.href = this.url('show', this.params_from_uid(uid));
  662. return true;
  663. }
  664. break;
  665. case 'close':
  666. if (this.env.extwin)
  667. window.close();
  668. break;
  669. case 'list':
  670. if (props && props != '') {
  671. this.reset_qsearch(true);
  672. }
  673. if (this.env.action == 'compose' && this.env.extwin) {
  674. window.close();
  675. }
  676. else if (this.task == 'mail') {
  677. this.list_mailbox(props);
  678. this.set_button_titles();
  679. }
  680. else if (this.task == 'addressbook')
  681. this.list_contacts(props);
  682. break;
  683. case 'set-listmode':
  684. this.set_list_options(null, undefined, undefined, props == 'threads' ? 1 : 0);
  685. break;
  686. case 'sort':
  687. var sort_order = this.env.sort_order,
  688. sort_col = !this.env.disabled_sort_col ? props : this.env.sort_col;
  689. if (!this.env.disabled_sort_order)
  690. sort_order = this.env.sort_col == sort_col && sort_order == 'ASC' ? 'DESC' : 'ASC';
  691. // set table header and update env
  692. this.set_list_sorting(sort_col, sort_order);
  693. // reload message list
  694. this.list_mailbox('', '', sort_col+'_'+sort_order);
  695. break;
  696. case 'nextpage':
  697. this.list_page('next');
  698. break;
  699. case 'lastpage':
  700. this.list_page('last');
  701. break;
  702. case 'previouspage':
  703. this.list_page('prev');
  704. break;
  705. case 'firstpage':
  706. this.list_page('first');
  707. break;
  708. case 'expunge':
  709. if (this.env.exists)
  710. this.expunge_mailbox(this.env.mailbox);
  711. break;
  712. case 'purge':
  713. case 'empty-mailbox':
  714. if (this.env.exists)
  715. this.purge_mailbox(this.env.mailbox);
  716. break;
  717. // common commands used in multiple tasks
  718. case 'show':
  719. if (this.task == 'mail') {
  720. uid = this.get_single_uid();
  721. if (uid && (!this.env.uid || uid != this.env.uid)) {
  722. if (this.env.mailbox == this.env.drafts_mailbox)
  723. this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
  724. else
  725. this.show_message(uid);
  726. }
  727. }
  728. else if (this.task == 'addressbook') {
  729. cid = props ? props : this.get_single_cid();
  730. if (cid && !(this.env.action == 'show' && cid == this.env.cid))
  731. this.load_contact(cid, 'show');
  732. }
  733. break;
  734. case 'add':
  735. if (this.task == 'addressbook')
  736. this.load_contact(0, 'add');
  737. else if (this.task == 'settings' && this.env.action == 'responses') {
  738. var frame;
  739. if ((frame = this.get_frame_window(this.env.contentframe))) {
  740. this.set_busy(true);
  741. this.location_href({ _action:'add-response', _framed:1 }, frame);
  742. }
  743. }
  744. else if (this.task == 'settings') {
  745. this.identity_list.clear_selection();
  746. this.load_identity(0, 'add-identity');
  747. }
  748. break;
  749. case 'edit':
  750. if (this.task == 'addressbook' && (cid = this.get_single_cid()))
  751. this.load_contact(cid, 'edit');
  752. else if (this.task == 'settings' && props)
  753. this.load_identity(props, 'edit-identity');
  754. else if (this.task == 'mail' && (uid = this.get_single_uid())) {
  755. url = { _mbox: this.get_message_mailbox(uid) };
  756. url[this.env.mailbox == this.env.drafts_mailbox && props != 'new' ? '_draft_uid' : '_uid'] = uid;
  757. this.open_compose_step(url);
  758. }
  759. break;
  760. case 'save':
  761. var input, form = this.gui_objects.editform;
  762. if (form) {
  763. // adv. search
  764. if (this.env.action == 'search') {
  765. }
  766. // user prefs
  767. else if ((input = $("input[name='_pagesize']", form)) && input.length && isNaN(parseInt(input.val()))) {
  768. alert(this.get_label('nopagesizewarning'));
  769. input.focus();
  770. break;
  771. }
  772. // contacts/identities
  773. else {
  774. // reload form
  775. if (props == 'reload') {
  776. form.action += '&_reload=1';
  777. }
  778. else if (this.task == 'settings' && (this.env.identities_level % 2) == 0 &&
  779. (input = $("input[name='_email']", form)) && input.length && !rcube_check_email(input.val())
  780. ) {
  781. alert(this.get_label('noemailwarning'));
  782. input.focus();
  783. break;
  784. }
  785. // clear empty input fields
  786. $('input.placeholder').each(function(){ if (this.value == this._placeholder) this.value = ''; });
  787. }
  788. // add selected source (on the list)
  789. if (parent.rcmail && parent.rcmail.env.source)
  790. form.action = this.add_url(form.action, '_orig_source', parent.rcmail.env.source);
  791. form.submit();
  792. }
  793. break;
  794. case 'delete':
  795. // mail task
  796. if (this.task == 'mail')
  797. this.delete_messages(event);
  798. // addressbook task
  799. else if (this.task == 'addressbook')
  800. this.delete_contacts();
  801. // settings: canned response
  802. else if (this.task == 'settings' && this.env.action == 'responses')
  803. this.delete_response();
  804. // settings: user identities
  805. else if (this.task == 'settings')
  806. this.delete_identity();
  807. break;
  808. // mail task commands
  809. case 'move':
  810. case 'moveto': // deprecated
  811. if (this.task == 'mail')
  812. this.move_messages(props, event);
  813. else if (this.task == 'addressbook')
  814. this.move_contacts(props);
  815. break;
  816. case 'copy':
  817. if (this.task == 'mail')
  818. this.copy_messages(props, event);
  819. else if (this.task == 'addressbook')
  820. this.copy_contacts(props);
  821. break;
  822. case 'mark':
  823. if (props)
  824. this.mark_message(props);
  825. break;
  826. case 'toggle_status':
  827. case 'toggle_flag':
  828. flag = command == 'toggle_flag' ? 'flagged' : 'read';
  829. if (uid = props) {
  830. // toggle flagged/unflagged
  831. if (flag == 'flagged') {
  832. if (this.message_list.rows[uid].flagged)
  833. flag = 'unflagged';
  834. }
  835. // toggle read/unread
  836. else if (this.message_list.rows[uid].deleted)
  837. flag = 'undelete';
  838. else if (!this.message_list.rows[uid].unread)
  839. flag = 'unread';
  840. this.mark_message(flag, uid);
  841. }
  842. break;
  843. case 'always-load':
  844. if (this.env.uid && this.env.sender) {
  845. this.add_contact(this.env.sender);
  846. setTimeout(function(){ ref.command('load-images'); }, 300);
  847. break;
  848. }
  849. case 'load-images':
  850. if (this.env.uid)
  851. this.show_message(this.env.uid, true, this.env.action=='preview');
  852. break;
  853. case 'load-attachment':
  854. case 'open-attachment':
  855. case 'download-attachment':
  856. var qstring = '_mbox='+urlencode(this.env.mailbox)+'&_uid='+this.env.uid+'&_part='+props,
  857. mimetype = this.env.attachments[props];
  858. // open attachment in frame if it's of a supported mimetype
  859. if (command != 'download-attachment' && mimetype && this.env.mimetypes && $.inArray(mimetype, this.env.mimetypes) >= 0) {
  860. if (this.open_window(this.env.comm_path+'&_action=get&'+qstring+'&_frame=1'))
  861. break;
  862. }
  863. this.goto_url('get', qstring+'&_download=1', false, true);
  864. break;
  865. case 'select-all':
  866. this.select_all_mode = props ? false : true;
  867. this.dummy_select = true; // prevent msg opening if there's only one msg on the list
  868. if (props == 'invert')
  869. this.message_list.invert_selection();
  870. else
  871. this.message_list.select_all(props == 'page' ? '' : props);
  872. this.dummy_select = null;
  873. break;
  874. case 'select-none':
  875. this.select_all_mode = false;
  876. this.message_list.clear_selection();
  877. break;
  878. case 'expand-all':
  879. this.env.autoexpand_threads = 1;
  880. this.message_list.expand_all();
  881. break;
  882. case 'expand-unread':
  883. this.env.autoexpand_threads = 2;
  884. this.message_list.collapse_all();
  885. this.expand_unread();
  886. break;
  887. case 'collapse-all':
  888. this.env.autoexpand_threads = 0;
  889. this.message_list.collapse_all();
  890. break;
  891. case 'nextmessage':
  892. if (this.env.next_uid)
  893. this.show_message(this.env.next_uid, false, this.env.action == 'preview');
  894. break;
  895. case 'lastmessage':
  896. if (this.env.last_uid)
  897. this.show_message(this.env.last_uid);
  898. break;
  899. case 'previousmessage':
  900. if (this.env.prev_uid)
  901. this.show_message(this.env.prev_uid, false, this.env.action == 'preview');
  902. break;
  903. case 'firstmessage':
  904. if (this.env.first_uid)
  905. this.show_message(this.env.first_uid);
  906. break;
  907. case 'compose':
  908. url = {};
  909. if (this.task == 'mail') {
  910. url = {_mbox: this.env.mailbox, _search: this.env.search_request};
  911. if (props)
  912. url._to = props;
  913. }
  914. // modify url if we're in addressbook
  915. else if (this.task == 'addressbook') {
  916. // switch to mail compose step directly
  917. if (props && props.indexOf('@') > 0) {
  918. url._to = props;
  919. }
  920. else {
  921. var a_cids = [];
  922. // use contact id passed as command parameter
  923. if (props)
  924. a_cids.push(props);
  925. // get selected contacts
  926. else if (this.contact_list)
  927. a_cids = this.contact_list.get_selection();
  928. if (a_cids.length)
  929. this.http_post('mailto', { _cid: a_cids.join(','), _source: this.env.source }, true);
  930. else if (this.env.group)
  931. this.http_post('mailto', { _gid: this.env.group, _source: this.env.source }, true);
  932. break;
  933. }
  934. }
  935. else if (props && typeof props == 'string') {
  936. url._to = props;
  937. }
  938. else if (props && typeof props == 'object') {
  939. $.extend(url, props);
  940. }
  941. this.open_compose_step(url);
  942. break;
  943. case 'spellcheck':
  944. if (this.spellcheck_state()) {
  945. this.editor.spellcheck_stop();
  946. }
  947. else {
  948. this.editor.spellcheck_start();
  949. }
  950. break;
  951. case 'savedraft':
  952. // Reset the auto-save timer
  953. clearTimeout(this.save_timer);
  954. // compose form did not change (and draft wasn't saved already)
  955. if (this.env.draft_id && this.cmp_hash == this.compose_field_hash()) {
  956. this.auto_save_start();
  957. break;
  958. }
  959. this.submit_messageform(true);
  960. break;
  961. case 'send':
  962. if (!props.nocheck && !this.env.is_sent && !this.check_compose_input(command))
  963. break;
  964. // Reset the auto-save timer
  965. clearTimeout(this.save_timer);
  966. this.submit_messageform();
  967. break;
  968. case 'send-attachment':
  969. // Reset the auto-save timer
  970. clearTimeout(this.save_timer);
  971. if (!(flag = this.upload_file(props || this.gui_objects.uploadform, 'upload'))) {
  972. if (flag !== false)
  973. alert(this.get_label('selectimportfile'));
  974. aborted = true;
  975. }
  976. break;
  977. case 'insert-sig':
  978. this.change_identity($("[name='_from']")[0], true);
  979. break;
  980. case 'list-addresses':
  981. this.list_contacts(props);
  982. this.enable_command('add-recipient', false);
  983. break;
  984. case 'add-recipient':
  985. this.compose_add_recipient(props);
  986. break;
  987. case 'reply-all':
  988. case 'reply-list':
  989. case 'reply':
  990. if (uid = this.get_single_uid()) {
  991. url = {_reply_uid: uid, _mbox: this.get_message_mailbox(uid), _search: this.env.search_request};
  992. if (command == 'reply-all')
  993. // do reply-list, when list is detected and popup menu wasn't used
  994. url._all = (!props && this.env.reply_all_mode == 1 && this.commands['reply-list'] ? 'list' : 'all');
  995. else if (command == 'reply-list')
  996. url._all = 'list';
  997. this.open_compose_step(url);
  998. }
  999. break;
  1000. case 'forward-attachment':
  1001. case 'forward-inline':
  1002. case 'forward':
  1003. var uids = this.env.uid ? [this.env.uid] : (this.message_list ? this.message_list.get_selection() : []);
  1004. if (uids.length) {
  1005. url = { _forward_uid: this.uids_to_list(uids), _mbox: this.env.mailbox, _search: this.env.search_request };
  1006. if (command == 'forward-attachment' || (!props && this.env.forward_attachment) || uids.length > 1)
  1007. url._attachment = 1;
  1008. this.open_compose_step(url);
  1009. }
  1010. break;
  1011. case 'print':
  1012. if (this.task == 'addressbook') {
  1013. if (uid = this.contact_list.get_single_selection()) {
  1014. url = '&_action=print&_cid=' + uid;
  1015. if (this.env.source)
  1016. url += '&_source=' + urlencode(this.env.source);
  1017. this.open_window(this.env.comm_path + url, true, true);
  1018. }
  1019. }
  1020. else if (this.env.action == 'get') {
  1021. this.gui_objects.messagepartframe.contentWindow.print();
  1022. }
  1023. else if (uid = this.get_single_uid()) {
  1024. url = this.url('print', this.params_from_uid(uid, {_safe: this.env.safemode ? 1 : 0}));
  1025. if (this.open_window(url, true, true)) {
  1026. if (this.env.action != 'show')
  1027. this.mark_message('read', uid);
  1028. }
  1029. }
  1030. break;
  1031. case 'viewsource':
  1032. if (uid = this.get_single_uid())
  1033. this.open_window(this.url('viewsource', this.params_from_uid(uid)), true, true);
  1034. break;
  1035. case 'download':
  1036. if (this.env.action == 'get') {
  1037. location.href = this.secure_url(location.href.replace(/_frame=/, '_download='));
  1038. }
  1039. else if (uid = this.get_single_uid()) {
  1040. this.goto_url('viewsource', this.params_from_uid(uid, {_save: 1}), false, true);
  1041. }
  1042. break;
  1043. // quicksearch
  1044. case 'search':
  1045. if (!props && this.gui_objects.qsearchbox)
  1046. props = this.gui_objects.qsearchbox.value;
  1047. if (props) {
  1048. this.qsearch(props);
  1049. break;
  1050. }
  1051. // reset quicksearch
  1052. case 'reset-search':
  1053. var n, s = this.env.search_request || this.env.qsearch;
  1054. this.reset_qsearch(true);
  1055. if (s && this.env.action == 'compose') {
  1056. if (this.contact_list)
  1057. this.list_contacts_clear();
  1058. }
  1059. else if (s && this.env.mailbox) {
  1060. this.list_mailbox(this.env.mailbox, 1);
  1061. }
  1062. else if (s && this.task == 'addressbook') {
  1063. if (this.env.source == '') {
  1064. for (n in this.env.address_sources) break;
  1065. this.env.source = n;
  1066. this.env.group = '';
  1067. }
  1068. this.list_contacts(this.env.source, this.env.group, 1);
  1069. }
  1070. break;
  1071. case 'pushgroup':
  1072. // add group ID to stack
  1073. this.env.address_group_stack.push(props.id);
  1074. if (obj && event)
  1075. rcube_event.cancel(event);
  1076. case 'listgroup':
  1077. this.reset_qsearch();
  1078. this.list_contacts(props.source, props.id);
  1079. break;
  1080. case 'popgroup':
  1081. if (this.env.address_group_stack.length > 1) {
  1082. this.env.address_group_stack.pop();
  1083. this.reset_qsearch();
  1084. this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]);
  1085. }
  1086. break;
  1087. case 'import-messages':
  1088. var form = props || this.gui_objects.importform,
  1089. importlock = this.set_busy(true, 'importwait');
  1090. $('input[name="_unlock"]', form).val(importlock);
  1091. if (!(flag = this.upload_file(form, 'import', importlock))) {
  1092. this.set_busy(false, null, importlock);
  1093. if (flag !== false)
  1094. alert(this.get_label('selectimportfile'));
  1095. aborted = true;
  1096. }
  1097. break;
  1098. case 'import':
  1099. if (this.env.action == 'import' && this.gui_objects.importform) {
  1100. var file = document.getElementById('rcmimportfile');
  1101. if (file && !file.value) {
  1102. alert(this.get_label('selectimportfile'));
  1103. aborted = true;
  1104. break;
  1105. }
  1106. this.gui_objects.importform.submit();
  1107. this.set_busy(true, 'importwait');
  1108. this.lock_form(this.gui_objects.importform, true);
  1109. }
  1110. else
  1111. this.goto_url('import', (this.env.source ? '_target='+urlencode(this.env.source)+'&' : ''));
  1112. break;
  1113. case 'export':
  1114. if (this.contact_list.rowcount > 0) {
  1115. this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _search: this.env.search_request }, false, true);
  1116. }
  1117. break;
  1118. case 'export-selected':
  1119. if (this.contact_list.rowcount > 0) {
  1120. this.goto_url('export', { _source: this.env.source, _gid: this.env.group, _cid: this.contact_list.get_selection().join(',') }, false, true);
  1121. }
  1122. break;
  1123. case 'upload-photo':
  1124. this.upload_contact_photo(props || this.gui_objects.uploadform);
  1125. break;
  1126. case 'delete-photo':
  1127. this.replace_contact_photo('-del-');
  1128. break;
  1129. // user settings commands
  1130. case 'preferences':
  1131. case 'identities':
  1132. case 'responses':
  1133. case 'folders':
  1134. this.goto_url('settings/' + command);
  1135. break;
  1136. case 'undo':
  1137. this.http_request('undo', '', this.display_message('', 'loading'));
  1138. break;
  1139. // unified command call (command name == function name)
  1140. default:
  1141. var func = command.replace(/-/g, '_');
  1142. if (this[func] && typeof this[func] === 'function') {
  1143. ret = this[func](props, obj, event);
  1144. }
  1145. break;
  1146. }
  1147. if (!aborted && this.triggerEvent('after'+command, props) === false)
  1148. ret = false;
  1149. this.triggerEvent('actionafter', { props:props, action:command, aborted:aborted, ret:ret });
  1150. return ret === false ? false : obj ? false : true;
  1151. };
  1152. // set command(s) enabled or disabled
  1153. this.enable_command = function()
  1154. {
  1155. var i, n, args = Array.prototype.slice.call(arguments),
  1156. enable = args.pop(), cmd;
  1157. for (n=0; n<args.length; n++) {
  1158. cmd = args[n];
  1159. // argument of type array
  1160. if (typeof cmd === 'string') {
  1161. this.commands[cmd] = enable;
  1162. this.set_button(cmd, (enable ? 'act' : 'pas'));
  1163. this.triggerEvent('enable-command', {command: cmd, status: enable});
  1164. }
  1165. // push array elements into commands array
  1166. else {
  1167. for (i in cmd)
  1168. args.push(cmd[i]);
  1169. }
  1170. }
  1171. };
  1172. this.command_enabled = function(cmd)
  1173. {
  1174. return this.commands[cmd];
  1175. };
  1176. // lock/unlock interface
  1177. this.set_busy = function(a, message, id)
  1178. {
  1179. if (a && message) {
  1180. var msg = this.get_label(message);
  1181. if (msg == message)
  1182. msg = 'Loading...';
  1183. id = this.display_message(msg, 'loading');
  1184. }
  1185. else if (!a && id) {
  1186. this.hide_message(id);
  1187. }
  1188. this.busy = a;
  1189. //document.body.style.cursor = a ? 'wait' : 'default';
  1190. if (this.gui_objects.editform)
  1191. this.lock_form(this.gui_objects.editform, a);
  1192. return id;
  1193. };
  1194. // return a localized string
  1195. this.get_label = function(name, domain)
  1196. {
  1197. if (domain && this.labels[domain+'.'+name])
  1198. return this.labels[domain+'.'+name];
  1199. else if (this.labels[name])
  1200. return this.labels[name];
  1201. else
  1202. return name;
  1203. };
  1204. // alias for convenience reasons
  1205. this.gettext = this.get_label;
  1206. // switch to another application task
  1207. this.switch_task = function(task)
  1208. {
  1209. if (this.task === task && task != 'mail')
  1210. return;
  1211. var url = this.get_task_url(task);
  1212. if (task == 'mail')
  1213. url += '&_mbox=INBOX';
  1214. else if (task == 'logout' && !this.env.server_error) {
  1215. url = this.secure_url(url);
  1216. this.clear_compose_data();
  1217. }
  1218. this.redirect(url);
  1219. };
  1220. this.get_task_url = function(task, url)
  1221. {
  1222. if (!url)
  1223. url = this.env.comm_path;
  1224. if (url.match(/[?&]_task=[a-zA-Z0-9_-]+/))
  1225. return url.replace(/_task=[a-zA-Z0-9_-]+/, '_task=' + task);
  1226. else
  1227. return url.replace(/\?.*$/, '') + '?_task=' + task;
  1228. };
  1229. this.reload = function(delay)
  1230. {
  1231. if (this.is_framed())
  1232. parent.rcmail.reload(delay);
  1233. else if (delay)
  1234. setTimeout(function() { ref.reload(); }, delay);
  1235. else if (window.location)
  1236. location.href = this.url('', {_extwin: this.env.extwin});
  1237. };
  1238. // Add variable to GET string, replace old value if exists
  1239. this.add_url = function(url, name, value)
  1240. {
  1241. value = urlencode(value);
  1242. if (/(\?.*)$/.test(url)) {
  1243. var urldata = RegExp.$1,
  1244. datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
  1245. if (datax.test(urldata)) {
  1246. urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
  1247. }
  1248. else
  1249. urldata += '&' + name + '=' + value
  1250. return url.replace(/(\?.*)$/, urldata);
  1251. }
  1252. return url + '?' + name + '=' + value;
  1253. };
  1254. // append CSRF protection token to the given url
  1255. this.secure_url = function(url)
  1256. {
  1257. return this.add_url(url, '_token', this.env.request_token);
  1258. },
  1259. this.is_framed = function()
  1260. {
  1261. return this.env.framed && parent.rcmail && parent.rcmail != this && typeof parent.rcmail.command == 'function';
  1262. };
  1263. this.save_pref = function(prop)
  1264. {
  1265. var request = {_name: prop.name, _value: prop.value};
  1266. if (prop.session)
  1267. request._session = prop.session;
  1268. if (prop.env)
  1269. this.env[prop.env] = prop.value;
  1270. this.http_post('save-pref', request);
  1271. };
  1272. this.html_identifier = function(str, encode)
  1273. {
  1274. return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_');
  1275. };
  1276. this.html_identifier_encode = function(str)
  1277. {
  1278. return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
  1279. };
  1280. this.html_identifier_decode = function(str)
  1281. {
  1282. str = String(str).replace(/-/g, '+').replace(/_/g, '/');
  1283. while (str.length % 4) str += '=';
  1284. return Base64.decode(str);
  1285. };
  1286. /*********************************************************/
  1287. /********* event handling methods *********/
  1288. /*********************************************************/
  1289. this.drag_menu = function(e, target)
  1290. {
  1291. var modkey = rcube_event.get_modifier(e),
  1292. menu = this.gui_objects.dragmenu;
  1293. if (menu && modkey == SHIFT_KEY && this.commands['copy']) {
  1294. var pos = rcube_event.get_mouse_pos(e);
  1295. this.env.drag_target = target;
  1296. this.show_menu(this.gui_objects.dragmenu.id, true, e);
  1297. $(menu).css({top: (pos.y-10)+'px', left: (pos.x-10)+'px'});
  1298. return true;
  1299. }
  1300. return false;
  1301. };
  1302. this.drag_menu_action = function(action)
  1303. {
  1304. var menu = this.gui_objects.dragmenu;
  1305. if (menu) {
  1306. $(menu).hide();
  1307. }
  1308. this.command(action, this.env.drag_target);
  1309. this.env.drag_target = null;
  1310. };
  1311. this.drag_start = function(list)
  1312. {
  1313. this.drag_active = true;
  1314. if (this.preview_timer)
  1315. clearTimeout(this.preview_timer);
  1316. if (this.preview_read_timer)
  1317. clearTimeout(this.preview_read_timer);
  1318. // prepare treelist widget for dragging interactions
  1319. if (this.treelist)
  1320. this.treelist.drag_start();
  1321. };
  1322. this.drag_end = function(e)
  1323. {
  1324. var list, model;
  1325. if (this.treelist)
  1326. this.treelist.drag_end();
  1327. // execute drag & drop action when mouse was released
  1328. if (list = this.message_list)
  1329. model = this.env.mailboxes;
  1330. else if (list = this.contact_list)
  1331. model = this.env.contactfolders;
  1332. if (this.drag_active && model && this.env.last_folder_target) {
  1333. var target = model[this.env.last_folder_target];
  1334. list.draglayer.hide();
  1335. if (this.contact_list) {
  1336. if (!this.contacts_drag_menu(e, target))
  1337. this.command('move', target);
  1338. }
  1339. else if (!this.drag_menu(e, target))
  1340. this.command('move', target);
  1341. }
  1342. this.drag_active = false;
  1343. this.env.last_folder_target = null;
  1344. };
  1345. this.drag_move = function(e)
  1346. {
  1347. if (this.gui_objects.folderlist) {
  1348. var drag_target, oldclass,
  1349. layerclass = 'draglayernormal',
  1350. mouse = rcube_event.get_mouse_pos(e);
  1351. if (this.contact_list && this.contact_list.draglayer)
  1352. oldclass = this.contact_list.draglayer.attr('class');
  1353. // mouse intersects a valid drop target on the treelist
  1354. if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) {
  1355. this.env.last_folder_target = drag_target;
  1356. layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal');
  1357. }
  1358. else {
  1359. // Clear target, otherwise drag end will trigger move into last valid droptarget
  1360. this.env.last_folder_target = null;
  1361. }
  1362. if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer)
  1363. this.contact_list.draglayer.attr('class', layerclass);
  1364. }
  1365. };
  1366. this.collapse_folder = function(name)
  1367. {
  1368. if (this.treelist)
  1369. this.treelist.toggle(name);
  1370. };
  1371. this.folder_collapsed = function(node)
  1372. {
  1373. var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders',
  1374. old = this.env[prefname];
  1375. if (node.collapsed) {
  1376. this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&';
  1377. // select the folder if one of its childs is currently selected
  1378. // don't select if it's virtual (#1488346)
  1379. if (!node.virtual && this.env.mailbox && this.env.mailbox.startsWith(node.id + this.env.delimiter))
  1380. this.command('list', node.id);
  1381. }
  1382. else {
  1383. var reg = new RegExp('&'+urlencode(node.id)+'&');
  1384. this.env[prefname] = this.env[prefname].replace(reg, '');
  1385. }
  1386. if (!this.drag_active) {
  1387. if (old !== this.env[prefname])
  1388. this.command('save-pref', { name: prefname, value: this.env[prefname] });
  1389. if (this.env.unread_counts)
  1390. this.set_unread_count_display(node.id, false);
  1391. }
  1392. };
  1393. // global mouse-click handler to cleanup some UI elements
  1394. this.doc_mouse_up = function(e)
  1395. {
  1396. var list, id, target = rcube_event.get_target(e);
  1397. // ignore event if jquery UI dialog is open
  1398. if ($(target).closest('.ui-dialog, .ui-widget-overlay').length)
  1399. return;
  1400. // remove focus from list widgets
  1401. if (window.rcube_list_widget && rcube_list_widget._instances.length) {
  1402. $.each(rcube_list_widget._instances, function(i,list){
  1403. if (list && !rcube_mouse_is_over(e, list.list.parentNode))
  1404. list.blur();
  1405. });
  1406. }
  1407. // reset 'pressed' buttons
  1408. if (this.buttons_sel) {
  1409. for (id in this.buttons_sel)
  1410. if (typeof id !== 'function')
  1411. this.button_out(this.buttons_sel[id], id);
  1412. this.buttons_sel = {};
  1413. }
  1414. // reset popup menus; delayed to have updated menu_stack data
  1415. setTimeout(function(e){
  1416. var obj, skip, config, id, i, parents = $(target).parents();
  1417. for (i = ref.menu_stack.length - 1; i >= 0; i--) {
  1418. id = ref.menu_stack[i];
  1419. obj = $('#' + id);
  1420. if (obj.is(':visible')
  1421. && target != obj.data('opener')
  1422. && target != obj.get(0) // check if scroll bar was clicked (#1489832)
  1423. && !parents.is(obj.data('opener'))
  1424. && id != skip
  1425. && (obj.attr('data-editable') != 'true' || !$(target).parents('#' + id).length)
  1426. && (obj.attr('data-sticky') != 'true' || !rcube_mouse_is_over(e, obj.get(0)))
  1427. ) {
  1428. ref.hide_menu(id, e);
  1429. }
  1430. skip = obj.data('parent');
  1431. }
  1432. }, 10, e);
  1433. };
  1434. // global keypress event handler
  1435. this.doc_keypress = function(e)
  1436. {
  1437. // Helper method to move focus to the next/prev active menu item
  1438. var focus_menu_item = function(dir) {
  1439. var obj, item, mod = dir < 0 ? 'prevAll' : 'nextAll', limit = dir < 0 ? 'last' : 'first';
  1440. if (ref.focused_menu && (obj = $('#'+ref.focused_menu))) {
  1441. item = obj.find(':focus').closest('li')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
  1442. if (!item.length)
  1443. item = obj.find(':focus').closest('ul')[mod](':has(:not([aria-disabled=true]))').find('a,input')[limit]();
  1444. return item.focus().length;
  1445. }
  1446. return 0;
  1447. };
  1448. var target = e.target || {},
  1449. keyCode = rcube_event.get_keycode(e);
  1450. // save global reference for keyboard detection on click events in IE
  1451. rcube_event._last_keyboard_event = e;
  1452. if (e.keyCode != 27 && (!this.menu_keyboard_active || target.nodeName == 'TEXTAREA' || target.nodeName == 'SELECT')) {
  1453. return true;
  1454. }
  1455. switch (keyCode) {
  1456. case 38:
  1457. case 40:
  1458. case 63232: // "up", in safari keypress
  1459. case 63233: // "down", in safari keypress
  1460. focus_menu_item(keyCode == 38 || keyCode == 63232 ? -1 : 1);
  1461. return rcube_event.cancel(e);
  1462. case 9: // tab
  1463. if (this.focused_menu) {
  1464. var mod = rcube_event.get_modifier(e);
  1465. if (!focus_menu_item(mod == SHIFT_KEY ? -1 : 1)) {
  1466. this.hide_menu(this.focused_menu, e);
  1467. }
  1468. }
  1469. return rcube_event.cancel(e);
  1470. case 27: // esc
  1471. if (this.menu_stack.length)
  1472. this.hide_menu(this.menu_stack[this.menu_stack.length-1], e);
  1473. break;
  1474. }
  1475. return true;
  1476. }
  1477. this.msglist_select = function(list)
  1478. {
  1479. if (this.preview_timer)
  1480. clearTimeout(this.preview_timer);
  1481. if (this.preview_read_timer)
  1482. clearTimeout(this.preview_read_timer);
  1483. var selected = list.get_single_selection();
  1484. this.enable_command(this.env.message_commands, selected != null);
  1485. if (selected) {
  1486. // Hide certain command buttons when Drafts folder is selected
  1487. if (this.env.mailbox == this.env.drafts_mailbox)
  1488. this.enable_command('reply', 'reply-all', 'reply-list', 'forward', 'forward-attachment', 'forward-inline', false);
  1489. // Disable reply-list when List-Post header is not set
  1490. else {
  1491. var msg = this.env.messages[selected];
  1492. if (!msg.ml)
  1493. this.enable_command('reply-list', false);
  1494. }
  1495. }
  1496. // Multi-message commands
  1497. this.enable_command('delete', 'move', 'copy', 'mark', 'forward', 'forward-attachment', list.selection.length > 0);
  1498. // reset all-pages-selection
  1499. if (selected || (list.selection.length && list.selection.length != list.rowcount))
  1500. this.select_all_mode = false;
  1501. // start timer for message preview (wait for double click)
  1502. if (selected && this.env.contentframe && !list.multi_selecting && !this.dummy_select)
  1503. this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
  1504. else if (this.env.contentframe)
  1505. this.show_contentframe(false);
  1506. };
  1507. // This allow as to re-select selected message and display it in preview frame
  1508. this.msglist_click = function(list)
  1509. {
  1510. if (list.multi_selecting || !this.env.contentframe)
  1511. return;
  1512. if (list.get_single_selection())
  1513. return;
  1514. var win = this.get_frame_window(this.env.contentframe);
  1515. if (win && win.location.href.indexOf(this.env.blankpage) >= 0) {
  1516. if (this.preview_timer)
  1517. clearTimeout(this.preview_timer);
  1518. if (this.preview_read_timer)
  1519. clearTimeout(this.preview_read_timer);
  1520. this.preview_timer = setTimeout(function() { ref.msglist_get_preview(); }, this.dblclick_time);
  1521. }
  1522. };
  1523. this.msglist_dbl_click = function(list)
  1524. {
  1525. if (this.preview_timer)
  1526. clearTimeout(this.preview_timer);
  1527. if (this.preview_read_timer)
  1528. clearTimeout(this.preview_read_timer);
  1529. var uid = list.get_single_selection();
  1530. if (uid && (this.env.messages[uid].mbox || this.env.mailbox) == this.env.drafts_mailbox)
  1531. this.open_compose_step({ _draft_uid: uid, _mbox: this.env.mailbox });
  1532. else if (uid)
  1533. this.show_message(uid, false, false);
  1534. };
  1535. this.msglist_keypress = function(list)
  1536. {
  1537. if (list.modkey == CONTROL_KEY)
  1538. return;
  1539. if (list.key_pressed == list.ENTER_KEY)
  1540. this.command('show');
  1541. else if (list.key_pressed == list.DELETE_KEY || list.key_pressed == list.BACKSPACE_KEY)
  1542. this.command('delete');
  1543. else if (list.key_pressed == 33)
  1544. this.command('previouspage');
  1545. else if (list.key_pressed == 34)
  1546. this.command('nextpage');
  1547. };
  1548. this.msglist_get_preview = function()
  1549. {
  1550. var uid = this.get_single_uid();
  1551. if (uid && this.env.contentframe && !this.drag_active)
  1552. this.show_message(uid, false, true);
  1553. else if (this.env.contentframe)
  1554. this.show_contentframe(false);
  1555. };
  1556. this.msglist_expand = function(row)
  1557. {
  1558. if (this.env.messages[row.uid])
  1559. this.env.messages[row.uid].expanded = row.expanded;
  1560. $(row.obj)[row.expanded?'addClass':'removeClass']('expanded');
  1561. };
  1562. this.msglist_set_coltypes = function(list)
  1563. {
  1564. var i, found, name, cols = list.thead.rows[0].cells;
  1565. this.env.listcols = [];
  1566. for (i=0; i<cols.length; i++)
  1567. if (cols[i].id && cols[i].id.startsWith('rcm')) {
  1568. name = cols[i].id.slice(3);
  1569. this.env.listcols.push(name);
  1570. }
  1571. if ((found = $.inArray('flag', this.env.listcols)) >= 0)
  1572. this.env.flagged_col = found;
  1573. if ((found = $.inArray('subject', this.env.listcols)) >= 0)
  1574. this.env.subject_col = found;
  1575. this.command('save-pref', { name: 'list_cols', value: this.env.listcols, session: 'list_attrib/columns' });
  1576. };
  1577. this.check_droptarget = function(id)
  1578. {
  1579. switch (this.task) {
  1580. case 'mail':
  1581. return (this.env.mailboxes[id]
  1582. && !this.env.mailboxes[id].virtual
  1583. && (this.env.mailboxes[id].id != this.env.mailbox || this.is_multifolder_listing())) ? 1 : 0;
  1584. case 'addressbook':
  1585. var target;
  1586. if (id != this.env.source && (target = this.env.contactfolders[id])) {
  1587. // droptarget is a group
  1588. if (target.type == 'group') {
  1589. if (target.id != this.env.group && !this.env.contactfolders[target.source].readonly) {
  1590. var is_other = this.env.selection_sources.length > 1 || $.inArray(target.source, this.env.selection_sources) == -1;
  1591. return !is_other || this.commands.move ? 1 : 2;
  1592. }
  1593. }
  1594. // droptarget is a (writable) addressbook and it's not the source
  1595. else if (!target.readonly && (this.env.selection_sources.length > 1 || $.inArray(id, this.env.selection_sources) == -1)) {
  1596. return this.commands.move ? 1 : 2;
  1597. }
  1598. }
  1599. }
  1600. return 0;
  1601. };
  1602. // open popup window
  1603. this.open_window = function(url, small, toolbar)
  1604. {
  1605. var wname = 'rcmextwin' + new Date().getTime();
  1606. url += (url.match(/\?/) ? '&' : '?') + '_extwin=1';
  1607. if (this.env.standard_windows)
  1608. var extwin = window.open(url, wname);
  1609. else {
  1610. var win = this.is_framed() ? parent.window : window,
  1611. page = $(win),
  1612. page_width = page.width(),
  1613. page_height = bw.mz ? $('body', win).height() : page.height(),
  1614. w = Math.min(small ? this.env.popup_width_small : this.env.popup_width, page_width),
  1615. h = page_height, // always use same height
  1616. l = (win.screenLeft || win.screenX) + 20,
  1617. t = (win.screenTop || win.screenY) + 20,
  1618. extwin = window.open(url, wname,
  1619. 'width='+w+',height='+h+',top='+t+',left='+l+',resizable=yes,location=no,scrollbars=yes'
  1620. +(toolbar ? ',toolbar=yes,menubar=yes,status=yes' : ',toolbar=no,menubar=no,status=no'));
  1621. }
  1622. // detect popup blocker (#1489618)
  1623. // don't care this might not work with all browsers
  1624. if (!extwin || extwin.closed) {
  1625. this.display_message(this.get_label('windowopenerror'), 'warning');
  1626. return;
  1627. }
  1628. // write loading... message to empty windows
  1629. if (!url && extwin.document) {
  1630. extwin.document.write('<html><body>' + this.get_label('loading') + '</body></html>');
  1631. }
  1632. // allow plugins to grab the window reference (#1489413)
  1633. this.triggerEvent('openwindow', { url:url, handle:extwin });
  1634. // focus window, delayed to bring to front
  1635. setTimeout(function() { extwin && extwin.focus(); }, 10);
  1636. return extwin;
  1637. };
  1638. /*********************************************************/
  1639. /********* (message) list functionality *********/
  1640. /*********************************************************/
  1641. this.init_message_row = function(row)
  1642. {
  1643. var i, fn = {}, uid = row.uid,
  1644. status_icon = (this.env.status_col != null ? 'status' : 'msg') + 'icn' + row.id;
  1645. if (uid && this.env.messages[uid])
  1646. $.extend(row, this.env.messages[uid]);
  1647. // set eventhandler to status icon
  1648. if (row.icon = document.getElementById(status_icon)) {
  1649. fn.icon = function(e) { ref.command('toggle_status', uid); };
  1650. }
  1651. // save message icon position too
  1652. if (this.env.status_col != null)
  1653. row.msgicon = document.getElementById('msgicn'+row.id);
  1654. else
  1655. row.msgicon = row.icon;
  1656. // set eventhandler to flag icon
  1657. if (this.env.flagged_col != null && (row.flagicon = document.getElementById('flagicn'+row.id))) {
  1658. fn.flagicon = function(e) { ref.command('toggle_flag', uid); };
  1659. }
  1660. // set event handler to thread expand/collapse icon
  1661. if (!row.depth && row.has_children && (row.expando = document.getElementById('rcmexpando'+row.id))) {
  1662. fn.expando = function(e) { ref.expand_message_row(e, uid); };
  1663. }
  1664. // attach events
  1665. $.each(fn, function(i, f) {
  1666. row[i].onclick = function(e) { f(e); return rcube_event.cancel(e); };
  1667. if (bw.touch && row[i].addEventListener) {
  1668. row[i].addEventListener('touchend', function(e) {
  1669. if (e.changedTouches.length == 1) {
  1670. f(e);
  1671. return rcube_event.cancel(e);
  1672. }
  1673. }, false);
  1674. }
  1675. });
  1676. this.triggerEvent('insertrow', { uid:uid, row:row });
  1677. };
  1678. // create a table row in the message list
  1679. this.add_message_row = function(uid, cols, flags, attop)
  1680. {
  1681. if (!this.gui_objects.messagelist || !this.message_list)
  1682. return false;
  1683. // Prevent from adding messages from different folder (#1487752)
  1684. if (flags.mbox != this.env.mailbox && !flags.skip_mbox_check)
  1685. return false;
  1686. if (!this.env.messages[uid])
  1687. this.env.messages[uid] = {};
  1688. // merge flags over local message object
  1689. $.extend(this.env.messages[uid], {
  1690. deleted: flags.deleted?1:0,
  1691. replied: flags.answered?1:0,
  1692. unread: !flags.seen?1:0,
  1693. forwarded: flags.forwarded?1:0,
  1694. flagged: flags.flagged?1:0,
  1695. has_children: flags.has_children?1:0,
  1696. depth: flags.depth?flags.depth:0,
  1697. unread_children: flags.unread_children?flags.unread_children:0,
  1698. parent_uid: flags.parent_uid?flags.parent_uid:0,
  1699. selected: this.select_all_mode || this.message_list.in_selection(uid),
  1700. ml: flags.ml?1:0,
  1701. ctype: flags.ctype,
  1702. mbox: flags.mbox,
  1703. // flags from plugins
  1704. flags: flags.extra_flags
  1705. });
  1706. var c, n, col, html, css_class, label, status_class = '', status_label = '',
  1707. tree = '', expando = '',
  1708. list = this.message_list,
  1709. rows = list.rows,
  1710. message = this.env.messages[uid],
  1711. msg_id = this.html_identifier(uid,true),
  1712. row_class = 'message'
  1713. + (!flags.seen ? ' unread' : '')
  1714. + (flags.deleted ? ' deleted' : '')
  1715. + (flags.flagged ? ' flagged' : '')
  1716. + (message.selected ? ' selected' : ''),
  1717. row = { cols:[], style:{}, id:'rcmrow'+msg_id, uid:uid };
  1718. // message status icons
  1719. css_class = 'msgicon';
  1720. if (this.env.status_col === null) {
  1721. css_class += ' status';
  1722. if (flags.deleted) {
  1723. status_class += ' deleted';
  1724. status_label += this.get_label('deleted') + ' ';
  1725. }
  1726. else if (!flags.seen) {
  1727. status_class += ' unread';
  1728. status_label += this.get_label('unread') + ' ';
  1729. }
  1730. else if (flags.unread_children > 0) {
  1731. status_class += ' unreadchildren';
  1732. }
  1733. }
  1734. if (flags.answered) {
  1735. status_class += ' replied';
  1736. status_label += this.get_label('replied') + ' ';
  1737. }
  1738. if (flags.forwarded) {
  1739. status_class += ' forwarded';
  1740. status_label += this.get_label('forwarded') + ' ';
  1741. }
  1742. // update selection
  1743. if (message.selected && !list.in_selection(uid))
  1744. list.selection.push(uid);
  1745. // threads
  1746. if (this.env.threading) {
  1747. if (message.depth) {
  1748. // This assumes that div width is hardcoded to 15px,
  1749. tree += '<span id="rcmtab' + msg_id + '" class="branch" style="width:' + (message.depth * 15) + 'px;">&nbsp;&nbsp;</span>';
  1750. if ((rows[message.parent_uid] && rows[message.parent_uid].expanded === false)
  1751. || ((this.env.autoexpand_threads == 0 || this.env.autoexpand_threads == 2) &&
  1752. (!rows[message.parent_uid] || !rows[message.parent_uid].expanded))
  1753. ) {
  1754. row.style.display = 'none';
  1755. message.expanded = false;
  1756. }
  1757. else
  1758. message.expanded = true;
  1759. row_class += ' thread expanded';
  1760. }
  1761. else if (message.has_children) {
  1762. if (message.expanded === undefined && (this.env.autoexpand_threads == 1 || (this.env.autoexpand_threads == 2 && message.unread_children))) {
  1763. message.expanded = true;
  1764. }
  1765. expando = '<div id="rcmexpando' + row.id + '" class="' + (message.expanded ? 'expanded' : 'collapsed') + '">&nbsp;&nbsp;</div>';
  1766. row_class += ' thread' + (message.expanded? ' expanded' : '');
  1767. }
  1768. if (flags.unread_children && flags.seen && !message.expanded)
  1769. row_class += ' unroot';
  1770. }
  1771. tree += '<span id="msgicn'+row.id+'" class="'+css_class+status_class+'" title="'+status_label+'"></span>';
  1772. row.className = row_class;
  1773. // build subject link
  1774. if (cols.subject) {
  1775. var action = flags.mbox == this.env.drafts_mailbox ? 'compose' : 'show',
  1776. uid_param = flags.mbox == this.env.drafts_mailbox ? '_draft_uid' : '_uid',
  1777. query = { _mbox: flags.mbox };
  1778. query[uid_param] = uid;
  1779. cols.subject = '<a href="' + this.url(action, query) + '" onclick="return rcube_event.keyboard_only(event)"' +
  1780. ' onmouseover="rcube_webmail.long_subject_title(this,'+(message.depth+1)+')" tabindex="-1"><span>'+cols.subject+'</span></a>';
  1781. }
  1782. // add each submitted col
  1783. for (n in this.env.listcols) {
  1784. c = this.env.listcols[n];
  1785. col = {className: String(c).toLowerCase(), events:{}};
  1786. if (this.env.coltypes[c] && this.env.coltypes[c].hidden) {
  1787. col.className += ' hidden';
  1788. }
  1789. if (c == 'flag') {
  1790. css_class = (flags.flagged ? 'flagged' : 'unflagged');
  1791. label = this.get_label(css_class);
  1792. html = '<span id="flagicn'+row.id+'" class="'+css_class+'" title="'+label+'"></span>';
  1793. }
  1794. else if (c == 'attachment') {
  1795. label = this.get_label('withattachment');
  1796. if (flags.attachmentClass)
  1797. html = '<span class="'+flags.attachmentClass+'" title="'+label+'"></span>';
  1798. else if (/application\/|multipart\/(m|signed)/.test(flags.ctype))
  1799. html = '<span class="attachment" title="'+label+'"></span>';
  1800. else if (/multipart\/report/.test(flags.ctype))
  1801. html = '<span class="report"></span>';
  1802. else
  1803. html = '&nbsp;';
  1804. }
  1805. else if (c == 'status') {
  1806. label = '';
  1807. if (flags.deleted) {
  1808. css_class = 'deleted';
  1809. label = this.get_label('deleted');
  1810. }
  1811. else if (!flags.seen) {
  1812. css_class = 'unread';
  1813. label = this.get_label('unread');
  1814. }
  1815. else if (flags.unread_children > 0) {
  1816. css_class = 'unreadchildren';
  1817. }
  1818. else
  1819. css_class = 'msgicon';
  1820. html = '<span id="statusicn'+row.id+'" class="'+css_class+status_class+'" title="'+label+'"></span>';
  1821. }
  1822. else if (c == 'threads')
  1823. html = expando;
  1824. else if (c == 'subject') {
  1825. if (bw.ie)
  1826. col.events.mouseover = function() { rcube_webmail.long_subject_title_ex(this); };
  1827. html = tree + cols[c];
  1828. }
  1829. else if (c == 'priority') {
  1830. if (flags.prio > 0 && flags.prio < 6) {
  1831. label = this.get_label('priority') + ' ' + flags.prio;
  1832. html = '<span class="prio'+flags.prio+'" title="'+label+'"></span>';
  1833. }
  1834. else
  1835. html = '&nbsp;';
  1836. }
  1837. else if (c == 'folder') {
  1838. html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>';
  1839. }
  1840. else
  1841. html = cols[c];
  1842. col.innerHTML = html;
  1843. row.cols.push(col);
  1844. }
  1845. list.insert_row(row, attop);
  1846. // remove 'old' row
  1847. if (attop && this.env.pagesize && list.rowcount > this.env.pagesize) {
  1848. var uid = list.get_last_row();
  1849. list.remove_row(uid);
  1850. list.clear_selection(uid);
  1851. }
  1852. };
  1853. this.set_list_sorting = function(sort_col, sort_order)
  1854. {
  1855. var sort_old = this.env.sort_col == 'arrival' ? 'date' : this.env.sort_col,
  1856. sort_new = sort_col == 'arrival' ? 'date' : sort_col;
  1857. // set table header class
  1858. $('#rcm' + sort_old).removeClass('sorted' + this.env.sort_order.toUpperCase());
  1859. if (sort_new)
  1860. $('#rcm' + sort_new).addClass('sorted' + sort_order);
  1861. // if sorting by 'arrival' is selected, click on date column should not switch to 'date'
  1862. $('#rcmdate > a').prop('rel', sort_col == 'arrival' ? 'arrival' : 'date');
  1863. this.env.sort_col = sort_col;
  1864. this.env.sort_order = sort_order;
  1865. };
  1866. this.set_list_options = function(cols, sort_col, sort_order, threads)
  1867. {
  1868. var update, post_data = {};
  1869. if (sort_col === undefined)
  1870. sort_col = this.env.sort_col;
  1871. if (!sort_order)
  1872. sort_order = this.env.sort_order;
  1873. if (this.env.sort_col != sort_col || this.env.sort_order != sort_order) {
  1874. update = 1;
  1875. this.set_list_sorting(sort_col, sort_order);
  1876. }
  1877. if (this.env.threading != threads) {
  1878. update = 1;
  1879. post_data._threads = threads;
  1880. }
  1881. if (cols && cols.length) {
  1882. // make sure new columns are added at the end of the list
  1883. var i, idx, name, newcols = [], oldcols = this.env.listcols;
  1884. for (i=0; i<oldcols.length; i++) {
  1885. name = oldcols[i];
  1886. idx = $.inArray(name, cols);
  1887. if (idx != -1) {
  1888. newcols.push(name);
  1889. delete cols[idx];
  1890. }
  1891. }
  1892. for (i=0; i<cols.length; i++)
  1893. if (cols[i])
  1894. newcols.push(cols[i]);
  1895. if (newcols.join() != oldcols.join()) {
  1896. update = 1;
  1897. post_data._cols = newcols.join(',');
  1898. }
  1899. }
  1900. if (update)
  1901. this.list_mailbox('', '', sort_col+'_'+sort_order, post_data);
  1902. };
  1903. // when user double-clicks on a row
  1904. this.show_message = function(id, safe, preview)
  1905. {
  1906. if (!id)
  1907. return;
  1908. var win, target = window,
  1909. url = this.params_from_uid(id, {_caps: this.browser_capabilities()});
  1910. if (preview && (win = this.get_frame_window(this.env.contentframe))) {
  1911. target = win;
  1912. url._framed = 1;
  1913. }
  1914. if (safe)
  1915. url._safe = 1;
  1916. // also send search request to get the right messages
  1917. if (this.env.search_request)
  1918. url._search = this.env.search_request;
  1919. if (this.env.extwin)
  1920. url._extwin = 1;
  1921. url = this.url(preview ? 'preview': 'show', url);
  1922. if (preview && String(target.location.href).indexOf(url) >= 0) {
  1923. this.show_contentframe(true);
  1924. }
  1925. else {
  1926. if (!preview && this.env.message_extwin && !this.env.extwin)
  1927. this.open_window(url, true);
  1928. else
  1929. this.location_href(url, target, true);
  1930. // mark as read and change mbox unread counter
  1931. if (preview && this.message_list && this.message_list.rows[id] && this.message_list.rows[id].unread && this.env.preview_pane_mark_read > 0) {
  1932. this.preview_read_timer = setTimeout(function() {
  1933. ref.set_unread_message(id, ref.env.mailbox);
  1934. ref.http_post('mark', {_uid: id, _flag: 'read', _mbox: ref.env.mailbox, _quiet: 1});
  1935. }, this.env.preview_pane_mark_read * 1000);
  1936. }
  1937. }
  1938. };
  1939. // update message status and unread counter after marking a message as read
  1940. this.set_unread_message = function(id, folder)
  1941. {
  1942. var self = this;
  1943. // find window with messages list
  1944. if (!self.message_list)
  1945. self = self.opener();
  1946. if (!self && window.parent)
  1947. self = parent.rcmail;
  1948. if (!self || !self.message_list)
  1949. return;
  1950. // this may fail in multifolder mode
  1951. if (self.set_message(id, 'unread', false) === false)
  1952. self.set_message(id + '-' + folder, 'unread', false);
  1953. if (self.env.unread_counts[folder] > 0) {
  1954. self.env.unread_counts[folder] -= 1;
  1955. self.set_unread_count(folder, self.env.unread_counts[folder], folder == 'INBOX' && !self.is_multifolder_listing());
  1956. }
  1957. };
  1958. this.show_contentframe = function(show)
  1959. {
  1960. var frame, win, name = this.env.contentframe;
  1961. if (name && (frame = this.get_frame_element(name))) {
  1962. if (!show && (win = this.get_frame_window(name))) {
  1963. if (win.location.href.indexOf(this.env.blankpage) < 0) {
  1964. if (win.stop)
  1965. win.stop();
  1966. else // IE
  1967. win.document.execCommand('Stop');
  1968. win.location.href = this.env.blankpage;
  1969. }
  1970. }
  1971. else if (!bw.safari && !bw.konq)
  1972. $(frame)[show ? 'show' : 'hide']();
  1973. }
  1974. if (!show && this.env.frame_lock)
  1975. this.set_busy(false, null, this.env.frame_lock);
  1976. };
  1977. this.get_frame_element = function(id)
  1978. {
  1979. var frame;
  1980. if (id && (frame = document.getElementById(id)))
  1981. return frame;
  1982. };
  1983. this.get_frame_window = function(id)
  1984. {
  1985. var frame = this.get_frame_element(id);
  1986. if (frame && frame.name && window.frames)
  1987. return window.frames[frame.name];
  1988. };
  1989. this.lock_frame = function()
  1990. {
  1991. if (!this.env.frame_lock)
  1992. (this.is_framed() ? parent.rcmail : this).env.frame_lock = this.set_busy(true, 'loading');
  1993. };
  1994. // list a specific page
  1995. this.list_page = function(page)
  1996. {
  1997. if (page == 'next')
  1998. page = this.env.current_page+1;
  1999. else if (page == 'last')
  2000. page = this.env.pagecount;
  2001. else if (page == 'prev' && this.env.current_page > 1)
  2002. page = this.env.current_page-1;
  2003. else if (page == 'first' && this.env.current_page > 1)
  2004. page = 1;
  2005. if (page > 0 && page <= this.env.pagecount) {
  2006. this.env.current_page = page;
  2007. if (this.task == 'addressbook' || this.contact_list)
  2008. this.list_contacts(this.env.source, this.env.group, page);
  2009. else if (this.task == 'mail')
  2010. this.list_mailbox(this.env.mailbox, page);
  2011. }
  2012. };
  2013. // sends request to check for recent messages
  2014. this.checkmail = function()
  2015. {
  2016. var lock = this.set_busy(true, 'checkingmail'),
  2017. params = this.check_recent_params();
  2018. this.http_post('check-recent', params, lock);
  2019. };
  2020. // list messages of a specific mailbox using filter
  2021. this.filter_mailbox = function(filter)
  2022. {
  2023. if (this.filter_disabled)
  2024. return;
  2025. var lock = this.set_busy(true, 'searching');
  2026. this.clear_message_list();
  2027. // reset vars
  2028. this.env.current_page = 1;
  2029. this.env.search_filter = filter;
  2030. this.http_request('search', this.search_params(false, filter), lock);
  2031. };
  2032. // reload the current message listing
  2033. this.refresh_list = function()
  2034. {
  2035. this.list_mailbox(this.env.mailbox, this.env.current_page || 1, null, { _clear:1 }, true);
  2036. if (this.message_list)
  2037. this.message_list.clear_selection();
  2038. };
  2039. // list messages of a specific mailbox
  2040. this.list_mailbox = function(mbox, page, sort, url, update_only)
  2041. {
  2042. var win, target = window;
  2043. if (typeof url != 'object')
  2044. url = {};
  2045. if (!mbox)
  2046. mbox = this.env.mailbox ? this.env.mailbox : 'INBOX';
  2047. // add sort to url if set
  2048. if (sort)
  2049. url._sort = sort;
  2050. // folder change, reset page, search scope, etc.
  2051. if (this.env.mailbox != mbox) {
  2052. page = 1;
  2053. this.env.current_page = page;
  2054. this.env.search_scope = 'base';
  2055. this.select_all_mode = false;
  2056. this.reset_search_filter();
  2057. }
  2058. // also send search request to get the right messages
  2059. else if (this.env.search_request)
  2060. url._search = this.env.search_request;
  2061. if (!update_only) {
  2062. // unselect selected messages and clear the list and message data
  2063. this.clear_message_list();
  2064. if (mbox != this.env.mailbox || (mbox == this.env.mailbox && !page && !sort))
  2065. url._refresh = 1;
  2066. this.select_folder(mbox, '', true);
  2067. this.unmark_folder(mbox, 'recent', '', true);
  2068. this.env.mailbox = mbox;
  2069. }
  2070. // load message list remotely
  2071. if (this.gui_objects.messagelist) {
  2072. this.list_mailbox_remote(mbox, page, url);
  2073. return;
  2074. }
  2075. if (win = this.get_frame_window(this.env.contentframe)) {
  2076. target = win;
  2077. url._framed = 1;
  2078. }
  2079. if (this.env.uid)
  2080. url._uid = this.env.uid;
  2081. // load message list to target frame/window
  2082. if (mbox) {
  2083. this.set_busy(true, 'loading');
  2084. url._mbox = mbox;
  2085. if (page)
  2086. url._page = page;
  2087. this.location_href(url, target);
  2088. }
  2089. };
  2090. this.clear_message_list = function()
  2091. {
  2092. this.env.messages = {};
  2093. this.show_contentframe(false);
  2094. if (this.message_list)
  2095. this.message_list.clear(true);
  2096. };
  2097. // send remote request to load message list
  2098. this.list_mailbox_remote = function(mbox, page, url)
  2099. {
  2100. var lock = this.set_busy(true, 'loading');
  2101. if (typeof url != 'object')
  2102. url = {};
  2103. url._mbox = mbox;
  2104. if (page)
  2105. url._page = page;
  2106. this.http_request('list', url, lock);
  2107. this.update_state({ _mbox: mbox, _page: (page && page > 1 ? page : null) });
  2108. };
  2109. // removes messages that doesn't exists from list selection array
  2110. this.update_selection = function()
  2111. {
  2112. var list = this.message_list,
  2113. selected = list.selection,
  2114. rows = list.rows,
  2115. i, selection = [];
  2116. for (i in selected)
  2117. if (rows[selected[i]])
  2118. selection.push(selected[i]);
  2119. list.selection = selection;
  2120. // reset preview frame, if currently previewed message is not selected (has been removed)
  2121. try {
  2122. var win = this.get_frame_window(this.env.contentframe),
  2123. id = win.rcmail.env.uid;
  2124. if (id && !list.in_selection(id))
  2125. this.show_contentframe(false);
  2126. }
  2127. catch (e) {};
  2128. };
  2129. // expand all threads with unread children
  2130. this.expand_unread = function()
  2131. {
  2132. var r, tbody = this.message_list.tbody,
  2133. new_row = tbody.firstChild;
  2134. while (new_row) {
  2135. if (new_row.nodeType == 1 && (r = this.message_list.rows[new_row.uid]) && r.unread_children) {
  2136. this.message_list.expand_all(r);
  2137. this.set_unread_children(r.uid);
  2138. }
  2139. new_row = new_row.nextSibling;
  2140. }
  2141. return false;
  2142. };
  2143. // thread expanding/collapsing handler
  2144. this.expand_message_row = function(e, uid)
  2145. {
  2146. var row = this.message_list.rows[uid];
  2147. // handle unread_children mark
  2148. row.expanded = !row.expanded;
  2149. this.set_unread_children(uid);
  2150. row.expanded = !row.expanded;
  2151. this.message_list.expand_row(e, uid);
  2152. };
  2153. // message list expanding
  2154. this.expand_threads = function()
  2155. {
  2156. if (!this.env.threading || !this.env.autoexpand_threads || !this.message_list)
  2157. return;
  2158. switch (this.env.autoexpand_threads) {
  2159. case 2: this.expand_unread(); break;
  2160. case 1: this.message_list.expand_all(); break;
  2161. }
  2162. };
  2163. // Initializes threads indicators/expanders after list update
  2164. this.init_threads = function(roots, mbox)
  2165. {
  2166. // #1487752
  2167. if (mbox && mbox != this.env.mailbox)
  2168. return false;
  2169. for (var n=0, len=roots.length; n<len; n++)
  2170. this.add_tree_icons(roots[n]);
  2171. this.expand_threads();
  2172. };
  2173. // adds threads tree icons to the list (or specified thread)
  2174. this.add_tree_icons = function(root)
  2175. {
  2176. var i, l, r, n, len, pos, tmp = [], uid = [],
  2177. row, rows = this.message_list.rows;
  2178. if (root)
  2179. row = rows[root] ? rows[root].obj : null;
  2180. else
  2181. row = this.message_list.tbody.firstChild;
  2182. while (row) {
  2183. if (row.nodeType == 1 && (r = rows[row.uid])) {
  2184. if (r.depth) {
  2185. for (i=tmp.length-1; i>=0; i--) {
  2186. len = tmp[i].length;
  2187. if (len > r.depth) {
  2188. pos = len - r.depth;
  2189. if (!(tmp[i][pos] & 2))
  2190. tmp[i][pos] = tmp[i][pos] ? tmp[i][pos]+2 : 2;
  2191. }
  2192. else if (len == r.depth) {
  2193. if (!(tmp[i][0] & 2))
  2194. tmp[i][0] += 2;
  2195. }
  2196. if (r.depth > len)
  2197. break;
  2198. }
  2199. tmp.push(new Array(r.depth));
  2200. tmp[tmp.length-1][0] = 1;
  2201. uid.push(r.uid);
  2202. }
  2203. else {
  2204. if (tmp.length) {
  2205. for (i in tmp) {
  2206. this.set_tree_icons(uid[i], tmp[i]);
  2207. }
  2208. tmp = [];
  2209. uid = [];
  2210. }
  2211. if (root && row != rows[root].obj)
  2212. break;
  2213. }
  2214. }
  2215. row = row.nextSibling;
  2216. }
  2217. if (tmp.length) {
  2218. for (i in tmp) {
  2219. this.set_tree_icons(uid[i], tmp[i]);
  2220. }
  2221. }
  2222. };
  2223. // adds tree icons to specified message row
  2224. this.set_tree_icons = function(uid, tree)
  2225. {
  2226. var i, divs = [], html = '', len = tree.length;
  2227. for (i=0; i<len; i++) {
  2228. if (tree[i] > 2)
  2229. divs.push({'class': 'l3', width: 15});
  2230. else if (tree[i] > 1)
  2231. divs.push({'class': 'l2', width: 15});
  2232. else if (tree[i] > 0)
  2233. divs.push({'class': 'l1', width: 15});
  2234. // separator div
  2235. else if (divs.length && !divs[divs.length-1]['class'])
  2236. divs[divs.length-1].width += 15;
  2237. else
  2238. divs.push({'class': null, width: 15});
  2239. }
  2240. for (i=divs.length-1; i>=0; i--) {
  2241. if (divs[i]['class'])
  2242. html += '<div class="tree '+divs[i]['class']+'" />';
  2243. else
  2244. html += '<div style="width:'+divs[i].width+'px" />';
  2245. }
  2246. if (html)
  2247. $('#rcmtab'+this.html_identifier(uid, true)).html(html);
  2248. };
  2249. // update parent in a thread
  2250. this.update_thread_root = function(uid, flag)
  2251. {
  2252. if (!this.env.threading)
  2253. return;
  2254. var root = this.message_list.find_root(uid);
  2255. if (uid == root)
  2256. return;
  2257. var p = this.message_list.rows[root];
  2258. if (flag == 'read' && p.unread_children) {
  2259. p.unread_children--;
  2260. }
  2261. else if (flag == 'unread' && p.has_children) {
  2262. // unread_children may be undefined
  2263. p.unread_children = p.unread_children ? p.unread_children + 1 : 1;
  2264. }
  2265. else {
  2266. return;
  2267. }
  2268. this.set_message_icon(root);
  2269. this.set_unread_children(root);
  2270. };
  2271. // update thread indicators for all messages in a thread below the specified message
  2272. // return number of removed/added root level messages
  2273. this.update_thread = function (uid)
  2274. {
  2275. if (!this.env.threading)
  2276. return 0;
  2277. var r, parent, count = 0,
  2278. rows = this.message_list.rows,
  2279. row = rows[uid],
  2280. depth = rows[uid].depth,
  2281. roots = [];
  2282. if (!row.depth) // root message: decrease roots count
  2283. count--;
  2284. else if (row.unread) {
  2285. // update unread_children for thread root
  2286. parent = this.message_list.find_root(uid);
  2287. rows[parent].unread_children--;
  2288. this.set_unread_children(parent);
  2289. }
  2290. parent = row.parent_uid;
  2291. // childrens
  2292. row = row.obj.nextSibling;
  2293. while (row) {
  2294. if (row.nodeType == 1 && (r = rows[row.uid])) {
  2295. if (!r.depth || r.depth <= depth)
  2296. break;
  2297. r.depth--; // move left
  2298. // reset width and clear the content of a tab, icons will be added later
  2299. $('#rcmtab'+r.id).width(r.depth * 15).html('');
  2300. if (!r.depth) { // a new root
  2301. count++; // increase roots count
  2302. r.parent_uid = 0;
  2303. if (r.has_children) {
  2304. // replace 'leaf' with 'collapsed'
  2305. $('#'+r.id+' .leaf:first')
  2306. .attr('id', 'rcmexpando' + r.id)
  2307. .attr('class', (r.obj.style.display != 'none' ? 'expanded' : 'collapsed'))
  2308. .mousedown({uid: r.uid}, function(e) {
  2309. return ref.expand_message_row(e, e.data.uid);
  2310. });
  2311. r.unread_children = 0;
  2312. roots.push(r);
  2313. }
  2314. // show if it was hidden
  2315. if (r.obj.style.display == 'none')
  2316. $(r.obj).show();
  2317. }
  2318. else {
  2319. if (r.depth == depth)
  2320. r.parent_uid = parent;
  2321. if (r.unread && roots.length)
  2322. roots[roots.length-1].unread_children++;
  2323. }
  2324. }
  2325. row = row.nextSibling;
  2326. }
  2327. // update unread_children for roots
  2328. for (r=0; r<roots.length; r++)
  2329. this.set_unread_children(roots[r].uid);
  2330. return count;
  2331. };
  2332. this.delete_excessive_thread_rows = function()
  2333. {
  2334. var rows = this.message_list.rows,
  2335. tbody = this.message_list.tbody,
  2336. row = tbody.firstChild,
  2337. cnt = this.env.pagesize + 1;
  2338. while (row) {
  2339. if (row.nodeType == 1 && (r = rows[row.uid])) {
  2340. if (!r.depth && cnt)
  2341. cnt--;
  2342. if (!cnt)
  2343. this.message_list.remove_row(row.uid);
  2344. }
  2345. row = row.nextSibling;
  2346. }
  2347. };
  2348. // set message icon
  2349. this.set_message_icon = function(uid)
  2350. {
  2351. var css_class, label = '',
  2352. row = this.message_list.rows[uid];
  2353. if (!row)
  2354. return false;
  2355. if (row.icon) {
  2356. css_class = 'msgicon';
  2357. if (row.deleted) {
  2358. css_class += ' deleted';
  2359. label += this.get_label('deleted') + ' ';
  2360. }
  2361. else if (row.unread) {
  2362. css_class += ' unread';
  2363. label += this.get_label('unread') + ' ';
  2364. }
  2365. else if (row.unread_children)
  2366. css_class += ' unreadchildren';
  2367. if (row.msgicon == row.icon) {
  2368. if (row.replied) {
  2369. css_class += ' replied';
  2370. label += this.get_label('replied') + ' ';
  2371. }
  2372. if (row.forwarded) {
  2373. css_class += ' forwarded';
  2374. label += this.get_label('forwarded') + ' ';
  2375. }
  2376. css_class += ' status';
  2377. }
  2378. $(row.icon).attr('class', css_class).attr('title', label);
  2379. }
  2380. if (row.msgicon && row.msgicon != row.icon) {
  2381. label = '';
  2382. css_class = 'msgicon';
  2383. if (!row.unread && row.unread_children) {
  2384. css_class += ' unreadchildren';
  2385. }
  2386. if (row.replied) {
  2387. css_class += ' replied';
  2388. label += this.get_label('replied') + ' ';
  2389. }
  2390. if (row.forwarded) {
  2391. css_class += ' forwarded';
  2392. label += this.get_label('forwarded') + ' ';
  2393. }
  2394. $(row.msgicon).attr('class', css_class).attr('title', label);
  2395. }
  2396. if (row.flagicon) {
  2397. css_class = (row.flagged ? 'flagged' : 'unflagged');
  2398. label = this.get_label(css_class);
  2399. $(row.flagicon).attr('class', css_class)
  2400. .attr('aria-label', label)
  2401. .attr('title', label);
  2402. }
  2403. };
  2404. // set message status
  2405. this.set_message_status = function(uid, flag, status)
  2406. {
  2407. var row = this.message_list.rows[uid];
  2408. if (!row)
  2409. return false;
  2410. if (flag == 'unread') {
  2411. if (row.unread != status)
  2412. this.update_thread_root(uid, status ? 'unread' : 'read');
  2413. }
  2414. if ($.inArray(flag, ['unread', 'deleted', 'replied', 'forwarded', 'flagged']) > -1)
  2415. row[flag] = status;
  2416. };
  2417. // set message row status, class and icon
  2418. this.set_message = function(uid, flag, status)
  2419. {
  2420. var row = this.message_list && this.message_list.rows[uid];
  2421. if (!row)
  2422. return false;
  2423. if (flag)
  2424. this.set_message_status(uid, flag, status);
  2425. if ($.inArray(flag, ['unread', 'deleted', 'flagged']) > -1)
  2426. $(row.obj)[row[flag] ? 'addClass' : 'removeClass'](flag);
  2427. this.set_unread_children(uid);
  2428. this.set_message_icon(uid);
  2429. };
  2430. // sets unroot (unread_children) class of parent row
  2431. this.set_unread_children = function(uid)
  2432. {
  2433. var row = this.message_list.rows[uid];
  2434. if (row.parent_uid)
  2435. return;
  2436. if (!row.unread && row.unread_children && !row.expanded)
  2437. $(row.obj).addClass('unroot');
  2438. else
  2439. $(row.obj).removeClass('unroot');
  2440. };
  2441. // copy selected messages to the specified mailbox
  2442. this.copy_messages = function(mbox, event)
  2443. {
  2444. if (mbox && typeof mbox === 'object')
  2445. mbox = mbox.id;
  2446. else if (!mbox)
  2447. return this.folder_selector(event, function(folder) { ref.command('copy', folder); });
  2448. // exit if current or no mailbox specified
  2449. if (!mbox || mbox == this.env.mailbox)
  2450. return;
  2451. var post_data = this.selection_post_data({_target_mbox: mbox});
  2452. // exit if selection is empty
  2453. if (!post_data._uid)
  2454. return;
  2455. // send request to server
  2456. this.http_post('copy', post_data, this.display_message(this.get_label('copyingmessage'), 'loading'));
  2457. };
  2458. // move selected messages to the specified mailbox
  2459. this.move_messages = function(mbox, event)
  2460. {
  2461. if (mbox && typeof mbox === 'object')
  2462. mbox = mbox.id;
  2463. else if (!mbox)
  2464. return this.folder_selector(event, function(folder) { ref.command('move', folder); });
  2465. // exit if current or no mailbox specified
  2466. if (!mbox || (mbox == this.env.mailbox && !this.is_multifolder_listing()))
  2467. return;
  2468. var lock = false, post_data = this.selection_post_data({_target_mbox: mbox});
  2469. // exit if selection is empty
  2470. if (!post_data._uid)
  2471. return;
  2472. // show wait message
  2473. if (this.env.action == 'show')
  2474. lock = this.set_busy(true, 'movingmessage');
  2475. else
  2476. this.show_contentframe(false);
  2477. // Hide message command buttons until a message is selected
  2478. this.enable_command(this.env.message_commands, false);
  2479. this._with_selected_messages('move', post_data, lock);
  2480. };
  2481. // delete selected messages from the current mailbox
  2482. this.delete_messages = function(event)
  2483. {
  2484. var list = this.message_list, trash = this.env.trash_mailbox;
  2485. // if config is set to flag for deletion
  2486. if (this.env.flag_for_deletion) {
  2487. this.mark_message('delete');
  2488. return false;
  2489. }
  2490. // if there isn't a defined trash mailbox or we are in it
  2491. else if (!trash || this.env.mailbox == trash)
  2492. this.permanently_remove_messages();
  2493. // we're in Junk folder and delete_junk is enabled
  2494. else if (this.env.delete_junk && this.env.junk_mailbox && this.env.mailbox == this.env.junk_mailbox)
  2495. this.permanently_remove_messages();
  2496. // if there is a trash mailbox defined and we're not currently in it
  2497. else {
  2498. // if shift was pressed delete it immediately
  2499. if ((list && list.modkey == SHIFT_KEY) || (event && rcube_event.get_modifier(event) == SHIFT_KEY)) {
  2500. if (confirm(this.get_label('deletemessagesconfirm')))
  2501. this.permanently_remove_messages();
  2502. }
  2503. else
  2504. this.move_messages(trash);
  2505. }
  2506. return true;
  2507. };
  2508. // delete the selected messages permanently
  2509. this.permanently_remove_messages = function()
  2510. {
  2511. var post_data = this.selection_post_data();
  2512. // exit if selection is empty
  2513. if (!post_data._uid)
  2514. return;
  2515. this.show_contentframe(false);
  2516. this._with_selected_messages('delete', post_data);
  2517. };
  2518. // Send a specific move/delete request with UIDs of all selected messages
  2519. // @private
  2520. this._with_selected_messages = function(action, post_data, lock)
  2521. {
  2522. var count = 0, msg,
  2523. remove = (action == 'delete' || !this.is_multifolder_listing());
  2524. // update the list (remove rows, clear selection)
  2525. if (this.message_list) {
  2526. var n, id, root, roots = [],
  2527. selection = this.message_list.get_selection();
  2528. for (n=0, len=selection.length; n<len; n++) {
  2529. id = selection[n];
  2530. if (this.env.threading) {
  2531. count += this.update_thread(id);
  2532. root = this.message_list.find_root(id);
  2533. if (root != id && $.inArray(root, roots) < 0) {
  2534. roots.push(root);
  2535. }
  2536. }
  2537. if (remove)
  2538. this.message_list.remove_row(id, (this.env.display_next && n == selection.length-1));
  2539. }
  2540. // make sure there are no selected rows
  2541. if (!this.env.display_next && remove)
  2542. this.message_list.clear_selection();
  2543. // update thread tree icons
  2544. for (n=0, len=roots.length; n<len; n++) {
  2545. this.add_tree_icons(roots[n]);
  2546. }
  2547. }
  2548. if (count < 0)
  2549. post_data._count = (count*-1);
  2550. // remove threads from the end of the list
  2551. else if (count > 0 && remove)
  2552. this.delete_excessive_thread_rows();
  2553. if (!remove)
  2554. post_data._refresh = 1;
  2555. if (!lock) {
  2556. msg = action == 'move' ? 'movingmessage' : 'deletingmessage';
  2557. lock = this.display_message(this.get_label(msg), 'loading');
  2558. }
  2559. // send request to server
  2560. this.http_post(action, post_data, lock);
  2561. };
  2562. // build post data for message delete/move/copy/flag requests
  2563. this.selection_post_data = function(data)
  2564. {
  2565. if (typeof(data) != 'object')
  2566. data = {};
  2567. data._mbox = this.env.mailbox;
  2568. if (!data._uid) {
  2569. var uids = this.env.uid ? [this.env.uid] : this.message_list.get_selection();
  2570. data._uid = this.uids_to_list(uids);
  2571. }
  2572. if (this.env.action)
  2573. data._from = this.env.action;
  2574. // also send search request to get the right messages
  2575. if (this.env.search_request)
  2576. data._search = this.env.search_request;
  2577. if (this.env.display_next && this.env.next_uid)
  2578. data._next_uid = this.env.next_uid;
  2579. return data;
  2580. };
  2581. // set a specific flag to one or more messages
  2582. this.mark_message = function(flag, uid)
  2583. {
  2584. var a_uids = [], r_uids = [], len, n, id,
  2585. list = this.message_list;
  2586. if (uid)
  2587. a_uids[0] = uid;
  2588. else if (this.env.uid)
  2589. a_uids[0] = this.env.uid;
  2590. else if (list)
  2591. a_uids = list.get_selection();
  2592. if (!list)
  2593. r_uids = a_uids;
  2594. else {
  2595. list.focus();
  2596. for (n=0, len=a_uids.length; n<len; n++) {
  2597. id = a_uids[n];
  2598. if ((flag == 'read' && list.rows[id].unread)
  2599. || (flag == 'unread' && !list.rows[id].unread)
  2600. || (flag == 'delete' && !list.rows[id].deleted)
  2601. || (flag == 'undelete' && list.rows[id].deleted)
  2602. || (flag == 'flagged' && !list.rows[id].flagged)
  2603. || (flag == 'unflagged' && list.rows[id].flagged))
  2604. {
  2605. r_uids.push(id);
  2606. }
  2607. }
  2608. }
  2609. // nothing to do
  2610. if (!r_uids.length && !this.select_all_mode)
  2611. return;
  2612. switch (flag) {
  2613. case 'read':
  2614. case 'unread':
  2615. this.toggle_read_status(flag, r_uids);
  2616. break;
  2617. case 'delete':
  2618. case 'undelete':
  2619. this.toggle_delete_status(r_uids);
  2620. break;
  2621. case 'flagged':
  2622. case 'unflagged':
  2623. this.toggle_flagged_status(flag, a_uids);
  2624. break;
  2625. }
  2626. };
  2627. // set class to read/unread
  2628. this.toggle_read_status = function(flag, a_uids)
  2629. {
  2630. var i, len = a_uids.length,
  2631. post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
  2632. lock = this.display_message(this.get_label('markingmessage'), 'loading');
  2633. // mark all message rows as read/unread
  2634. for (i=0; i<len; i++)
  2635. this.set_message(a_uids[i], 'unread', (flag == 'unread' ? true : false));
  2636. this.http_post('mark', post_data, lock);
  2637. };
  2638. // set image to flagged or unflagged
  2639. this.toggle_flagged_status = function(flag, a_uids)
  2640. {
  2641. var i, len = a_uids.length,
  2642. post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: flag}),
  2643. lock = this.display_message(this.get_label('markingmessage'), 'loading');
  2644. // mark all message rows as flagged/unflagged
  2645. for (i=0; i<len; i++)
  2646. this.set_message(a_uids[i], 'flagged', (flag == 'flagged' ? true : false));
  2647. this.http_post('mark', post_data, lock);
  2648. };
  2649. // mark all message rows as deleted/undeleted
  2650. this.toggle_delete_status = function(a_uids)
  2651. {
  2652. var len = a_uids.length,
  2653. i, uid, all_deleted = true,
  2654. rows = this.message_list ? this.message_list.rows : {};
  2655. if (len == 1) {
  2656. if (!this.message_list || (rows[a_uids[0]] && !rows[a_uids[0]].deleted))
  2657. this.flag_as_deleted(a_uids);
  2658. else
  2659. this.flag_as_undeleted(a_uids);
  2660. return true;
  2661. }
  2662. for (i=0; i<len; i++) {
  2663. uid = a_uids[i];
  2664. if (rows[uid] && !rows[uid].deleted) {
  2665. all_deleted = false;
  2666. break;
  2667. }
  2668. }
  2669. if (all_deleted)
  2670. this.flag_as_undeleted(a_uids);
  2671. else
  2672. this.flag_as_deleted(a_uids);
  2673. return true;
  2674. };
  2675. this.flag_as_undeleted = function(a_uids)
  2676. {
  2677. var i, len = a_uids.length,
  2678. post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'undelete'}),
  2679. lock = this.display_message(this.get_label('markingmessage'), 'loading');
  2680. for (i=0; i<len; i++)
  2681. this.set_message(a_uids[i], 'deleted', false);
  2682. this.http_post('mark', post_data, lock);
  2683. };
  2684. this.flag_as_deleted = function(a_uids)
  2685. {
  2686. var r_uids = [],
  2687. post_data = this.selection_post_data({_uid: this.uids_to_list(a_uids), _flag: 'delete'}),
  2688. lock = this.display_message(this.get_label('markingmessage'), 'loading'),
  2689. rows = this.message_list ? this.message_list.rows : {},
  2690. count = 0;
  2691. for (var i=0, len=a_uids.length; i<len; i++) {
  2692. uid = a_uids[i];
  2693. if (rows[uid]) {
  2694. if (rows[uid].unread)
  2695. r_uids[r_uids.length] = uid;
  2696. if (this.env.skip_deleted) {
  2697. count += this.update_thread(uid);
  2698. this.message_list.remove_row(uid, (this.env.display_next && i == this.message_list.selection.length-1));
  2699. }
  2700. else
  2701. this.set_message(uid, 'deleted', true);
  2702. }
  2703. }
  2704. // make sure there are no selected rows
  2705. if (this.env.skip_deleted && this.message_list) {
  2706. if (!this.env.display_next)
  2707. this.message_list.clear_selection();
  2708. if (count < 0)
  2709. post_data._count = (count*-1);
  2710. else if (count > 0)
  2711. // remove threads from the end of the list
  2712. this.delete_excessive_thread_rows();
  2713. }
  2714. // set of messages to mark as seen
  2715. if (r_uids.length)
  2716. post_data._ruid = this.uids_to_list(r_uids);
  2717. if (this.env.skip_deleted && this.env.display_next && this.env.next_uid)
  2718. post_data._next_uid = this.env.next_uid;
  2719. this.http_post('mark', post_data, lock);
  2720. };
  2721. // flag as read without mark request (called from backend)
  2722. // argument should be a coma-separated list of uids
  2723. this.flag_deleted_as_read = function(uids)
  2724. {
  2725. var uid, i, len,
  2726. rows = this.message_list ? this.message_list.rows : {};
  2727. if (typeof uids == 'string')
  2728. uids = uids.split(',');
  2729. for (i=0, len=uids.length; i<len; i++) {
  2730. uid = uids[i];
  2731. if (rows[uid])
  2732. this.set_message(uid, 'unread', false);
  2733. }
  2734. };
  2735. // Converts array of message UIDs to comma-separated list for use in URL
  2736. // with select_all mode checking
  2737. this.uids_to_list = function(uids)
  2738. {
  2739. return this.select_all_mode ? '*' : (uids.length <= 1 ? uids.join(',') : uids);
  2740. };
  2741. // Sets title of the delete button
  2742. this.set_button_titles = function()
  2743. {
  2744. var label = 'deletemessage';
  2745. if (!this.env.flag_for_deletion
  2746. && this.env.trash_mailbox && this.env.mailbox != this.env.trash_mailbox
  2747. && (!this.env.delete_junk || !this.env.junk_mailbox || this.env.mailbox != this.env.junk_mailbox)
  2748. )
  2749. label = 'movemessagetotrash';
  2750. this.set_alttext('delete', label);
  2751. };
  2752. // Initialize input element for list page jump
  2753. this.init_pagejumper = function(element)
  2754. {
  2755. $(element).addClass('rcpagejumper')
  2756. .on('focus', function(e) {
  2757. // create and display popup with page selection
  2758. var i, html = '';
  2759. for (i = 1; i <= ref.env.pagecount; i++)
  2760. html += '<li>' + i + '</li>';
  2761. html = '<ul class="toolbarmenu">' + html + '</ul>';
  2762. if (!ref.pagejump) {
  2763. ref.pagejump = $('<div id="pagejump-selector" class="popupmenu"></div>')
  2764. .appendTo(document.body)
  2765. .on('click', 'li', function() {
  2766. if (!ref.busy)
  2767. $(element).val($(this).text()).change();
  2768. });
  2769. }
  2770. if (ref.pagejump.data('count') != i)
  2771. ref.pagejump.html(html);
  2772. ref.pagejump.attr('rel', '#' + this.id).data('count', i);
  2773. // display page selector
  2774. ref.show_menu('pagejump-selector', true, e);
  2775. $(this).keydown();
  2776. })
  2777. // keyboard navigation
  2778. .on('keydown keyup click', function(e) {
  2779. var current, selector = $('#pagejump-selector'),
  2780. ul = $('ul', selector),
  2781. list = $('li', ul),
  2782. height = ul.height(),
  2783. p = parseInt(this.value);
  2784. if (e.which != 27 && e.which != 9 && e.which != 13 && !selector.is(':visible'))
  2785. return ref.show_menu('pagejump-selector', true, e);
  2786. if (e.type == 'keydown') {
  2787. // arrow-down
  2788. if (e.which == 40) {
  2789. if (list.length > p)
  2790. this.value = (p += 1);
  2791. }
  2792. // arrow-up
  2793. else if (e.which == 38) {
  2794. if (p > 1 && list.length > p - 1)
  2795. this.value = (p -= 1);
  2796. }
  2797. // enter
  2798. else if (e.which == 13) {
  2799. return $(this).change();
  2800. }
  2801. // esc, tab
  2802. else if (e.which == 27 || e.which == 9) {
  2803. return $(element).val(ref.env.current_page);
  2804. }
  2805. }
  2806. $('li.selected', ul).removeClass('selected');
  2807. if ((current = $(list[p - 1])).length) {
  2808. current.addClass('selected');
  2809. $('#pagejump-selector').scrollTop(((ul.height() / list.length) * (p - 1)) - selector.height() / 2);
  2810. }
  2811. })
  2812. .on('change', function(e) {
  2813. // go to specified page
  2814. var p = parseInt(this.value);
  2815. if (p && p != ref.env.current_page && !ref.busy) {
  2816. ref.hide_menu('pagejump-selector');
  2817. ref.list_page(p);
  2818. }
  2819. });
  2820. };
  2821. // Update page-jumper state on list updates
  2822. this.update_pagejumper = function()
  2823. {
  2824. $('input.rcpagejumper').val(this.env.current_page).prop('disabled', this.env.pagecount < 2);
  2825. };
  2826. // check for mailvelope API
  2827. this.check_mailvelope = function(action)
  2828. {
  2829. if (typeof window.mailvelope !== 'undefined') {
  2830. this.mailvelope_load(action);
  2831. }
  2832. else {
  2833. $(window).on('mailvelope', function() {
  2834. ref.mailvelope_load(action);
  2835. });
  2836. }
  2837. };
  2838. // Load Mailvelope functionality (and initialize keyring if needed)
  2839. this.mailvelope_load = function(action)
  2840. {
  2841. if (this.env.browser_capabilities)
  2842. this.env.browser_capabilities['pgpmime'] = 1;
  2843. var keyring = this.env.user_id;
  2844. mailvelope.getKeyring(keyring).then(function(kr) {
  2845. ref.mailvelope_keyring = kr;
  2846. ref.mailvelope_init(action, kr);
  2847. }, function(err) {
  2848. // attempt to create a new keyring for this app/user
  2849. mailvelope.createKeyring(keyring).then(function(kr) {
  2850. ref.mailvelope_keyring = kr;
  2851. ref.mailvelope_init(action, kr);
  2852. }, function(err) {
  2853. console.error(err);
  2854. });
  2855. });
  2856. };
  2857. // Initializes Mailvelope editor or display container
  2858. this.mailvelope_init = function(action, keyring)
  2859. {
  2860. if (!window.mailvelope)
  2861. return;
  2862. if (action == 'show' || action == 'preview' || action == 'print') {
  2863. // decrypt text body
  2864. if (this.env.is_pgp_content) {
  2865. var data = $(this.env.is_pgp_content).text();
  2866. ref.mailvelope_display_container(this.env.is_pgp_content, data, keyring);
  2867. }
  2868. // load pgp/mime message and pass it to the mailvelope display container
  2869. else if (this.env.pgp_mime_part) {
  2870. var msgid = this.display_message(this.get_label('loadingdata'), 'loading'),
  2871. selector = this.env.pgp_mime_container;
  2872. $.ajax({
  2873. type: 'GET',
  2874. url: this.url('get', { '_mbox': this.env.mailbox, '_uid': this.env.uid, '_part': this.env.pgp_mime_part }),
  2875. error: function(o, status, err) {
  2876. ref.http_error(o, status, err, msgid);
  2877. },
  2878. success: function(data) {
  2879. ref.mailvelope_display_container(selector, data, keyring, msgid);
  2880. }
  2881. });
  2882. }
  2883. }
  2884. else if (action == 'compose') {
  2885. this.env.compose_commands.push('compose-encrypted');
  2886. var is_html = $('input[name="_is_html"]').val() > 0;
  2887. if (this.env.pgp_mime_message) {
  2888. // fetch PGP/Mime part and open load into Mailvelope editor
  2889. var lock = this.set_busy(true, this.get_label('loadingdata'));
  2890. $.ajax({
  2891. type: 'GET',
  2892. url: this.url('get', this.env.pgp_mime_message),
  2893. error: function(o, status, err) {
  2894. ref.http_error(o, status, err, lock);
  2895. ref.enable_command('compose-encrypted', !is_html);
  2896. },
  2897. success: function(data) {
  2898. ref.set_busy(false, null, lock);
  2899. if (is_html) {
  2900. ref.command('toggle-editor', {html: false, noconvert: true});
  2901. $('#' + ref.env.composebody).val('');
  2902. }
  2903. ref.compose_encrypted({ quotedMail: data });
  2904. ref.enable_command('compose-encrypted', true);
  2905. }
  2906. });
  2907. }
  2908. else {
  2909. // enable encrypted compose toggle
  2910. this.enable_command('compose-encrypted', !is_html);
  2911. }
  2912. // make sure to disable encryption button after toggling editor into HTML mode
  2913. this.addEventListener('actionafter', function(args) {
  2914. if (args.ret && args.action == 'toggle-editor')
  2915. ref.enable_command('compose-encrypted', !args.props.html);
  2916. });
  2917. }
  2918. };
  2919. // handler for the 'compose-encrypted' command
  2920. this.compose_encrypted = function(props)
  2921. {
  2922. var options, container = $('#' + this.env.composebody).parent();
  2923. // remove Mailvelope editor if active
  2924. if (ref.mailvelope_editor) {
  2925. ref.mailvelope_editor = null;
  2926. ref.compose_skip_unsavedcheck = false;
  2927. ref.set_button('compose-encrypted', 'act');
  2928. container.removeClass('mailvelope')
  2929. .find('iframe:not([aria-hidden=true])').remove();
  2930. $('#' + ref.env.composebody).show();
  2931. $("[name='_pgpmime']").remove();
  2932. // disable commands that operate on the compose body
  2933. ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', true);
  2934. ref.triggerEvent('compose-encrypted', { active:false });
  2935. }
  2936. // embed Mailvelope editor container
  2937. else {
  2938. if (this.spellcheck_state())
  2939. this.editor.spellcheck_stop();
  2940. if (props.quotedMail) {
  2941. options = { quotedMail: props.quotedMail, quotedMailIndent: false };
  2942. }
  2943. else {
  2944. options = { predefinedText: $('#' + this.env.composebody).val() };
  2945. }
  2946. if (this.env.compose_mode == 'reply') {
  2947. options.quotedMailIndent = true;
  2948. options.quotedMailHeader = this.env.compose_reply_header;
  2949. }
  2950. mailvelope.createEditorContainer('#' + container.attr('id'), ref.mailvelope_keyring, options).then(function(editor) {
  2951. ref.mailvelope_editor = editor;
  2952. ref.compose_skip_unsavedcheck = true;
  2953. ref.set_button('compose-encrypted', 'sel');
  2954. container.addClass('mailvelope');
  2955. $('#' + ref.env.composebody).hide();
  2956. // disable commands that operate on the compose body
  2957. ref.enable_command('spellcheck', 'insert-sig', 'toggle-editor', 'insert-response', 'save-response', false);
  2958. ref.triggerEvent('compose-encrypted', { active:true });
  2959. // notify user about loosing attachments
  2960. if (ref.env.attachments && !$.isEmptyObject(ref.env.attachments)) {
  2961. alert(ref.get_label('encryptnoattachments'));
  2962. $.each(ref.env.attachments, function(name, attach) {
  2963. ref.remove_from_attachment_list(name);
  2964. });
  2965. }
  2966. }, function(err) {
  2967. console.error(err);
  2968. console.log(options);
  2969. });
  2970. }
  2971. };
  2972. // callback to replace the message body with the full armored
  2973. this.mailvelope_submit_messageform = function(draft, saveonly)
  2974. {
  2975. // get recipients
  2976. var recipients = [];
  2977. $.each(['to', 'cc', 'bcc'], function(i,field) {
  2978. var pos, rcpt, val = $.trim($('[name="_' + field + '"]').val());
  2979. while (val.length && rcube_check_email(val, true)) {
  2980. rcpt = RegExp.$2;
  2981. recipients.push(rcpt);
  2982. val = val.substr(val.indexOf(rcpt) + rcpt.length + 1).replace(/^\s*,\s*/, '');
  2983. }
  2984. });
  2985. // check if we have keys for all recipients
  2986. var isvalid = recipients.length > 0;
  2987. ref.mailvelope_keyring.validKeyForAddress(recipients).then(function(status) {
  2988. var missing_keys = [];
  2989. $.each(status, function(k,v) {
  2990. if (v === false) {
  2991. isvalid = false;
  2992. missing_keys.push(k);
  2993. }
  2994. });
  2995. // list recipients with missing keys
  2996. if (!isvalid && missing_keys.length) {
  2997. // display dialog with missing keys
  2998. ref.show_popup_dialog(
  2999. ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')) +
  3000. '<p>' + ref.get_label('searchpubkeyservers') + '</p>',
  3001. ref.get_label('encryptedsendialog'),
  3002. [{
  3003. text: ref.get_label('search'),
  3004. 'class': 'mainaction',
  3005. click: function() {
  3006. var $dialog = $(this);
  3007. ref.mailvelope_search_pubkeys(missing_keys, function() {
  3008. $dialog.dialog('close')
  3009. });
  3010. }
  3011. },
  3012. {
  3013. text: ref.get_label('cancel'),
  3014. click: function(){
  3015. $(this).dialog('close');
  3016. }
  3017. }]
  3018. );
  3019. return false;
  3020. }
  3021. if (!isvalid) {
  3022. if (!recipients.length) {
  3023. alert(ref.get_label('norecipientwarning'));
  3024. $("[name='_to']").focus();
  3025. }
  3026. return false;
  3027. }
  3028. // add sender identity to recipients to be able to decrypt our very own message
  3029. var senders = [], selected_sender = ref.env.identities[$("[name='_from'] option:selected").val()];
  3030. $.each(ref.env.identities, function(k, sender) {
  3031. senders.push(sender.email);
  3032. });
  3033. ref.mailvelope_keyring.validKeyForAddress(senders).then(function(status) {
  3034. valid_sender = null;
  3035. $.each(status, function(k,v) {
  3036. if (v !== false) {
  3037. valid_sender = k;
  3038. if (valid_sender == selected_sender) {
  3039. return false; // break
  3040. }
  3041. }
  3042. });
  3043. if (!valid_sender) {
  3044. if (!confirm(ref.get_label('nopubkeyforsender'))) {
  3045. return false;
  3046. }
  3047. }
  3048. recipients.push(valid_sender);
  3049. ref.mailvelope_editor.encrypt(recipients).then(function(armored) {
  3050. // all checks passed, send message
  3051. var form = ref.gui_objects.messageform,
  3052. hidden = $("[name='_pgpmime']", form),
  3053. msgid = ref.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage')
  3054. form.target = 'savetarget';
  3055. form._draft.value = draft ? '1' : '';
  3056. form.action = ref.add_url(form.action, '_unlock', msgid);
  3057. form.action = ref.add_url(form.action, '_framed', 1);
  3058. if (saveonly) {
  3059. form.action = ref.add_url(form.action, '_saveonly', 1);
  3060. }
  3061. // send pgp conent via hidden field
  3062. if (!hidden.length) {
  3063. hidden = $('<input type="hidden" name="_pgpmime">').appendTo(form);
  3064. }
  3065. hidden.val(armored);
  3066. form.submit();
  3067. }, function(err) {
  3068. console.log(err);
  3069. }); // mailvelope_editor.encrypt()
  3070. }, function(err) {
  3071. console.error(err);
  3072. }); // mailvelope_keyring.validKeyForAddress(senders)
  3073. }, function(err) {
  3074. console.error(err);
  3075. }); // mailvelope_keyring.validKeyForAddress(recipients)
  3076. return false;
  3077. };
  3078. // wrapper for the mailvelope.createDisplayContainer API call
  3079. this.mailvelope_display_container = function(selector, data, keyring, msgid)
  3080. {
  3081. mailvelope.createDisplayContainer(selector, data, keyring, { showExternalContent: this.env.safemode }).then(function() {
  3082. $(selector).addClass('mailvelope').children().not('iframe').hide();
  3083. ref.hide_message(msgid);
  3084. setTimeout(function() { $(window).resize(); }, 10);
  3085. }, function(err) {
  3086. console.error(err);
  3087. ref.hide_message(msgid);
  3088. ref.display_message('Message decryption failed: ' + err.message, 'error')
  3089. });
  3090. };
  3091. // subroutine to query keyservers for public keys
  3092. this.mailvelope_search_pubkeys = function(emails, resolve)
  3093. {
  3094. // query with publickey.js
  3095. var deferreds = [],
  3096. pk = new PublicKey(),
  3097. lock = ref.display_message(ref.get_label('loading'), 'loading');
  3098. $.each(emails, function(i, email) {
  3099. var d = $.Deferred();
  3100. pk.search(email, function(results, errorCode) {
  3101. if (errorCode !== null) {
  3102. // rejecting would make all fail
  3103. // d.reject(email);
  3104. d.resolve([email]);
  3105. }
  3106. else {
  3107. d.resolve([email].concat(results));
  3108. }
  3109. });
  3110. deferreds.push(d);
  3111. });
  3112. $.when.apply($, deferreds).then(function() {
  3113. var missing_keys = [],
  3114. key_selection = [];
  3115. // alanyze results of all queries
  3116. $.each(arguments, function(i, result) {
  3117. var email = result.shift();
  3118. if (!result.length) {
  3119. missing_keys.push(email);
  3120. }
  3121. else {
  3122. key_selection = key_selection.concat(result);
  3123. }
  3124. });
  3125. ref.hide_message(lock);
  3126. resolve(true);
  3127. // show key import dialog
  3128. if (key_selection.length) {
  3129. ref.mailvelope_key_import_dialog(key_selection);
  3130. }
  3131. // some keys could not be found
  3132. if (missing_keys.length) {
  3133. ref.display_message(ref.get_label('nopubkeyfor').replace('$email', missing_keys.join(', ')), 'warning');
  3134. }
  3135. }).fail(function() {
  3136. console.error('Pubkey lookup failed with', arguments);
  3137. ref.hide_message(lock);
  3138. ref.display_message('pubkeysearcherror', 'error');
  3139. resolve(false);
  3140. });
  3141. };
  3142. // list the given public keys in a dialog with options to import
  3143. // them into the local Maivelope keyring
  3144. this.mailvelope_key_import_dialog = function(candidates)
  3145. {
  3146. var ul = $('<div>').addClass('listing mailvelopekeyimport');
  3147. $.each(candidates, function(i, keyrec) {
  3148. var li = $('<div>').addClass('key');
  3149. if (keyrec.revoked) li.addClass('revoked');
  3150. if (keyrec.disabled) li.addClass('disabled');
  3151. if (keyrec.expired) li.addClass('expired');
  3152. li.append($('<label>').addClass('keyid').text(ref.get_label('keyid')));
  3153. li.append($('<a>').text(keyrec.keyid.substr(-8).toUpperCase())
  3154. .attr('href', keyrec.info)
  3155. .attr('target', '_blank')
  3156. .attr('tabindex', '-1'));
  3157. li.append($('<label>').addClass('keylen').text(ref.get_label('keylength')));
  3158. li.append($('<span>').text(keyrec.keylen));
  3159. if (keyrec.expirationdate) {
  3160. li.append($('<label>').addClass('keyexpired').text(ref.get_label('keyexpired')));
  3161. li.append($('<span>').text(new Date(keyrec.expirationdate * 1000).toDateString()));
  3162. }
  3163. if (keyrec.revoked) {
  3164. li.append($('<span>').addClass('keyrevoked').text(ref.get_label('keyrevoked')));
  3165. }
  3166. var ul_ = $('<ul>').addClass('uids');
  3167. $.each(keyrec.uids, function(j, uid) {
  3168. var li_ = $('<li>').addClass('uid');
  3169. if (uid.revoked) li_.addClass('revoked');
  3170. if (uid.disabled) li_.addClass('disabled');
  3171. if (uid.expired) li_.addClass('expired');
  3172. ul_.append(li_.text(uid.uid));
  3173. });
  3174. li.append(ul_);
  3175. li.append($('<input>')
  3176. .attr('type', 'button')
  3177. .attr('rel', keyrec.keyid)
  3178. .attr('value', ref.get_label('import'))
  3179. .addClass('button importkey')
  3180. .prop('disabled', keyrec.revoked || keyrec.disabled || keyrec.expired));
  3181. ul.append(li);
  3182. });
  3183. // display dialog with missing keys
  3184. ref.show_popup_dialog(
  3185. $('<div>')
  3186. .append($('<p>').html(ref.get_label('encryptpubkeysfound')))
  3187. .append(ul),
  3188. ref.get_label('importpubkeys'),
  3189. [{
  3190. text: ref.get_label('close'),
  3191. click: function(){
  3192. $(this).dialog('close');
  3193. }
  3194. }]
  3195. );
  3196. // delegate handler for import button clicks
  3197. ul.on('click', 'input.button.importkey', function() {
  3198. var btn = $(this),
  3199. keyid = btn.attr('rel'),
  3200. pk = new PublicKey(),
  3201. lock = ref.display_message(ref.get_label('loading'), 'loading');
  3202. // fetch from keyserver and import to Mailvelope keyring
  3203. pk.get(keyid, function(armored, errorCode) {
  3204. ref.hide_message(lock);
  3205. if (errorCode) {
  3206. ref.display_message(ref.get_label('keyservererror'), 'error');
  3207. return;
  3208. }
  3209. // import to keyring
  3210. ref.mailvelope_keyring.importPublicKey(armored).then(function(status) {
  3211. if (status === 'REJECTED') {
  3212. // alert(ref.get_label('Key import was rejected'));
  3213. }
  3214. else {
  3215. var $key = keyid.substr(-8).toUpperCase();
  3216. btn.closest('.key').fadeOut();
  3217. ref.display_message(ref.get_label('keyimportsuccess').replace('$key', $key), 'confirmation');
  3218. }
  3219. }, function(err) {
  3220. console.log(err);
  3221. });
  3222. });
  3223. });
  3224. };
  3225. /*********************************************************/
  3226. /********* mailbox folders methods *********/
  3227. /*********************************************************/
  3228. this.expunge_mailbox = function(mbox)
  3229. {
  3230. var lock, post_data = {_mbox: mbox};
  3231. // lock interface if it's the active mailbox
  3232. if (mbox == this.env.mailbox) {
  3233. lock = this.set_busy(true, 'loading');
  3234. post_data._reload = 1;
  3235. if (this.env.search_request)
  3236. post_data._search = this.env.search_request;
  3237. }
  3238. // send request to server
  3239. this.http_post('expunge', post_data, lock);
  3240. };
  3241. this.purge_mailbox = function(mbox)
  3242. {
  3243. var lock, post_data = {_mbox: mbox};
  3244. if (!confirm(this.get_label('purgefolderconfirm')))
  3245. return false;
  3246. // lock interface if it's the active mailbox
  3247. if (mbox == this.env.mailbox) {
  3248. lock = this.set_busy(true, 'loading');
  3249. post_data._reload = 1;
  3250. }
  3251. // send request to server
  3252. this.http_post('purge', post_data, lock);
  3253. };
  3254. // test if purge command is allowed
  3255. this.purge_mailbox_test = function()
  3256. {
  3257. return (this.env.exists && (
  3258. this.env.mailbox == this.env.trash_mailbox
  3259. || this.env.mailbox == this.env.junk_mailbox
  3260. || this.env.mailbox.startsWith(this.env.trash_mailbox + this.env.delimiter)
  3261. || this.env.mailbox.startsWith(this.env.junk_mailbox + this.env.delimiter)
  3262. ));
  3263. };
  3264. /*********************************************************/
  3265. /********* login form methods *********/
  3266. /*********************************************************/
  3267. // handler for keyboard events on the _user field
  3268. this.login_user_keyup = function(e)
  3269. {
  3270. var key = rcube_event.get_keycode(e),
  3271. passwd = $('#rcmloginpwd');
  3272. // enter
  3273. if (key == 13 && passwd.length && !passwd.val()) {
  3274. passwd.focus();
  3275. return rcube_event.cancel(e);
  3276. }
  3277. return true;
  3278. };
  3279. /*********************************************************/
  3280. /********* message compose methods *********/
  3281. /*********************************************************/
  3282. this.open_compose_step = function(p)
  3283. {
  3284. var url = this.url('mail/compose', p);
  3285. // open new compose window
  3286. if (this.env.compose_extwin && !this.env.extwin) {
  3287. this.open_window(url);
  3288. }
  3289. else {
  3290. this.redirect(url);
  3291. if (this.env.extwin)
  3292. window.resizeTo(Math.max(this.env.popup_width, $(window).width()), $(window).height() + 24);
  3293. }
  3294. };
  3295. // init message compose form: set focus and eventhandlers
  3296. this.init_messageform = function()
  3297. {
  3298. if (!this.gui_objects.messageform)
  3299. return false;
  3300. var i, elem, pos, input_from = $("[name='_from']"),
  3301. input_to = $("[name='_to']"),
  3302. input_subject = $("input[name='_subject']"),
  3303. input_message = $("[name='_message']").get(0),
  3304. html_mode = $("input[name='_is_html']").val() == '1',
  3305. ac_fields = ['cc', 'bcc', 'replyto', 'followupto'],
  3306. ac_props, opener_rc = this.opener();
  3307. // close compose step in opener
  3308. if (opener_rc && opener_rc.env.action == 'compose') {
  3309. setTimeout(function(){
  3310. if (opener.history.length > 1)
  3311. opener.history.back();
  3312. else
  3313. opener_rc.redirect(opener_rc.get_task_url('mail'));
  3314. }, 100);
  3315. this.env.opened_extwin = true;
  3316. }
  3317. // configure parallel autocompletion
  3318. if (this.env.autocomplete_threads > 0) {
  3319. ac_props = {
  3320. threads: this.env.autocomplete_threads,
  3321. sources: this.env.autocomplete_sources
  3322. };
  3323. }
  3324. // init live search events
  3325. this.init_address_input_events(input_to, ac_props);
  3326. for (i in ac_fields) {
  3327. this.init_address_input_events($("[name='_"+ac_fields[i]+"']"), ac_props);
  3328. }
  3329. if (!html_mode) {
  3330. pos = this.env.top_posting && this.env.compose_mode ? 0 : input_message.value.length;
  3331. // add signature according to selected identity
  3332. // if we have HTML editor, signature is added in a callback
  3333. if (input_from.prop('type') == 'select-one') {
  3334. this.change_identity(input_from[0]);
  3335. }
  3336. // set initial cursor position
  3337. this.set_caret_pos(input_message, pos);
  3338. // scroll to the bottom of the textarea (#1490114)
  3339. if (pos) {
  3340. $(input_message).scrollTop(input_message.scrollHeight);
  3341. }
  3342. }
  3343. // check for locally stored compose data
  3344. if (this.env.save_localstorage)
  3345. this.compose_restore_dialog(0, html_mode)
  3346. if (input_to.val() == '')
  3347. elem = input_to;
  3348. else if (input_subject.val() == '')
  3349. elem = input_subject;
  3350. else if (input_message)
  3351. elem = input_message;
  3352. // focus first empty element (need to be visible on IE8)
  3353. $(elem).filter(':visible').focus();
  3354. this.env.compose_focus_elem = document.activeElement;
  3355. // get summary of all field values
  3356. this.compose_field_hash(true);
  3357. // start the auto-save timer
  3358. this.auto_save_start();
  3359. };
  3360. this.compose_restore_dialog = function(j, html_mode)
  3361. {
  3362. var i, key, formdata, index = this.local_storage_get_item('compose.index', []);
  3363. var show_next = function(i) {
  3364. if (++i < index.length)
  3365. ref.compose_restore_dialog(i, html_mode)
  3366. }
  3367. for (i = j || 0; i < index.length; i++) {
  3368. key = index[i];
  3369. formdata = this.local_storage_get_item('compose.' + key, null, true);
  3370. if (!formdata) {
  3371. continue;
  3372. }
  3373. // restore saved copy of current compose_id
  3374. if (formdata.changed && key == this.env.compose_id) {
  3375. this.restore_compose_form(key, html_mode);
  3376. break;
  3377. }
  3378. // skip records from 'other' drafts
  3379. if (this.env.draft_id && formdata.draft_id && formdata.draft_id != this.env.draft_id) {
  3380. continue;
  3381. }
  3382. // skip records on reply
  3383. if (this.env.reply_msgid && formdata.reply_msgid != this.env.reply_msgid) {
  3384. continue;
  3385. }
  3386. // show dialog asking to restore the message
  3387. if (formdata.changed && formdata.session != this.env.session_id) {
  3388. this.show_popup_dialog(
  3389. this.get_label('restoresavedcomposedata')
  3390. .replace('$date', new Date(formdata.changed).toLocaleString())
  3391. .replace('$subject', formdata._subject)
  3392. .replace(/\n/g, '<br/>'),
  3393. this.get_label('restoremessage'),
  3394. [{
  3395. text: this.get_label('restore'),
  3396. 'class': 'mainaction',
  3397. click: function(){
  3398. ref.restore_compose_form(key, html_mode);
  3399. ref.remove_compose_data(key); // remove old copy
  3400. ref.save_compose_form_local(); // save under current compose_id
  3401. $(this).dialog('close');
  3402. }
  3403. },
  3404. {
  3405. text: this.get_label('delete'),
  3406. 'class': 'delete',
  3407. click: function(){
  3408. ref.remove_compose_data(key);
  3409. $(this).dialog('close');
  3410. show_next(i);
  3411. }
  3412. },
  3413. {
  3414. text: this.get_label('ignore'),
  3415. click: function(){
  3416. $(this).dialog('close');
  3417. show_next(i);
  3418. }
  3419. }]
  3420. );
  3421. break;
  3422. }
  3423. }
  3424. }
  3425. this.init_address_input_events = function(obj, props)
  3426. {
  3427. this.env.recipients_delimiter = this.env.recipients_separator + ' ';
  3428. obj.keydown(function(e) { return ref.ksearch_keydown(e, this, props); })
  3429. .attr({ 'autocomplete': 'off', 'aria-autocomplete': 'list', 'aria-expanded': 'false', 'role': 'combobox' });
  3430. };
  3431. this.submit_messageform = function(draft, saveonly)
  3432. {
  3433. var form = this.gui_objects.messageform;
  3434. if (!form)
  3435. return;
  3436. // the message has been sent but not saved, ask the user what to do
  3437. if (!saveonly && this.env.is_sent) {
  3438. return this.show_popup_dialog(this.get_label('messageissent'), '',
  3439. [{
  3440. text: this.get_label('save'),
  3441. 'class': 'mainaction',
  3442. click: function() {
  3443. ref.submit_messageform(false, true);
  3444. $(this).dialog('close');
  3445. }
  3446. },
  3447. {
  3448. text: this.get_label('cancel'),
  3449. click: function() {
  3450. $(this).dialog('close');
  3451. }
  3452. }]
  3453. );
  3454. }
  3455. // delegate sending to Mailvelope routine
  3456. if (this.mailvelope_editor) {
  3457. return this.mailvelope_submit_messageform(draft, saveonly);
  3458. }
  3459. // all checks passed, send message
  3460. var msgid = this.set_busy(true, draft || saveonly ? 'savingmessage' : 'sendingmessage'),
  3461. lang = this.spellcheck_lang(),
  3462. files = [];
  3463. // send files list
  3464. $('li', this.gui_objects.attachmentlist).each(function() { files.push(this.id.replace(/^rcmfile/, '')); });
  3465. $('input[name="_attachments"]', form).val(files.join());
  3466. form.target = 'savetarget';
  3467. form._draft.value = draft ? '1' : '';
  3468. form.action = this.add_url(form.action, '_unlock', msgid);
  3469. form.action = this.add_url(form.action, '_lang', lang);
  3470. form.action = this.add_url(form.action, '_framed', 1);
  3471. if (saveonly) {
  3472. form.action = this.add_url(form.action, '_saveonly', 1);
  3473. }
  3474. // register timer to notify about connection timeout
  3475. this.submit_timer = setTimeout(function(){
  3476. ref.set_busy(false, null, msgid);
  3477. ref.display_message(ref.get_label('requesttimedout'), 'error');
  3478. }, this.env.request_timeout * 1000);
  3479. form.submit();
  3480. };
  3481. this.compose_recipient_select = function(list)
  3482. {
  3483. var id, n, recipients = 0;
  3484. for (n=0; n < list.selection.length; n++) {
  3485. id = list.selection[n];
  3486. if (this.env.contactdata[id])
  3487. recipients++;
  3488. }
  3489. this.enable_command('add-recipient', recipients);
  3490. };
  3491. this.compose_add_recipient = function(field)
  3492. {
  3493. // find last focused field name
  3494. if (!field) {
  3495. field = $(this.env.focused_field).filter(':visible');
  3496. field = field.length ? field.attr('id').replace('_', '') : 'to';
  3497. }
  3498. var recipients = [], input = $('#_'+field), delim = this.env.recipients_delimiter;
  3499. if (this.contact_list && this.contact_list.selection.length) {
  3500. for (var id, n=0; n < this.contact_list.selection.length; n++) {
  3501. id = this.contact_list.selection[n];
  3502. if (id && this.env.contactdata[id]) {
  3503. recipients.push(this.env.contactdata[id]);
  3504. // group is added, expand it
  3505. if (id.charAt(0) == 'E' && this.env.contactdata[id].indexOf('@') < 0 && input.length) {
  3506. var gid = id.substr(1);
  3507. this.group2expand[gid] = { name:this.env.contactdata[id], input:input.get(0) };
  3508. this.http_request('group-expand', {_source: this.env.source, _gid: gid}, false);
  3509. }
  3510. }
  3511. }
  3512. }
  3513. if (recipients.length && input.length) {
  3514. var oldval = input.val(), rx = new RegExp(RegExp.escape(delim) + '\\s*$');
  3515. if (oldval && !rx.test(oldval))
  3516. oldval += delim + ' ';
  3517. input.val(oldval + recipients.join(delim + ' ') + delim + ' ').change();
  3518. this.triggerEvent('add-recipient', { field:field, recipients:recipients });
  3519. }
  3520. return recipients.length;
  3521. };
  3522. // checks the input fields before sending a message
  3523. this.check_compose_input = function(cmd)
  3524. {
  3525. // check input fields
  3526. var input_to = $("[name='_to']"),
  3527. input_cc = $("[name='_cc']"),
  3528. input_bcc = $("[name='_bcc']"),
  3529. input_from = $("[name='_from']"),
  3530. input_subject = $("[name='_subject']");
  3531. // check sender (if have no identities)
  3532. if (input_from.prop('type') == 'text' && !rcube_check_email(input_from.val(), true)) {
  3533. alert(this.get_label('nosenderwarning'));
  3534. input_from.focus();
  3535. return false;
  3536. }
  3537. // check for empty recipient
  3538. var recipients = input_to.val() ? input_to.val() : (input_cc.val() ? input_cc.val() : input_bcc.val());
  3539. if (!rcube_check_email(recipients.replace(/^\s+/, '').replace(/[\s,;]+$/, ''), true)) {
  3540. alert(this.get_label('norecipientwarning'));
  3541. input_to.focus();
  3542. return false;
  3543. }
  3544. // check if all files has been uploaded
  3545. for (var key in this.env.attachments) {
  3546. if (typeof this.env.attachments[key] === 'object' && !this.env.attachments[key].complete) {
  3547. alert(this.get_label('notuploadedwarning'));
  3548. return false;
  3549. }
  3550. }
  3551. // display localized warning for missing subject
  3552. if (input_subject.val() == '') {
  3553. var buttons = {},
  3554. myprompt = $('<div class="prompt">').html('<div class="message">' + this.get_label('nosubjectwarning') + '</div>')
  3555. .appendTo(document.body),
  3556. prompt_value = $('<input>').attr({type: 'text', size: 30}).val(this.get_label('nosubject'))
  3557. .appendTo(myprompt),
  3558. save_func = function() {
  3559. input_subject.val(prompt_value.val());
  3560. myprompt.dialog('close');
  3561. ref.command(cmd, { nocheck:true }); // repeat command which triggered this
  3562. };
  3563. buttons[this.get_label('sendmessage')] = function() {
  3564. save_func($(this));
  3565. };
  3566. buttons[this.get_label('cancel')] = function() {
  3567. input_subject.focus();
  3568. $(this).dialog('close');
  3569. };
  3570. myprompt.dialog({
  3571. modal: true,
  3572. resizable: false,
  3573. buttons: buttons,
  3574. close: function(event, ui) { $(this).remove(); }
  3575. });
  3576. prompt_value.select().keydown(function(e) {
  3577. if (e.which == 13) save_func();
  3578. });
  3579. return false;
  3580. }
  3581. // check for empty body (only possible if not mailvelope encrypted)
  3582. if (!this.mailvelope_editor && !this.editor.get_content() && !confirm(this.get_label('nobodywarning'))) {
  3583. this.editor.focus();
  3584. return false;
  3585. }
  3586. // move body from html editor to textarea (just to be sure, #1485860)
  3587. this.editor.save();
  3588. return true;
  3589. };
  3590. this.toggle_editor = function(props, obj, e)
  3591. {
  3592. // @todo: this should work also with many editors on page
  3593. var result = this.editor.toggle(props.html, props.noconvert || false);
  3594. // satisfy the expectations of aftertoggle-editor event subscribers
  3595. props.mode = props.html ? 'html' : 'plain';
  3596. if (!result && e) {
  3597. // fix selector value if operation failed
  3598. props.mode = props.html ? 'plain' : 'html';
  3599. $(e.target).filter('select').val(props.mode);
  3600. }
  3601. if (result) {
  3602. // update internal format flag
  3603. $("input[name='_is_html']").val(props.html ? 1 : 0);
  3604. }
  3605. return result;
  3606. };
  3607. this.insert_response = function(key)
  3608. {
  3609. var insert = this.env.textresponses[key] ? this.env.textresponses[key].text : null;
  3610. if (!insert)
  3611. return false;
  3612. this.editor.replace(insert);
  3613. };
  3614. /**
  3615. * Open the dialog to save a new canned response
  3616. */
  3617. this.save_response = function()
  3618. {
  3619. // show dialog to enter a name and to modify the text to be saved
  3620. var buttons = {}, text = this.editor.get_content({selection: true, format: 'text', nosig: true}),
  3621. html = '<form class="propform">' +
  3622. '<div class="prop block"><label>' + this.get_label('responsename') + '</label>' +
  3623. '<input type="text" name="name" id="ffresponsename" size="40" /></div>' +
  3624. '<div class="prop block"><label>' + this.get_label('responsetext') + '</label>' +
  3625. '<textarea name="text" id="ffresponsetext" cols="40" rows="8"></textarea></div>' +
  3626. '</form>';
  3627. buttons[this.get_label('save')] = function(e) {
  3628. var name = $('#ffresponsename').val(),
  3629. text = $('#ffresponsetext').val();
  3630. if (!text) {
  3631. $('#ffresponsetext').select();
  3632. return false;
  3633. }
  3634. if (!name)
  3635. name = text.substring(0,40);
  3636. var lock = ref.display_message(ref.get_label('savingresponse'), 'loading');
  3637. ref.http_post('settings/responses', { _insert:1, _name:name, _text:text }, lock);
  3638. $(this).dialog('close');
  3639. };
  3640. buttons[this.get_label('cancel')] = function() {
  3641. $(this).dialog('close');
  3642. };
  3643. this.show_popup_dialog(html, this.get_label('newresponse'), buttons, {button_classes: ['mainaction']});
  3644. $('#ffresponsetext').val(text);
  3645. $('#ffresponsename').select();
  3646. };
  3647. this.add_response_item = function(response)
  3648. {
  3649. var key = response.key;
  3650. this.env.textresponses[key] = response;
  3651. // append to responses list
  3652. if (this.gui_objects.responseslist) {
  3653. var li = $('<li>').appendTo(this.gui_objects.responseslist);
  3654. $('<a>').addClass('insertresponse active')
  3655. .attr('href', '#')
  3656. .attr('rel', key)
  3657. .attr('tabindex', '0')
  3658. .html(this.quote_html(response.name))
  3659. .appendTo(li)
  3660. .mousedown(function(e) {
  3661. return rcube_event.cancel(e);
  3662. })
  3663. .on('mouseup keypress', function(e) {
  3664. if (e.type == 'mouseup' || rcube_event.get_keycode(e) == 13) {
  3665. ref.command('insert-response', $(this).attr('rel'));
  3666. $(document.body).trigger('mouseup'); // hides the menu
  3667. return rcube_event.cancel(e);
  3668. }
  3669. });
  3670. }
  3671. };
  3672. this.edit_responses = function()
  3673. {
  3674. // TODO: implement inline editing of responses
  3675. };
  3676. this.delete_response = function(key)
  3677. {
  3678. if (!key && this.responses_list) {
  3679. var selection = this.responses_list.get_selection();
  3680. key = selection[0];
  3681. }
  3682. // submit delete request
  3683. if (key && confirm(this.get_label('deleteresponseconfirm'))) {
  3684. this.http_post('settings/delete-response', { _key: key }, false);
  3685. }
  3686. };
  3687. // updates spellchecker buttons on state change
  3688. this.spellcheck_state = function()
  3689. {
  3690. var active = this.editor.spellcheck_state();
  3691. $.each(this.buttons.spellcheck || [], function(i, v) {
  3692. $('#' + v.id)[active ? 'addClass' : 'removeClass']('selected');
  3693. });
  3694. return active;
  3695. };
  3696. // get selected language
  3697. this.spellcheck_lang = function()
  3698. {
  3699. return this.editor.get_language();
  3700. };
  3701. this.spellcheck_lang_set = function(lang)
  3702. {
  3703. this.editor.set_language(lang);
  3704. };
  3705. // resume spellchecking, highlight provided mispellings without new ajax request
  3706. this.spellcheck_resume = function(data)
  3707. {
  3708. this.editor.spellcheck_resume(data);
  3709. };
  3710. this.set_draft_id = function(id)
  3711. {
  3712. if (id && id != this.env.draft_id) {
  3713. var filter = {task: 'mail', action: ''},
  3714. rc = this.opener(false, filter) || this.opener(true, filter);
  3715. // refresh the drafts folder in the opener window
  3716. if (rc && rc.env.mailbox == this.env.drafts_mailbox)
  3717. rc.command('checkmail');
  3718. this.env.draft_id = id;
  3719. $("input[name='_draft_saveid']").val(id);
  3720. // reset history of hidden iframe used for saving draft (#1489643)
  3721. // but don't do this on timer-triggered draft-autosaving (#1489789)
  3722. if (window.frames['savetarget'] && window.frames['savetarget'].history && !this.draft_autosave_submit && !this.mailvelope_editor) {
  3723. window.frames['savetarget'].history.back();
  3724. }
  3725. this.draft_autosave_submit = false;
  3726. }
  3727. // always remove local copy upon saving as draft
  3728. this.remove_compose_data(this.env.compose_id);
  3729. this.compose_skip_unsavedcheck = false;
  3730. };
  3731. this.auto_save_start = function()
  3732. {
  3733. if (this.env.draft_autosave) {
  3734. this.draft_autosave_submit = false;
  3735. this.save_timer = setTimeout(function(){
  3736. ref.draft_autosave_submit = true; // set auto-saved flag (#1489789)
  3737. ref.command("savedraft");
  3738. }, this.env.draft_autosave * 1000);
  3739. }
  3740. // save compose form content to local storage every 5 seconds
  3741. if (!this.local_save_timer && window.localStorage && this.env.save_localstorage) {
  3742. // track typing activity and only save on changes
  3743. this.compose_type_activity = this.compose_type_activity_last = 0;
  3744. $(document).keypress(function(e) { ref.compose_type_activity++; });
  3745. this.local_save_timer = setInterval(function(){
  3746. if (ref.compose_type_activity > ref.compose_type_activity_last) {
  3747. ref.save_compose_form_local();
  3748. ref.compose_type_activity_last = ref.compose_type_activity;
  3749. }
  3750. }, 5000);
  3751. $(window).on('unload', function() {
  3752. // remove copy from local storage if compose screen is left after warning
  3753. if (!ref.env.server_error)
  3754. ref.remove_compose_data(ref.env.compose_id);
  3755. });
  3756. }
  3757. // check for unsaved changes before leaving the compose page
  3758. if (!window.onbeforeunload) {
  3759. window.onbeforeunload = function() {
  3760. if (!ref.compose_skip_unsavedcheck && ref.cmp_hash != ref.compose_field_hash()) {
  3761. return ref.get_label('notsentwarning');
  3762. }
  3763. };
  3764. }
  3765. // Unlock interface now that saving is complete
  3766. this.busy = false;
  3767. };
  3768. this.compose_field_hash = function(save)
  3769. {
  3770. // check input fields
  3771. var i, id, val, str = '', hash_fields = ['to', 'cc', 'bcc', 'subject'];
  3772. for (i=0; i<hash_fields.length; i++)
  3773. if (val = $('[name="_' + hash_fields[i] + '"]').val())
  3774. str += val + ':';
  3775. str += this.editor.get_content({refresh: false});
  3776. if (this.env.attachments)
  3777. for (id in this.env.attachments)
  3778. str += id;
  3779. // we can't detect changes in the Mailvelope editor so assume it changed
  3780. if (this.mailvelope_editor) {
  3781. str += ';' + new Date().getTime();
  3782. }
  3783. if (save)
  3784. this.cmp_hash = str;
  3785. return str;
  3786. };
  3787. // store the contents of the compose form to localstorage
  3788. this.save_compose_form_local = function()
  3789. {
  3790. // feature is disabled
  3791. if (!this.env.save_localstorage)
  3792. return;
  3793. var formdata = { session:this.env.session_id, changed:new Date().getTime() },
  3794. ed, empty = true;
  3795. // get fresh content from editor
  3796. this.editor.save();
  3797. if (this.env.draft_id) {
  3798. formdata.draft_id = this.env.draft_id;
  3799. }
  3800. if (this.env.reply_msgid) {
  3801. formdata.reply_msgid = this.env.reply_msgid;
  3802. }
  3803. $('input, select, textarea', this.gui_objects.messageform).each(function(i, elem) {
  3804. switch (elem.tagName.toLowerCase()) {
  3805. case 'input':
  3806. if (elem.type == 'button' || elem.type == 'submit' || (elem.type == 'hidden' && elem.name != '_is_html')) {
  3807. break;
  3808. }
  3809. formdata[elem.name] = elem.type != 'checkbox' || elem.checked ? $(elem).val() : '';
  3810. if (formdata[elem.name] != '' && elem.type != 'hidden')
  3811. empty = false;
  3812. break;
  3813. case 'select':
  3814. formdata[elem.name] = $('option:checked', elem).val();
  3815. break;
  3816. default:
  3817. formdata[elem.name] = $(elem).val();
  3818. if (formdata[elem.name] != '')
  3819. empty = false;
  3820. }
  3821. });
  3822. if (!empty) {
  3823. var index = this.local_storage_get_item('compose.index', []),
  3824. key = this.env.compose_id;
  3825. if ($.inArray(key, index) < 0) {
  3826. index.push(key);
  3827. }
  3828. this.local_storage_set_item('compose.' + key, formdata, true);
  3829. this.local_storage_set_item('compose.index', index);
  3830. }
  3831. };
  3832. // write stored compose data back to form
  3833. this.restore_compose_form = function(key, html_mode)
  3834. {
  3835. var ed, formdata = this.local_storage_get_item('compose.' + key, true);
  3836. if (formdata && typeof formdata == 'object') {
  3837. $.each(formdata, function(k, value) {
  3838. if (k[0] == '_') {
  3839. var elem = $("*[name='"+k+"']");
  3840. if (elem[0] && elem[0].type == 'checkbox') {
  3841. elem.prop('checked', value != '');
  3842. }
  3843. else {
  3844. elem.val(value);
  3845. }
  3846. }
  3847. });
  3848. // initialize HTML editor
  3849. if ((formdata._is_html == '1' && !html_mode) || (formdata._is_html != '1' && html_mode)) {
  3850. this.command('toggle-editor', {id: this.env.composebody, html: !html_mode, noconvert: true});
  3851. }
  3852. }
  3853. };
  3854. // remove stored compose data from localStorage
  3855. this.remove_compose_data = function(key)
  3856. {
  3857. var index = this.local_storage_get_item('compose.index', []);
  3858. if ($.inArray(key, index) >= 0) {
  3859. this.local_storage_remove_item('compose.' + key);
  3860. this.local_storage_set_item('compose.index', $.grep(index, function(val,i) { return val != key; }));
  3861. }
  3862. };
  3863. // clear all stored compose data of this user
  3864. this.clear_compose_data = function()
  3865. {
  3866. var i, index = this.local_storage_get_item('compose.index', []);
  3867. for (i=0; i < index.length; i++) {
  3868. this.local_storage_remove_item('compose.' + index[i]);
  3869. }
  3870. this.local_storage_remove_item('compose.index');
  3871. };
  3872. this.change_identity = function(obj, show_sig)
  3873. {
  3874. if (!obj || !obj.options)
  3875. return false;
  3876. if (!show_sig)
  3877. show_sig = this.env.show_sig;
  3878. var id = obj.options[obj.selectedIndex].value,
  3879. sig = this.env.identity,
  3880. delim = this.env.recipients_separator,
  3881. rx_delim = RegExp.escape(delim);
  3882. // enable manual signature insert
  3883. if (this.env.signatures && this.env.signatures[id]) {
  3884. this.enable_command('insert-sig', true);
  3885. this.env.compose_commands.push('insert-sig');
  3886. }
  3887. else
  3888. this.enable_command('insert-sig', false);
  3889. // first function execution
  3890. if (!this.env.identities_initialized) {
  3891. this.env.identities_initialized = true;
  3892. if (this.env.show_sig_later)
  3893. this.env.show_sig = true;
  3894. if (this.env.opened_extwin)
  3895. return;
  3896. }
  3897. // update reply-to/bcc fields with addresses defined in identities
  3898. $.each(['replyto', 'bcc'], function() {
  3899. var rx, key = this,
  3900. old_val = sig && ref.env.identities[sig] ? ref.env.identities[sig][key] : '',
  3901. new_val = id && ref.env.identities[id] ? ref.env.identities[id][key] : '',
  3902. input = $('[name="_'+key+'"]'), input_val = input.val();
  3903. // remove old address(es)
  3904. if (old_val && input_val) {
  3905. rx = new RegExp('\\s*' + RegExp.escape(old_val) + '\\s*');
  3906. input_val = input_val.replace(rx, '');
  3907. }
  3908. // cleanup
  3909. rx = new RegExp(rx_delim + '\\s*' + rx_delim, 'g');
  3910. input_val = String(input_val).replace(rx, delim);
  3911. rx = new RegExp('^[\\s' + rx_delim + ']+');
  3912. input_val = input_val.replace(rx, '');
  3913. // add new address(es)
  3914. if (new_val && input_val.indexOf(new_val) == -1 && input_val.indexOf(new_val.replace(/"/g, '')) == -1) {
  3915. if (input_val) {
  3916. rx = new RegExp('[' + rx_delim + '\\s]+$')
  3917. input_val = input_val.replace(rx, '') + delim + ' ';
  3918. }
  3919. input_val += new_val + delim + ' ';
  3920. }
  3921. if (old_val || new_val)
  3922. input.val(input_val).change();
  3923. });
  3924. this.editor.change_signature(id, show_sig);
  3925. this.env.identity = id;
  3926. this.triggerEvent('change_identity');
  3927. return true;
  3928. };
  3929. // upload (attachment) file
  3930. this.upload_file = function(form, action, lock)
  3931. {
  3932. if (!form)
  3933. return;
  3934. // count files and size on capable browser
  3935. var size = 0, numfiles = 0;
  3936. $('input[type=file]', form).each(function(i, field) {
  3937. var files = field.files ? field.files.length : (field.value ? 1 : 0);
  3938. // check file size
  3939. if (field.files) {
  3940. for (var i=0; i < files; i++)
  3941. size += field.files[i].size;
  3942. }
  3943. numfiles += files;
  3944. });
  3945. // create hidden iframe and post upload form
  3946. if (numfiles) {
  3947. if (this.env.max_filesize && this.env.filesizeerror && size > this.env.max_filesize) {
  3948. this.display_message(this.env.filesizeerror, 'error');
  3949. return false;
  3950. }
  3951. var frame_name = this.async_upload_form(form, action || 'upload', function(e) {
  3952. var d, content = '';
  3953. try {
  3954. if (this.contentDocument) {
  3955. d = this.contentDocument;
  3956. } else if (this.contentWindow) {
  3957. d = this.contentWindow.document;
  3958. }
  3959. content = d.childNodes[1].innerHTML;
  3960. } catch (err) {}
  3961. if (!content.match(/add2attachment/) && (!bw.opera || (ref.env.uploadframe && ref.env.uploadframe == e.data.ts))) {
  3962. if (!content.match(/display_message/))
  3963. ref.display_message(ref.get_label('fileuploaderror'), 'error');
  3964. ref.remove_from_attachment_list(e.data.ts);
  3965. if (lock)
  3966. ref.set_busy(false, null, lock);
  3967. }
  3968. // Opera hack: handle double onload
  3969. if (bw.opera)
  3970. ref.env.uploadframe = e.data.ts;
  3971. });
  3972. // display upload indicator and cancel button
  3973. var content = '<span>' + this.get_label('uploading' + (numfiles > 1 ? 'many' : '')) + '</span>',
  3974. ts = frame_name.replace(/^rcmupload/, '');
  3975. this.add2attachment_list(ts, { name:'', html:content, classname:'uploading', frame:frame_name, complete:false });
  3976. // upload progress support
  3977. if (this.env.upload_progress_time) {
  3978. this.upload_progress_start('upload', ts);
  3979. }
  3980. // set reference to the form object
  3981. this.gui_objects.attachmentform = form;
  3982. return true;
  3983. }
  3984. };
  3985. // add file name to attachment list
  3986. // called from upload page
  3987. this.add2attachment_list = function(name, att, upload_id)
  3988. {
  3989. if (upload_id)
  3990. this.triggerEvent('fileuploaded', {name: name, attachment: att, id: upload_id});
  3991. if (!this.env.attachments)
  3992. this.env.attachments = {};
  3993. if (upload_id && this.env.attachments[upload_id])
  3994. delete this.env.attachments[upload_id];
  3995. this.env.attachments[name] = att;
  3996. if (!this.gui_objects.attachmentlist)
  3997. return false;
  3998. if (!att.complete && this.env.loadingicon)
  3999. att.html = '<img src="'+this.env.loadingicon+'" alt="" class="uploading" />' + att.html;
  4000. if (!att.complete && att.frame)
  4001. att.html = '<a title="'+this.get_label('cancel')+'" onclick="return rcmail.cancel_attachment_upload(\''+name+'\', \''+att.frame+'\');" href="#cancelupload" class="cancelupload">'
  4002. + (this.env.cancelicon ? '<img src="'+this.env.cancelicon+'" alt="'+this.get_label('cancel')+'" />' : this.get_label('cancel')) + '</a>' + att.html;
  4003. var indicator, li = $('<li>');
  4004. li.attr('id', name)
  4005. .addClass(att.classname)
  4006. .html(att.html)
  4007. .on('mouseover', function() { rcube_webmail.long_subject_title_ex(this); });
  4008. // replace indicator's li
  4009. if (upload_id && (indicator = document.getElementById(upload_id))) {
  4010. li.replaceAll(indicator);
  4011. }
  4012. else { // add new li
  4013. li.appendTo(this.gui_objects.attachmentlist);
  4014. }
  4015. // set tabindex attribute
  4016. var tabindex = $(this.gui_objects.attachmentlist).attr('data-tabindex') || '0';
  4017. li.find('a').attr('tabindex', tabindex);
  4018. return true;
  4019. };
  4020. this.remove_from_attachment_list = function(name)
  4021. {
  4022. if (this.env.attachments) {
  4023. delete this.env.attachments[name];
  4024. $('#'+name).remove();
  4025. }
  4026. };
  4027. this.remove_attachment = function(name)
  4028. {
  4029. if (name && this.env.attachments[name])
  4030. this.http_post('remove-attachment', { _id:this.env.compose_id, _file:name });
  4031. return true;
  4032. };
  4033. this.cancel_attachment_upload = function(name, frame_name)
  4034. {
  4035. if (!name || !frame_name)
  4036. return false;
  4037. this.remove_from_attachment_list(name);
  4038. $("iframe[name='"+frame_name+"']").remove();
  4039. return false;
  4040. };
  4041. this.upload_progress_start = function(action, name)
  4042. {
  4043. setTimeout(function() { ref.http_request(action, {_progress: name}); },
  4044. this.env.upload_progress_time * 1000);
  4045. };
  4046. this.upload_progress_update = function(param)
  4047. {
  4048. var elem = $('#'+param.name + ' > span');
  4049. if (!elem.length || !param.text)
  4050. return;
  4051. elem.text(param.text);
  4052. if (!param.done)
  4053. this.upload_progress_start(param.action, param.name);
  4054. };
  4055. // send remote request to add a new contact
  4056. this.add_contact = function(value)
  4057. {
  4058. if (value)
  4059. this.http_post('addcontact', {_address: value});
  4060. return true;
  4061. };
  4062. // send remote request to search mail or contacts
  4063. this.qsearch = function(value)
  4064. {
  4065. if (value != '') {
  4066. var r, lock = this.set_busy(true, 'searching'),
  4067. url = this.search_params(value),
  4068. action = this.env.action == 'compose' && this.contact_list ? 'search-contacts' : 'search';
  4069. if (this.message_list)
  4070. this.clear_message_list();
  4071. else if (this.contact_list)
  4072. this.list_contacts_clear();
  4073. if (this.env.source)
  4074. url._source = this.env.source;
  4075. if (this.env.group)
  4076. url._gid = this.env.group;
  4077. // reset vars
  4078. this.env.current_page = 1;
  4079. r = this.http_request(action, url, lock);
  4080. this.env.qsearch = {lock: lock, request: r};
  4081. this.enable_command('set-listmode', this.env.threads && (this.env.search_scope || 'base') == 'base');
  4082. return true;
  4083. }
  4084. return false;
  4085. };
  4086. this.continue_search = function(request_id)
  4087. {
  4088. var lock = this.set_busy(true, 'stillsearching');
  4089. setTimeout(function() {
  4090. var url = ref.search_params();
  4091. url._continue = request_id;
  4092. ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) };
  4093. }, 100);
  4094. };
  4095. // build URL params for search
  4096. this.search_params = function(search, filter)
  4097. {
  4098. var n, url = {}, mods_arr = [],
  4099. mods = this.env.search_mods,
  4100. scope = this.env.search_scope || 'base',
  4101. mbox = scope == 'all' ? '*' : this.env.mailbox;
  4102. if (!filter && this.gui_objects.search_filter)
  4103. filter = this.gui_objects.search_filter.value;
  4104. if (!search && this.gui_objects.qsearchbox)
  4105. search = this.gui_objects.qsearchbox.value;
  4106. if (filter)
  4107. url._filter = filter;
  4108. if (this.gui_objects.search_interval)
  4109. url._interval = $(this.gui_objects.search_interval).val();
  4110. if (search) {
  4111. url._q = search;
  4112. if (mods && this.message_list)
  4113. mods = mods[mbox] || mods['*'];
  4114. if (mods) {
  4115. for (n in mods)
  4116. mods_arr.push(n);
  4117. url._headers = mods_arr.join(',');
  4118. }
  4119. }
  4120. if (scope)
  4121. url._scope = scope;
  4122. if (mbox && scope != 'all')
  4123. url._mbox = mbox;
  4124. return url;
  4125. };
  4126. // reset search filter
  4127. this.reset_search_filter = function()
  4128. {
  4129. this.filter_disabled = true;
  4130. if (this.gui_objects.search_filter)
  4131. $(this.gui_objects.search_filter).val('ALL').change();
  4132. this.filter_disabled = false;
  4133. };
  4134. // reset quick-search form
  4135. this.reset_qsearch = function(all)
  4136. {
  4137. if (this.gui_objects.qsearchbox)
  4138. this.gui_objects.qsearchbox.value = '';
  4139. if (this.gui_objects.search_interval)
  4140. $(this.gui_objects.search_interval).val('');
  4141. if (this.env.qsearch)
  4142. this.abort_request(this.env.qsearch);
  4143. if (all) {
  4144. this.env.search_scope = 'base';
  4145. this.reset_search_filter();
  4146. }
  4147. this.env.qsearch = null;
  4148. this.env.search_request = null;
  4149. this.env.search_id = null;
  4150. this.select_all_mode = false;
  4151. this.enable_command('set-listmode', this.env.threads);
  4152. };
  4153. this.set_searchscope = function(scope)
  4154. {
  4155. var old = this.env.search_scope;
  4156. this.env.search_scope = scope;
  4157. // re-send search query with new scope
  4158. if (scope != old && this.env.search_request) {
  4159. if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
  4160. this.filter_mailbox(this.env.search_filter);
  4161. if (scope != 'all')
  4162. this.select_folder(this.env.mailbox, '', true);
  4163. }
  4164. };
  4165. this.set_searchinterval = function(interval)
  4166. {
  4167. var old = this.env.search_interval;
  4168. this.env.search_interval = interval;
  4169. // re-send search query with new interval
  4170. if (interval != old && this.env.search_request) {
  4171. if (!this.qsearch(this.gui_objects.qsearchbox.value) && this.env.search_filter && this.env.search_filter != 'ALL')
  4172. this.filter_mailbox(this.env.search_filter);
  4173. if (interval)
  4174. this.select_folder(this.env.mailbox, '', true);
  4175. }
  4176. };
  4177. this.set_searchmods = function(mods)
  4178. {
  4179. var mbox = this.env.mailbox,
  4180. scope = this.env.search_scope || 'base';
  4181. if (scope == 'all')
  4182. mbox = '*';
  4183. if (!this.env.search_mods)
  4184. this.env.search_mods = {};
  4185. if (mbox)
  4186. this.env.search_mods[mbox] = mods;
  4187. };
  4188. this.is_multifolder_listing = function()
  4189. {
  4190. return this.env.multifolder_listing !== undefined ? this.env.multifolder_listing :
  4191. (this.env.search_request && (this.env.search_scope || 'base') != 'base');
  4192. };
  4193. // action executed after mail is sent
  4194. this.sent_successfully = function(type, msg, folders, save_error)
  4195. {
  4196. this.display_message(msg, type);
  4197. this.compose_skip_unsavedcheck = true;
  4198. if (this.env.extwin) {
  4199. if (!save_error)
  4200. this.lock_form(this.gui_objects.messageform);
  4201. var filter = {task: 'mail', action: ''},
  4202. rc = this.opener(false, filter) || this.opener(true, filter);
  4203. if (rc) {
  4204. rc.display_message(msg, type);
  4205. // refresh the folder where sent message was saved or replied message comes from
  4206. if (folders && $.inArray(rc.env.mailbox, folders) >= 0) {
  4207. rc.command('checkmail');
  4208. }
  4209. }
  4210. if (!save_error)
  4211. setTimeout(function() { window.close(); }, 1000);
  4212. }
  4213. else if (!save_error) {
  4214. // before redirect we need to wait some time for Chrome (#1486177)
  4215. setTimeout(function() { ref.list_mailbox(); }, 500);
  4216. }
  4217. if (save_error)
  4218. this.env.is_sent = true;
  4219. };
  4220. /*********************************************************/
  4221. /********* keyboard live-search methods *********/
  4222. /*********************************************************/
  4223. // handler for keyboard events on address-fields
  4224. this.ksearch_keydown = function(e, obj, props)
  4225. {
  4226. if (this.ksearch_timer)
  4227. clearTimeout(this.ksearch_timer);
  4228. var key = rcube_event.get_keycode(e),
  4229. mod = rcube_event.get_modifier(e);
  4230. switch (key) {
  4231. case 38: // arrow up
  4232. case 40: // arrow down
  4233. if (!this.ksearch_visible())
  4234. return;
  4235. var dir = key == 38 ? 1 : 0,
  4236. highlight = document.getElementById('rcmkSearchItem' + this.ksearch_selected);
  4237. if (!highlight)
  4238. highlight = this.ksearch_pane.__ul.firstChild;
  4239. if (highlight)
  4240. this.ksearch_select(dir ? highlight.previousSibling : highlight.nextSibling);
  4241. return rcube_event.cancel(e);
  4242. case 9: // tab
  4243. if (mod == SHIFT_KEY || !this.ksearch_visible()) {
  4244. this.ksearch_hide();
  4245. return;
  4246. }
  4247. case 13: // enter
  4248. if (!this.ksearch_visible())
  4249. return false;
  4250. // insert selected address and hide ksearch pane
  4251. this.insert_recipient(this.ksearch_selected);
  4252. this.ksearch_hide();
  4253. return rcube_event.cancel(e);
  4254. case 27: // escape
  4255. this.ksearch_hide();
  4256. return;
  4257. case 37: // left
  4258. case 39: // right
  4259. return;
  4260. }
  4261. // start timer
  4262. this.ksearch_timer = setTimeout(function(){ ref.ksearch_get_results(props); }, 200);
  4263. this.ksearch_input = obj;
  4264. return true;
  4265. };
  4266. this.ksearch_visible = function()
  4267. {
  4268. return this.ksearch_selected !== null && this.ksearch_selected !== undefined && this.ksearch_value;
  4269. };
  4270. this.ksearch_select = function(node)
  4271. {
  4272. if (this.ksearch_pane && node) {
  4273. this.ksearch_pane.find('li.selected').removeClass('selected').removeAttr('aria-selected');
  4274. }
  4275. if (node) {
  4276. $(node).addClass('selected').attr('aria-selected', 'true');
  4277. this.ksearch_selected = node._rcm_id;
  4278. $(this.ksearch_input).attr('aria-activedescendant', 'rcmkSearchItem' + this.ksearch_selected);
  4279. }
  4280. };
  4281. this.insert_recipient = function(id)
  4282. {
  4283. if (id === null || !this.env.contacts[id] || !this.ksearch_input)
  4284. return;
  4285. // get cursor pos
  4286. var inp_value = this.ksearch_input.value,
  4287. cpos = this.get_caret_pos(this.ksearch_input),
  4288. p = inp_value.lastIndexOf(this.ksearch_value, cpos),
  4289. trigger = false,
  4290. insert = '',
  4291. // replace search string with full address
  4292. pre = inp_value.substring(0, p),
  4293. end = inp_value.substring(p+this.ksearch_value.length, inp_value.length);
  4294. this.ksearch_destroy();
  4295. // insert all members of a group
  4296. if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].type == 'group' && !this.env.contacts[id].email) {
  4297. insert += this.env.contacts[id].name + this.env.recipients_delimiter;
  4298. this.group2expand[this.env.contacts[id].id] = $.extend({ input: this.ksearch_input }, this.env.contacts[id]);
  4299. this.http_request('mail/group-expand', {_source: this.env.contacts[id].source, _gid: this.env.contacts[id].id}, false);
  4300. }
  4301. else if (typeof this.env.contacts[id] === 'object' && this.env.contacts[id].name) {
  4302. insert = this.env.contacts[id].name + this.env.recipients_delimiter;
  4303. trigger = true;
  4304. }
  4305. else if (typeof this.env.contacts[id] === 'string') {
  4306. insert = this.env.contacts[id] + this.env.recipients_delimiter;
  4307. trigger = true;
  4308. }
  4309. this.ksearch_input.value = pre + insert + end;
  4310. // set caret to insert pos
  4311. this.set_caret_pos(this.ksearch_input, p + insert.length);
  4312. if (trigger) {
  4313. this.triggerEvent('autocomplete_insert', { field:this.ksearch_input, insert:insert, data:this.env.contacts[id] });
  4314. this.compose_type_activity++;
  4315. }
  4316. };
  4317. this.replace_group_recipients = function(id, recipients)
  4318. {
  4319. if (this.group2expand[id]) {
  4320. this.group2expand[id].input.value = this.group2expand[id].input.value.replace(this.group2expand[id].name, recipients);
  4321. this.triggerEvent('autocomplete_insert', { field:this.group2expand[id].input, insert:recipients });
  4322. this.group2expand[id] = null;
  4323. this.compose_type_activity++;
  4324. }
  4325. };
  4326. // address search processor
  4327. this.ksearch_get_results = function(props)
  4328. {
  4329. var inp_value = this.ksearch_input ? this.ksearch_input.value : null;
  4330. if (inp_value === null)
  4331. return;
  4332. if (this.ksearch_pane && this.ksearch_pane.is(":visible"))
  4333. this.ksearch_pane.hide();
  4334. // get string from current cursor pos to last comma
  4335. var cpos = this.get_caret_pos(this.ksearch_input),
  4336. p = inp_value.lastIndexOf(this.env.recipients_separator, cpos-1),
  4337. q = inp_value.substring(p+1, cpos),
  4338. min = this.env.autocomplete_min_length,
  4339. data = this.ksearch_data;
  4340. // trim query string
  4341. q = $.trim(q);
  4342. // Don't (re-)search if the last results are still active
  4343. if (q == this.ksearch_value)
  4344. return;
  4345. this.ksearch_destroy();
  4346. if (q.length && q.length < min) {
  4347. if (!this.ksearch_info) {
  4348. this.ksearch_info = this.display_message(
  4349. this.get_label('autocompletechars').replace('$min', min));
  4350. }
  4351. return;
  4352. }
  4353. var old_value = this.ksearch_value;
  4354. this.ksearch_value = q;
  4355. // ...string is empty
  4356. if (!q.length)
  4357. return;
  4358. // ...new search value contains old one and previous search was not finished or its result was empty
  4359. if (old_value && old_value.length && q.startsWith(old_value) && (!data || data.num <= 0) && this.env.contacts && !this.env.contacts.length)
  4360. return;
  4361. var sources = props && props.sources ? props.sources : [''];
  4362. var reqid = this.multi_thread_http_request({
  4363. items: sources,
  4364. threads: props && props.threads ? props.threads : 1,
  4365. action: props && props.action ? props.action : 'mail/autocomplete',
  4366. postdata: { _search:q, _source:'%s' },
  4367. lock: this.display_message(this.get_label('searching'), 'loading')
  4368. });
  4369. this.ksearch_data = { id:reqid, sources:sources.slice(), num:sources.length };
  4370. };
  4371. this.ksearch_query_results = function(results, search, reqid)
  4372. {
  4373. // trigger multi-thread http response callback
  4374. this.multi_thread_http_response(results, reqid);
  4375. // search stopped in meantime?
  4376. if (!this.ksearch_value)
  4377. return;
  4378. // ignore this outdated search response
  4379. if (this.ksearch_input && search != this.ksearch_value)
  4380. return;
  4381. // display search results
  4382. var i, id, len, ul, text, type, init,
  4383. value = this.ksearch_value,
  4384. maxlen = this.env.autocomplete_max ? this.env.autocomplete_max : 15;
  4385. // create results pane if not present
  4386. if (!this.ksearch_pane) {
  4387. ul = $('<ul>');
  4388. this.ksearch_pane = $('<div>').attr('id', 'rcmKSearchpane').attr('role', 'listbox')
  4389. .css({ position:'absolute', 'z-index':30000 }).append(ul).appendTo(document.body);
  4390. this.ksearch_pane.__ul = ul[0];
  4391. }
  4392. ul = this.ksearch_pane.__ul;
  4393. // remove all search results or add to existing list if parallel search
  4394. if (reqid && this.ksearch_pane.data('reqid') == reqid) {
  4395. maxlen -= ul.childNodes.length;
  4396. }
  4397. else {
  4398. this.ksearch_pane.data('reqid', reqid);
  4399. init = 1;
  4400. // reset content
  4401. ul.innerHTML = '';
  4402. this.env.contacts = [];
  4403. // move the results pane right under the input box
  4404. var pos = $(this.ksearch_input).offset();
  4405. this.ksearch_pane.css({ left:pos.left+'px', top:(pos.top + this.ksearch_input.offsetHeight)+'px', display: 'none'});
  4406. }
  4407. // add each result line to list
  4408. if (results && (len = results.length)) {
  4409. for (i=0; i < len && maxlen > 0; i++) {
  4410. text = typeof results[i] === 'object' ? (results[i].display || results[i].name) : results[i];
  4411. type = typeof results[i] === 'object' ? results[i].type : '';
  4412. id = i + this.env.contacts.length;
  4413. $('<li>').attr('id', 'rcmkSearchItem' + id)
  4414. .attr('role', 'option')
  4415. .html('<i class="icon"></i>' + this.quote_html(text.replace(new RegExp('('+RegExp.escape(value)+')', 'ig'), '##$1%%')).replace(/##([^%]+)%%/g, '<b>$1</b>'))
  4416. .addClass(type || '')
  4417. .appendTo(ul)
  4418. .mouseover(function() { ref.ksearch_select(this); })
  4419. .mouseup(function() { ref.ksearch_click(this); })
  4420. .get(0)._rcm_id = id;
  4421. maxlen -= 1;
  4422. }
  4423. }
  4424. if (ul.childNodes.length) {
  4425. // set the right aria-* attributes to the input field
  4426. $(this.ksearch_input)
  4427. .attr('aria-haspopup', 'true')
  4428. .attr('aria-expanded', 'true')
  4429. .attr('aria-owns', 'rcmKSearchpane');
  4430. this.ksearch_pane.show();
  4431. // select the first
  4432. if (!this.env.contacts.length) {
  4433. this.ksearch_select($('li:first', ul).get(0));
  4434. }
  4435. }
  4436. if (len)
  4437. this.env.contacts = this.env.contacts.concat(results);
  4438. if (this.ksearch_data.id == reqid)
  4439. this.ksearch_data.num--;
  4440. };
  4441. this.ksearch_click = function(node)
  4442. {
  4443. if (this.ksearch_input)
  4444. this.ksearch_input.focus();
  4445. this.insert_recipient(node._rcm_id);
  4446. this.ksearch_hide();
  4447. };
  4448. this.ksearch_blur = function()
  4449. {
  4450. if (this.ksearch_timer)
  4451. clearTimeout(this.ksearch_timer);
  4452. this.ksearch_input = null;
  4453. this.ksearch_hide();
  4454. };
  4455. this.ksearch_hide = function()
  4456. {
  4457. this.ksearch_selected = null;
  4458. this.ksearch_value = '';
  4459. if (this.ksearch_pane)
  4460. this.ksearch_pane.hide();
  4461. $(this.ksearch_input)
  4462. .attr('aria-haspopup', 'false')
  4463. .attr('aria-expanded', 'false')
  4464. .removeAttr('aria-activedescendant')
  4465. .removeAttr('aria-owns');
  4466. this.ksearch_destroy();
  4467. };
  4468. // Clears autocomplete data/requests
  4469. this.ksearch_destroy = function()
  4470. {
  4471. if (this.ksearch_data)
  4472. this.multi_thread_request_abort(this.ksearch_data.id);
  4473. if (this.ksearch_info)
  4474. this.hide_message(this.ksearch_info);
  4475. if (this.ksearch_msg)
  4476. this.hide_message(this.ksearch_msg);
  4477. this.ksearch_data = null;
  4478. this.ksearch_info = null;
  4479. this.ksearch_msg = null;
  4480. };
  4481. /*********************************************************/
  4482. /********* address book methods *********/
  4483. /*********************************************************/
  4484. this.contactlist_keypress = function(list)
  4485. {
  4486. if (list.key_pressed == list.DELETE_KEY)
  4487. this.command('delete');
  4488. };
  4489. this.contactlist_select = function(list)
  4490. {
  4491. if (this.preview_timer)
  4492. clearTimeout(this.preview_timer);
  4493. var n, id, sid, contact, writable = false,
  4494. selected = list.selection.length,
  4495. source = this.env.source ? this.env.address_sources[this.env.source] : null;
  4496. // we don't have dblclick handler here, so use 200 instead of this.dblclick_time
  4497. if (this.env.contentframe && (id = list.get_single_selection()))
  4498. this.preview_timer = setTimeout(function(){ ref.load_contact(id, 'show'); }, 200);
  4499. else if (this.env.contentframe)
  4500. this.show_contentframe(false);
  4501. if (selected) {
  4502. list.draggable = false;
  4503. // no source = search result, we'll need to detect if any of
  4504. // selected contacts are in writable addressbook to enable edit/delete
  4505. // we'll also need to know sources used in selection for copy
  4506. // and group-addmember operations (drag&drop)
  4507. this.env.selection_sources = [];
  4508. if (source) {
  4509. this.env.selection_sources.push(this.env.source);
  4510. }
  4511. for (n in list.selection) {
  4512. contact = list.data[list.selection[n]];
  4513. if (!source) {
  4514. sid = String(list.selection[n]).replace(/^[^-]+-/, '');
  4515. if (sid && this.env.address_sources[sid]) {
  4516. writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
  4517. this.env.selection_sources.push(sid);
  4518. }
  4519. }
  4520. else {
  4521. writable = writable || (!source.readonly && !contact.readonly);
  4522. }
  4523. if (contact._type != 'group')
  4524. list.draggable = true;
  4525. }
  4526. this.env.selection_sources = $.unique(this.env.selection_sources);
  4527. }
  4528. // if a group is currently selected, and there is at least one contact selected
  4529. // thend we can enable the group-remove-selected command
  4530. this.enable_command('group-remove-selected', this.env.group && selected && writable);
  4531. this.enable_command('compose', this.env.group || selected);
  4532. this.enable_command('print', selected == 1);
  4533. this.enable_command('export-selected', 'copy', selected > 0);
  4534. this.enable_command('edit', id && writable);
  4535. this.enable_command('delete', 'move', selected && writable);
  4536. return false;
  4537. };
  4538. this.list_contacts = function(src, group, page)
  4539. {
  4540. var win, folder, url = {},
  4541. refresh = src === undefined && group === undefined && page === undefined,
  4542. target = window;
  4543. if (!src)
  4544. src = this.env.source;
  4545. if (refresh)
  4546. group = this.env.group;
  4547. if (page && this.current_page == page && src == this.env.source && group == this.env.group)
  4548. return false;
  4549. if (src != this.env.source) {
  4550. page = this.env.current_page = 1;
  4551. this.reset_qsearch();
  4552. }
  4553. else if (!refresh && group != this.env.group)
  4554. page = this.env.current_page = 1;
  4555. if (this.env.search_id)
  4556. folder = 'S'+this.env.search_id;
  4557. else if (!this.env.search_request)
  4558. folder = group ? 'G'+src+group : src;
  4559. this.env.source = src;
  4560. this.env.group = group;
  4561. // truncate groups listing stack
  4562. var index = $.inArray(this.env.group, this.env.address_group_stack);
  4563. if (index < 0)
  4564. this.env.address_group_stack = [];
  4565. else
  4566. this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
  4567. // make sure the current group is on top of the stack
  4568. if (this.env.group) {
  4569. this.env.address_group_stack.push(this.env.group);
  4570. // mark the first group on the stack as selected in the directory list
  4571. folder = 'G'+src+this.env.address_group_stack[0];
  4572. }
  4573. else if (this.gui_objects.addresslist_title) {
  4574. $(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
  4575. }
  4576. if (!this.env.search_id)
  4577. this.select_folder(folder, '', true);
  4578. // load contacts remotely
  4579. if (this.gui_objects.contactslist) {
  4580. this.list_contacts_remote(src, group, page);
  4581. return;
  4582. }
  4583. if (win = this.get_frame_window(this.env.contentframe)) {
  4584. target = win;
  4585. url._framed = 1;
  4586. }
  4587. if (group)
  4588. url._gid = group;
  4589. if (page)
  4590. url._page = page;
  4591. if (src)
  4592. url._source = src;
  4593. // also send search request to get the correct listing
  4594. if (this.env.search_request)
  4595. url._search = this.env.search_request;
  4596. this.set_busy(true, 'loading');
  4597. this.location_href(url, target);
  4598. };
  4599. // send remote request to load contacts list
  4600. this.list_contacts_remote = function(src, group, page)
  4601. {
  4602. // clear message list first
  4603. this.list_contacts_clear();
  4604. // send request to server
  4605. var url = {}, lock = this.set_busy(true, 'loading');
  4606. if (src)
  4607. url._source = src;
  4608. if (page)
  4609. url._page = page;
  4610. if (group)
  4611. url._gid = group;
  4612. this.env.source = src;
  4613. this.env.group = group;
  4614. // also send search request to get the right records
  4615. if (this.env.search_request)
  4616. url._search = this.env.search_request;
  4617. this.http_request(this.env.task == 'mail' ? 'list-contacts' : 'list', url, lock);
  4618. };
  4619. this.list_contacts_clear = function()
  4620. {
  4621. this.contact_list.data = {};
  4622. this.contact_list.clear(true);
  4623. this.show_contentframe(false);
  4624. this.enable_command('delete', 'move', 'copy', 'print', false);
  4625. this.enable_command('compose', this.env.group);
  4626. };
  4627. this.set_group_prop = function(prop)
  4628. {
  4629. if (this.gui_objects.addresslist_title) {
  4630. var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents
  4631. // add link to pop back to parent group
  4632. if (this.env.address_group_stack.length > 1) {
  4633. $('<a href="#list">...</a>')
  4634. .attr('title', this.get_label('uponelevel'))
  4635. .addClass('poplink')
  4636. .appendTo(boxtitle)
  4637. .click(function(e){ return ref.command('popgroup','',this); });
  4638. boxtitle.append('&nbsp;&raquo;&nbsp;');
  4639. }
  4640. boxtitle.append($('<span>').text(prop.name));
  4641. }
  4642. this.triggerEvent('groupupdate', prop);
  4643. };
  4644. // load contact record
  4645. this.load_contact = function(cid, action, framed)
  4646. {
  4647. var win, url = {}, target = window,
  4648. rec = this.contact_list ? this.contact_list.data[cid] : null;
  4649. if (win = this.get_frame_window(this.env.contentframe)) {
  4650. url._framed = 1;
  4651. target = win;
  4652. this.show_contentframe(true);
  4653. // load dummy content, unselect selected row(s)
  4654. if (!cid)
  4655. this.contact_list.clear_selection();
  4656. this.enable_command('compose', rec && rec.email);
  4657. this.enable_command('export-selected', 'print', rec && rec._type != 'group');
  4658. }
  4659. else if (framed)
  4660. return false;
  4661. if (action && (cid || action == 'add') && !this.drag_active) {
  4662. if (this.env.group)
  4663. url._gid = this.env.group;
  4664. if (this.env.search_request)
  4665. url._search = this.env.search_request;
  4666. url._action = action;
  4667. url._source = this.env.source;
  4668. url._cid = cid;
  4669. this.location_href(url, target, true);
  4670. }
  4671. return true;
  4672. };
  4673. // add/delete member to/from the group
  4674. this.group_member_change = function(what, cid, source, gid)
  4675. {
  4676. if (what != 'add')
  4677. what = 'del';
  4678. var label = this.get_label(what == 'add' ? 'addingmember' : 'removingmember'),
  4679. lock = this.display_message(label, 'loading'),
  4680. post_data = {_cid: cid, _source: source, _gid: gid};
  4681. this.http_post('group-'+what+'members', post_data, lock);
  4682. };
  4683. this.contacts_drag_menu = function(e, to)
  4684. {
  4685. var dest = to.type == 'group' ? to.source : to.id,
  4686. source = this.env.source;
  4687. if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
  4688. return true;
  4689. // search result may contain contacts from many sources, but if there is only one...
  4690. if (source == '' && this.env.selection_sources.length == 1)
  4691. source = this.env.selection_sources[0];
  4692. if (to.type == 'group' && dest == source) {
  4693. var cid = this.contact_list.get_selection().join(',');
  4694. this.group_member_change('add', cid, dest, to.id);
  4695. return true;
  4696. }
  4697. // move action is not possible, "redirect" to copy if menu wasn't requested
  4698. else if (!this.commands.move && rcube_event.get_modifier(e) != SHIFT_KEY) {
  4699. this.copy_contacts(to);
  4700. return true;
  4701. }
  4702. return this.drag_menu(e, to);
  4703. };
  4704. // copy contact(s) to the specified target (group or directory)
  4705. this.copy_contacts = function(to)
  4706. {
  4707. var dest = to.type == 'group' ? to.source : to.id,
  4708. source = this.env.source,
  4709. group = this.env.group ? this.env.group : '',
  4710. cid = this.contact_list.get_selection().join(',');
  4711. if (!cid || !this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
  4712. return;
  4713. // search result may contain contacts from many sources, but if there is only one...
  4714. if (source == '' && this.env.selection_sources.length == 1)
  4715. source = this.env.selection_sources[0];
  4716. // tagret is a group
  4717. if (to.type == 'group') {
  4718. if (dest == source)
  4719. return;
  4720. var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
  4721. post_data = {_cid: cid, _source: this.env.source, _to: dest, _togid: to.id, _gid: group};
  4722. this.http_post('copy', post_data, lock);
  4723. }
  4724. // target is an addressbook
  4725. else if (to.id != source) {
  4726. var lock = this.display_message(this.get_label('copyingcontact'), 'loading'),
  4727. post_data = {_cid: cid, _source: this.env.source, _to: to.id, _gid: group};
  4728. this.http_post('copy', post_data, lock);
  4729. }
  4730. };
  4731. // move contact(s) to the specified target (group or directory)
  4732. this.move_contacts = function(to)
  4733. {
  4734. var dest = to.type == 'group' ? to.source : to.id,
  4735. source = this.env.source,
  4736. group = this.env.group ? this.env.group : '';
  4737. if (!this.env.address_sources[dest] || this.env.address_sources[dest].readonly)
  4738. return;
  4739. // search result may contain contacts from many sources, but if there is only one...
  4740. if (source == '' && this.env.selection_sources.length == 1)
  4741. source = this.env.selection_sources[0];
  4742. if (to.type == 'group') {
  4743. if (dest == source)
  4744. return;
  4745. this._with_selected_contacts('move', {_to: dest, _togid: to.id});
  4746. }
  4747. // target is an addressbook
  4748. else if (to.id != source)
  4749. this._with_selected_contacts('move', {_to: to.id});
  4750. };
  4751. // delete contact(s)
  4752. this.delete_contacts = function()
  4753. {
  4754. var undelete = this.env.source && this.env.address_sources[this.env.source].undelete;
  4755. if (!undelete && !confirm(this.get_label('deletecontactconfirm')))
  4756. return;
  4757. return this._with_selected_contacts('delete');
  4758. };
  4759. this._with_selected_contacts = function(action, post_data)
  4760. {
  4761. var selection = this.contact_list ? this.contact_list.get_selection() : [];
  4762. // exit if no contact specified or if selection is empty
  4763. if (!selection.length && !this.env.cid)
  4764. return;
  4765. var n, a_cids = [],
  4766. label = action == 'delete' ? 'contactdeleting' : 'movingcontact',
  4767. lock = this.display_message(this.get_label(label), 'loading');
  4768. if (this.env.cid)
  4769. a_cids.push(this.env.cid);
  4770. else {
  4771. for (n=0; n<selection.length; n++) {
  4772. id = selection[n];
  4773. a_cids.push(id);
  4774. this.contact_list.remove_row(id, (n == selection.length-1));
  4775. }
  4776. // hide content frame if we delete the currently displayed contact
  4777. if (selection.length == 1)
  4778. this.show_contentframe(false);
  4779. }
  4780. if (!post_data)
  4781. post_data = {};
  4782. post_data._source = this.env.source;
  4783. post_data._from = this.env.action;
  4784. post_data._cid = a_cids.join(',');
  4785. if (this.env.group)
  4786. post_data._gid = this.env.group;
  4787. // also send search request to get the right records from the next page
  4788. if (this.env.search_request)
  4789. post_data._search = this.env.search_request;
  4790. // send request to server
  4791. this.http_post(action, post_data, lock)
  4792. return true;
  4793. };
  4794. // update a contact record in the list
  4795. this.update_contact_row = function(cid, cols_arr, newcid, source, data)
  4796. {
  4797. var list = this.contact_list;
  4798. cid = this.html_identifier(cid);
  4799. // when in searching mode, concat cid with the source name
  4800. if (!list.rows[cid]) {
  4801. cid = cid + '-' + source;
  4802. if (newcid)
  4803. newcid = newcid + '-' + source;
  4804. }
  4805. list.update_row(cid, cols_arr, newcid, true);
  4806. list.data[cid] = data;
  4807. };
  4808. // add row to contacts list
  4809. this.add_contact_row = function(cid, cols, classes, data)
  4810. {
  4811. if (!this.gui_objects.contactslist)
  4812. return false;
  4813. var c, col, list = this.contact_list,
  4814. row = { cols:[] };
  4815. row.id = 'rcmrow' + this.html_identifier(cid);
  4816. row.className = 'contact ' + (classes || '');
  4817. if (list.in_selection(cid))
  4818. row.className += ' selected';
  4819. // add each submitted col
  4820. for (c in cols) {
  4821. col = {};
  4822. col.className = String(c).toLowerCase();
  4823. col.innerHTML = cols[c];
  4824. row.cols.push(col);
  4825. }
  4826. // store data in list member
  4827. list.data[cid] = data;
  4828. list.insert_row(row);
  4829. this.enable_command('export', list.rowcount > 0);
  4830. };
  4831. this.init_contact_form = function()
  4832. {
  4833. var col;
  4834. if (this.env.coltypes) {
  4835. this.set_photo_actions($('#ff_photo').val());
  4836. for (col in this.env.coltypes)
  4837. this.init_edit_field(col, null);
  4838. }
  4839. $('.contactfieldgroup .row a.deletebutton').click(function() {
  4840. ref.delete_edit_field(this);
  4841. return false;
  4842. });
  4843. $('select.addfieldmenu').change(function() {
  4844. ref.insert_edit_field($(this).val(), $(this).attr('rel'), this);
  4845. this.selectedIndex = 0;
  4846. });
  4847. // enable date pickers on date fields
  4848. if ($.datepicker && this.env.date_format) {
  4849. $.datepicker.setDefaults({
  4850. dateFormat: this.env.date_format,
  4851. changeMonth: true,
  4852. changeYear: true,
  4853. yearRange: '-120:+10',
  4854. showOtherMonths: true,
  4855. selectOtherMonths: true
  4856. // onSelect: function(dateText) { $(this).focus().val(dateText); }
  4857. });
  4858. $('input.datepicker').datepicker();
  4859. }
  4860. // Submit search form on Enter
  4861. if (this.env.action == 'search')
  4862. $(this.gui_objects.editform).append($('<input type="submit">').hide())
  4863. .submit(function() { $('input.mainaction').click(); return false; });
  4864. };
  4865. // group creation dialog
  4866. this.group_create = function()
  4867. {
  4868. var input = $('<input>').attr('type', 'text'),
  4869. content = $('<label>').text(this.get_label('namex')).append(input);
  4870. this.show_popup_dialog(content, this.get_label('newgroup'),
  4871. [{
  4872. text: this.get_label('save'),
  4873. 'class': 'mainaction',
  4874. click: function() {
  4875. var name;
  4876. if (name = input.val()) {
  4877. ref.http_post('group-create', {_source: ref.env.source, _name: name},
  4878. ref.set_busy(true, 'loading'));
  4879. }
  4880. $(this).dialog('close');
  4881. }
  4882. }]
  4883. );
  4884. };
  4885. // group rename dialog
  4886. this.group_rename = function()
  4887. {
  4888. if (!this.env.group)
  4889. return;
  4890. var group_name = this.env.contactgroups['G' + this.env.source + this.env.group].name,
  4891. input = $('<input>').attr('type', 'text').val(group_name),
  4892. content = $('<label>').text(this.get_label('namex')).append(input);
  4893. this.show_popup_dialog(content, this.get_label('grouprename'),
  4894. [{
  4895. text: this.get_label('save'),
  4896. 'class': 'mainaction',
  4897. click: function() {
  4898. var name;
  4899. if ((name = input.val()) && name != group_name) {
  4900. ref.http_post('group-rename', {_source: ref.env.source, _gid: ref.env.group, _name: name},
  4901. ref.set_busy(true, 'loading'));
  4902. }
  4903. $(this).dialog('close');
  4904. }
  4905. }],
  4906. {open: function() { input.select(); }}
  4907. );
  4908. };
  4909. this.group_delete = function()
  4910. {
  4911. if (this.env.group && confirm(this.get_label('deletegroupconfirm'))) {
  4912. var lock = this.set_busy(true, 'groupdeleting');
  4913. this.http_post('group-delete', {_source: this.env.source, _gid: this.env.group}, lock);
  4914. }
  4915. };
  4916. // callback from server upon group-delete command
  4917. this.remove_group_item = function(prop)
  4918. {
  4919. var key = 'G'+prop.source+prop.id;
  4920. if (this.treelist.remove(key)) {
  4921. this.triggerEvent('group_delete', { source:prop.source, id:prop.id });
  4922. delete this.env.contactfolders[key];
  4923. delete this.env.contactgroups[key];
  4924. }
  4925. this.list_contacts(prop.source, 0);
  4926. };
  4927. //remove selected contacts from current active group
  4928. this.group_remove_selected = function()
  4929. {
  4930. this.http_post('group-delmembers', {_cid: this.contact_list.selection,
  4931. _source: this.env.source, _gid: this.env.group});
  4932. };
  4933. //callback after deleting contact(s) from current group
  4934. this.remove_group_contacts = function(props)
  4935. {
  4936. if (this.env.group !== undefined && (this.env.group === props.gid)) {
  4937. var n, selection = this.contact_list.get_selection();
  4938. for (n=0; n<selection.length; n++) {
  4939. id = selection[n];
  4940. this.contact_list.remove_row(id, (n == selection.length-1));
  4941. }
  4942. }
  4943. };
  4944. // callback for creating a new contact group
  4945. this.insert_contact_group = function(prop)
  4946. {
  4947. prop.type = 'group';
  4948. var key = 'G'+prop.source+prop.id,
  4949. link = $('<a>').attr('href', '#')
  4950. .attr('rel', prop.source+':'+prop.id)
  4951. .click(function() { return ref.command('listgroup', prop, this); })
  4952. .html(prop.name);
  4953. this.env.contactfolders[key] = this.env.contactgroups[key] = prop;
  4954. this.treelist.insert({ id:key, html:link, classes:['contactgroup'] }, prop.source, 'contactgroup');
  4955. this.triggerEvent('group_insert', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key) });
  4956. };
  4957. // callback for renaming a contact group
  4958. this.update_contact_group = function(prop)
  4959. {
  4960. var key = 'G'+prop.source+prop.id,
  4961. newnode = {};
  4962. // group ID has changed, replace link node and identifiers
  4963. if (prop.newid) {
  4964. var newkey = 'G'+prop.source+prop.newid,
  4965. newprop = $.extend({}, prop);
  4966. this.env.contactfolders[newkey] = this.env.contactfolders[key];
  4967. this.env.contactfolders[newkey].id = prop.newid;
  4968. this.env.group = prop.newid;
  4969. delete this.env.contactfolders[key];
  4970. delete this.env.contactgroups[key];
  4971. newprop.id = prop.newid;
  4972. newprop.type = 'group';
  4973. newnode.id = newkey;
  4974. newnode.html = $('<a>').attr('href', '#')
  4975. .attr('rel', prop.source+':'+prop.newid)
  4976. .click(function() { return ref.command('listgroup', newprop, this); })
  4977. .html(prop.name);
  4978. }
  4979. // update displayed group name
  4980. else {
  4981. $(this.treelist.get_item(key)).children().first().html(prop.name);
  4982. this.env.contactfolders[key].name = this.env.contactgroups[key].name = prop.name;
  4983. }
  4984. // update list node and re-sort it
  4985. this.treelist.update(key, newnode, true);
  4986. this.triggerEvent('group_update', { id:prop.id, source:prop.source, name:prop.name, li:this.treelist.get_item(key), newid:prop.newid });
  4987. };
  4988. this.update_group_commands = function()
  4989. {
  4990. var source = this.env.source != '' ? this.env.address_sources[this.env.source] : null,
  4991. supported = source && source.groups && !source.readonly;
  4992. this.enable_command('group-create', supported);
  4993. this.enable_command('group-rename', 'group-delete', supported && this.env.group);
  4994. };
  4995. this.init_edit_field = function(col, elem)
  4996. {
  4997. var label = this.env.coltypes[col].label;
  4998. if (!elem)
  4999. elem = $('.ff_' + col);
  5000. if (label)
  5001. elem.placeholder(label);
  5002. };
  5003. this.insert_edit_field = function(col, section, menu)
  5004. {
  5005. // just make pre-defined input field visible
  5006. var elem = $('#ff_'+col);
  5007. if (elem.length) {
  5008. elem.show().focus();
  5009. $(menu).children('option[value="'+col+'"]').prop('disabled', true);
  5010. }
  5011. else {
  5012. var lastelem = $('.ff_'+col),
  5013. appendcontainer = $('#contactsection'+section+' .contactcontroller'+col);
  5014. if (!appendcontainer.length) {
  5015. var sect = $('#contactsection'+section),
  5016. lastgroup = $('.contactfieldgroup', sect).last();
  5017. appendcontainer = $('<fieldset>').addClass('contactfieldgroup contactcontroller'+col);
  5018. if (lastgroup.length)
  5019. appendcontainer.insertAfter(lastgroup);
  5020. else
  5021. sect.prepend(appendcontainer);
  5022. }
  5023. if (appendcontainer.length && appendcontainer.get(0).nodeName == 'FIELDSET') {
  5024. var input, colprop = this.env.coltypes[col],
  5025. input_id = 'ff_' + col + (colprop.count || 0),
  5026. row = $('<div>').addClass('row'),
  5027. cell = $('<div>').addClass('contactfieldcontent data'),
  5028. label = $('<div>').addClass('contactfieldlabel label');
  5029. if (colprop.subtypes_select)
  5030. label.html(colprop.subtypes_select);
  5031. else
  5032. label.html('<label for="' + input_id + '">' + colprop.label + '</label>');
  5033. var name_suffix = colprop.limit != 1 ? '[]' : '';
  5034. if (colprop.type == 'text' || colprop.type == 'date') {
  5035. input = $('<input>')
  5036. .addClass('ff_'+col)
  5037. .attr({type: 'text', name: '_'+col+name_suffix, size: colprop.size, id: input_id})
  5038. .appendTo(cell);
  5039. this.init_edit_field(col, input);
  5040. if (colprop.type == 'date' && $.datepicker)
  5041. input.datepicker();
  5042. }
  5043. else if (colprop.type == 'textarea') {
  5044. input = $('<textarea>')
  5045. .addClass('ff_'+col)
  5046. .attr({ name: '_'+col+name_suffix, cols:colprop.size, rows:colprop.rows, id: input_id })
  5047. .appendTo(cell);
  5048. this.init_edit_field(col, input);
  5049. }
  5050. else if (colprop.type == 'composite') {
  5051. var i, childcol, cp, first, templ, cols = [], suffices = [];
  5052. // read template for composite field order
  5053. if ((templ = this.env[col+'_template'])) {
  5054. for (i=0; i < templ.length; i++) {
  5055. cols.push(templ[i][1]);
  5056. suffices.push(templ[i][2]);
  5057. }
  5058. }
  5059. else { // list fields according to appearance in colprop
  5060. for (childcol in colprop.childs)
  5061. cols.push(childcol);
  5062. }
  5063. for (i=0; i < cols.length; i++) {
  5064. childcol = cols[i];
  5065. cp = colprop.childs[childcol];
  5066. input = $('<input>')
  5067. .addClass('ff_'+childcol)
  5068. .attr({ type: 'text', name: '_'+childcol+name_suffix, size: cp.size })
  5069. .appendTo(cell);
  5070. cell.append(suffices[i] || " ");
  5071. this.init_edit_field(childcol, input);
  5072. if (!first) first = input;
  5073. }
  5074. input = first; // set focus to the first of this composite fields
  5075. }
  5076. else if (colprop.type == 'select') {
  5077. input = $('<select>')
  5078. .addClass('ff_'+col)
  5079. .attr({ 'name': '_'+col+name_suffix, id: input_id })
  5080. .appendTo(cell);
  5081. var options = input.attr('options');
  5082. options[options.length] = new Option('---', '');
  5083. if (colprop.options)
  5084. $.each(colprop.options, function(i, val){ options[options.length] = new Option(val, i); });
  5085. }
  5086. if (input) {
  5087. var delbutton = $('<a href="#del"></a>')
  5088. .addClass('contactfieldbutton deletebutton')
  5089. .attr({title: this.get_label('delete'), rel: col})
  5090. .html(this.env.delbutton)
  5091. .click(function(){ ref.delete_edit_field(this); return false })
  5092. .appendTo(cell);
  5093. row.append(label).append(cell).appendTo(appendcontainer.show());
  5094. input.first().focus();
  5095. // disable option if limit reached
  5096. if (!colprop.count) colprop.count = 0;
  5097. if (++colprop.count == colprop.limit && colprop.limit)
  5098. $(menu).children('option[value="'+col+'"]').prop('disabled', true);
  5099. }
  5100. }
  5101. }
  5102. };
  5103. this.delete_edit_field = function(elem)
  5104. {
  5105. var col = $(elem).attr('rel'),
  5106. colprop = this.env.coltypes[col],
  5107. fieldset = $(elem).parents('fieldset.contactfieldgroup'),
  5108. addmenu = fieldset.parent().find('select.addfieldmenu');
  5109. // just clear input but don't hide the last field
  5110. if (--colprop.count <= 0 && colprop.visible)
  5111. $(elem).parent().children('input').val('').blur();
  5112. else {
  5113. $(elem).parents('div.row').remove();
  5114. // hide entire fieldset if no more rows
  5115. if (!fieldset.children('div.row').length)
  5116. fieldset.hide();
  5117. }
  5118. // enable option in add-field selector or insert it if necessary
  5119. if (addmenu.length) {
  5120. var option = addmenu.children('option[value="'+col+'"]');
  5121. if (option.length)
  5122. option.prop('disabled', false);
  5123. else
  5124. option = $('<option>').attr('value', col).html(colprop.label).appendTo(addmenu);
  5125. addmenu.show();
  5126. }
  5127. };
  5128. this.upload_contact_photo = function(form)
  5129. {
  5130. if (form && form.elements._photo.value) {
  5131. this.async_upload_form(form, 'upload-photo', function(e) {
  5132. ref.set_busy(false, null, ref.file_upload_id);
  5133. });
  5134. // display upload indicator
  5135. this.file_upload_id = this.set_busy(true, 'uploading');
  5136. }
  5137. };
  5138. this.replace_contact_photo = function(id)
  5139. {
  5140. var img_src = id == '-del-' ? this.env.photo_placeholder :
  5141. this.env.comm_path + '&_action=photo&_source=' + this.env.source + '&_cid=' + (this.env.cid || 0) + '&_photo=' + id;
  5142. this.set_photo_actions(id);
  5143. $(this.gui_objects.contactphoto).children('img').attr('src', img_src);
  5144. };
  5145. this.photo_upload_end = function()
  5146. {
  5147. this.set_busy(false, null, this.file_upload_id);
  5148. delete this.file_upload_id;
  5149. };
  5150. this.set_photo_actions = function(id)
  5151. {
  5152. var n, buttons = this.buttons['upload-photo'];
  5153. for (n=0; buttons && n < buttons.length; n++)
  5154. $('a#'+buttons[n].id).html(this.get_label(id == '-del-' ? 'addphoto' : 'replacephoto'));
  5155. $('#ff_photo').val(id);
  5156. this.enable_command('upload-photo', this.env.coltypes.photo ? true : false);
  5157. this.enable_command('delete-photo', this.env.coltypes.photo && id != '-del-');
  5158. };
  5159. // load advanced search page
  5160. this.advanced_search = function()
  5161. {
  5162. var win, url = {_form: 1, _action: 'search'}, target = window;
  5163. if (win = this.get_frame_window(this.env.contentframe)) {
  5164. url._framed = 1;
  5165. target = win;
  5166. this.contact_list.clear_selection();
  5167. }
  5168. this.location_href(url, target, true);
  5169. return true;
  5170. };
  5171. // unselect directory/group
  5172. this.unselect_directory = function()
  5173. {
  5174. this.select_folder('');
  5175. this.enable_command('search-delete', false);
  5176. };
  5177. // callback for creating a new saved search record
  5178. this.insert_saved_search = function(name, id)
  5179. {
  5180. var key = 'S'+id,
  5181. link = $('<a>').attr('href', '#')
  5182. .attr('rel', id)
  5183. .click(function() { return ref.command('listsearch', id, this); })
  5184. .html(name),
  5185. prop = { name:name, id:id };
  5186. this.savedsearchlist.insert({ id:key, html:link, classes:['contactsearch'] }, null, 'contactsearch');
  5187. this.select_folder(key,'',true);
  5188. this.enable_command('search-delete', true);
  5189. this.env.search_id = id;
  5190. this.triggerEvent('abook_search_insert', prop);
  5191. };
  5192. // creates a dialog for saved search
  5193. this.search_create = function()
  5194. {
  5195. var input = $('<input>').attr('type', 'text'),
  5196. content = $('<label>').text(this.get_label('namex')).append(input);
  5197. this.show_popup_dialog(content, this.get_label('searchsave'),
  5198. [{
  5199. text: this.get_label('save'),
  5200. 'class': 'mainaction',
  5201. click: function() {
  5202. var name;
  5203. if (name = input.val()) {
  5204. ref.http_post('search-create', {_search: ref.env.search_request, _name: name},
  5205. ref.set_busy(true, 'loading'));
  5206. }
  5207. $(this).dialog('close');
  5208. }
  5209. }]
  5210. );
  5211. };
  5212. this.search_delete = function()
  5213. {
  5214. if (this.env.search_request) {
  5215. var lock = this.set_busy(true, 'savedsearchdeleting');
  5216. this.http_post('search-delete', {_sid: this.env.search_id}, lock);
  5217. }
  5218. };
  5219. // callback from server upon search-delete command
  5220. this.remove_search_item = function(id)
  5221. {
  5222. var li, key = 'S'+id;
  5223. if (this.savedsearchlist.remove(key)) {
  5224. this.triggerEvent('search_delete', { id:id, li:li });
  5225. }
  5226. this.env.search_id = null;
  5227. this.env.search_request = null;
  5228. this.list_contacts_clear();
  5229. this.reset_qsearch();
  5230. this.enable_command('search-delete', 'search-create', false);
  5231. };
  5232. this.listsearch = function(id)
  5233. {
  5234. var lock = this.set_busy(true, 'searching');
  5235. if (this.contact_list) {
  5236. this.list_contacts_clear();
  5237. }
  5238. this.reset_qsearch();
  5239. if (this.savedsearchlist) {
  5240. this.treelist.select('');
  5241. this.savedsearchlist.select('S'+id);
  5242. }
  5243. else
  5244. this.select_folder('S'+id, '', true);
  5245. // reset vars
  5246. this.env.current_page = 1;
  5247. this.http_request('search', {_sid: id}, lock);
  5248. };
  5249. /*********************************************************/
  5250. /********* user settings methods *********/
  5251. /*********************************************************/
  5252. // preferences section select and load options frame
  5253. this.section_select = function(list)
  5254. {
  5255. var win, id = list.get_single_selection(), target = window,
  5256. url = {_action: 'edit-prefs', _section: id};
  5257. if (id) {
  5258. if (win = this.get_frame_window(this.env.contentframe)) {
  5259. url._framed = 1;
  5260. target = win;
  5261. }
  5262. this.location_href(url, target, true);
  5263. }
  5264. return true;
  5265. };
  5266. this.identity_select = function(list)
  5267. {
  5268. var id;
  5269. if (id = list.get_single_selection()) {
  5270. this.enable_command('delete', list.rowcount > 1 && this.env.identities_level < 2);
  5271. this.load_identity(id, 'edit-identity');
  5272. }
  5273. };
  5274. // load identity record
  5275. this.load_identity = function(id, action)
  5276. {
  5277. if (action == 'edit-identity' && (!id || id == this.env.iid))
  5278. return false;
  5279. var win, target = window,
  5280. url = {_action: action, _iid: id};
  5281. if (win = this.get_frame_window(this.env.contentframe)) {
  5282. url._framed = 1;
  5283. target = win;
  5284. }
  5285. if (id || action == 'add-identity') {
  5286. this.location_href(url, target, true);
  5287. }
  5288. return true;
  5289. };
  5290. this.delete_identity = function(id)
  5291. {
  5292. // exit if no identity is specified or if selection is empty
  5293. var selection = this.identity_list.get_selection();
  5294. if (!(selection.length || this.env.iid))
  5295. return;
  5296. if (!id)
  5297. id = this.env.iid ? this.env.iid : selection[0];
  5298. // submit request with appended token
  5299. if (id && confirm(this.get_label('deleteidentityconfirm')))
  5300. this.http_post('settings/delete-identity', { _iid: id }, true);
  5301. };
  5302. this.update_identity_row = function(id, name, add)
  5303. {
  5304. var list = this.identity_list,
  5305. rid = this.html_identifier(id);
  5306. if (add) {
  5307. list.insert_row({ id:'rcmrow'+rid, cols:[ { className:'mail', innerHTML:name } ] });
  5308. list.select(rid);
  5309. }
  5310. else {
  5311. list.update_row(rid, [ name ]);
  5312. }
  5313. };
  5314. this.update_response_row = function(response, oldkey)
  5315. {
  5316. var list = this.responses_list;
  5317. if (list && oldkey) {
  5318. list.update_row(oldkey, [ response.name ], response.key, true);
  5319. }
  5320. else if (list) {
  5321. list.insert_row({ id:'rcmrow'+response.key, cols:[ { className:'name', innerHTML:response.name } ] });
  5322. list.select(response.key);
  5323. }
  5324. };
  5325. this.remove_response = function(key)
  5326. {
  5327. var frame;
  5328. if (this.env.textresponses) {
  5329. delete this.env.textresponses[key];
  5330. }
  5331. if (this.responses_list) {
  5332. this.responses_list.remove_row(key);
  5333. if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) {
  5334. frame.location.href = this.env.blankpage;
  5335. }
  5336. }
  5337. this.enable_command('delete', false);
  5338. };
  5339. this.remove_identity = function(id)
  5340. {
  5341. var frame, list = this.identity_list,
  5342. rid = this.html_identifier(id);
  5343. if (list && id) {
  5344. list.remove_row(rid);
  5345. if (this.env.contentframe && (frame = this.get_frame_window(this.env.contentframe))) {
  5346. frame.location.href = this.env.blankpage;
  5347. }
  5348. }
  5349. this.enable_command('delete', false);
  5350. };
  5351. /*********************************************************/
  5352. /********* folder manager methods *********/
  5353. /*********************************************************/
  5354. this.init_subscription_list = function()
  5355. {
  5356. var delim = RegExp.escape(this.env.delimiter);
  5357. this.last_sub_rx = RegExp('['+delim+']?[^'+delim+']+$');
  5358. this.subscription_list = new rcube_treelist_widget(this.gui_objects.subscriptionlist, {
  5359. selectable: true,
  5360. tabexit: false,
  5361. parent_focus: true,
  5362. id_prefix: 'rcmli',
  5363. id_encode: this.html_identifier_encode,
  5364. id_decode: this.html_identifier_decode,
  5365. searchbox: '#foldersearch'
  5366. });
  5367. this.subscription_list
  5368. .addEventListener('select', function(node) { ref.subscription_select(node.id); })
  5369. .addEventListener('collapse', function(node) { ref.folder_collapsed(node) })
  5370. .addEventListener('expand', function(node) { ref.folder_collapsed(node) })
  5371. .addEventListener('search', function(p) { if (p.query) ref.subscription_select(); })
  5372. .draggable({cancel: 'li.mailbox.root'})
  5373. .droppable({
  5374. // @todo: find better way, accept callback is executed for every folder
  5375. // on the list when dragging starts (and stops), this is slow, but
  5376. // I didn't find a method to check droptarget on over event
  5377. accept: function(node) {
  5378. if (!$(node).is('.mailbox'))
  5379. return false;
  5380. var source_folder = ref.folder_id2name($(node).attr('id')),
  5381. dest_folder = ref.folder_id2name(this.id),
  5382. source = ref.env.subscriptionrows[source_folder],
  5383. dest = ref.env.subscriptionrows[dest_folder];
  5384. return source && !source[2]
  5385. && dest_folder != source_folder.replace(ref.last_sub_rx, '')
  5386. && !dest_folder.startsWith(source_folder + ref.env.delimiter);
  5387. },
  5388. drop: function(e, ui) {
  5389. var source = ref.folder_id2name(ui.draggable.attr('id')),
  5390. dest = ref.folder_id2name(this.id);
  5391. ref.subscription_move_folder(source, dest);
  5392. }
  5393. });
  5394. };
  5395. this.folder_id2name = function(id)
  5396. {
  5397. return id ? ref.html_identifier_decode(id.replace(/^rcmli/, '')) : null;
  5398. };
  5399. this.subscription_select = function(id)
  5400. {
  5401. var folder;
  5402. if (id && id != '*' && (folder = this.env.subscriptionrows[id])) {
  5403. this.env.mailbox = id;
  5404. this.show_folder(id);
  5405. this.enable_command('delete-folder', !folder[2]);
  5406. }
  5407. else {
  5408. this.env.mailbox = null;
  5409. this.show_contentframe(false);
  5410. this.enable_command('delete-folder', 'purge', false);
  5411. }
  5412. };
  5413. this.subscription_move_folder = function(from, to)
  5414. {
  5415. if (from && to !== null && from != to && to != from.replace(this.last_sub_rx, '')) {
  5416. var path = from.split(this.env.delimiter),
  5417. basename = path.pop(),
  5418. newname = to === '' || to === '*' ? basename : to + this.env.delimiter + basename;
  5419. if (newname != from) {
  5420. this.http_post('rename-folder', {_folder_oldname: from, _folder_newname: newname},
  5421. this.set_busy(true, 'foldermoving'));
  5422. }
  5423. }
  5424. };
  5425. // tell server to create and subscribe a new mailbox
  5426. this.create_folder = function()
  5427. {
  5428. this.show_folder('', this.env.mailbox);
  5429. };
  5430. // delete a specific mailbox with all its messages
  5431. this.delete_folder = function(name)
  5432. {
  5433. if (!name)
  5434. name = this.env.mailbox;
  5435. if (name && confirm(this.get_label('deletefolderconfirm'))) {
  5436. this.http_post('delete-folder', {_mbox: name}, this.set_busy(true, 'folderdeleting'));
  5437. }
  5438. };
  5439. // Add folder row to the table and initialize it
  5440. this.add_folder_row = function (id, name, display_name, is_protected, subscribed, class_name, refrow, subfolders)
  5441. {
  5442. if (!this.gui_objects.subscriptionlist)
  5443. return false;
  5444. // reset searching
  5445. if (this.subscription_list.is_search()) {
  5446. this.subscription_select();
  5447. this.subscription_list.reset_search();
  5448. }
  5449. // disable drag-n-drop temporarily
  5450. this.subscription_list.draggable('destroy').droppable('destroy');
  5451. var row, n, tmp, tmp_name, rowid, collator, pos, p, parent = '',
  5452. folders = [], list = [], slist = [],
  5453. list_element = $(this.gui_objects.subscriptionlist);
  5454. row = refrow ? refrow : $($('li', list_element).get(1)).clone(true);
  5455. if (!row.length) {
  5456. // Refresh page if we don't have a table row to clone
  5457. this.goto_url('folders');
  5458. return false;
  5459. }
  5460. // set ID, reset css class
  5461. row.attr({id: 'rcmli' + this.html_identifier_encode(id), 'class': class_name});
  5462. if (!refrow || !refrow.length) {
  5463. // remove old data, subfolders and toggle
  5464. $('ul,div.treetoggle', row).remove();
  5465. row.removeData('filtered');
  5466. }
  5467. // set folder name
  5468. $('a:first', row).text(display_name);
  5469. // update subscription checkbox
  5470. $('input[name="_subscribed[]"]:first', row).val(id)
  5471. .prop({checked: subscribed ? true : false, disabled: is_protected ? true : false});
  5472. // add to folder/row-ID map
  5473. this.env.subscriptionrows[id] = [name, display_name, false];
  5474. // copy folders data to an array for sorting
  5475. $.each(this.env.subscriptionrows, function(k, v) { v[3] = k; folders.push(v); });
  5476. try {
  5477. // use collator if supported (FF29, IE11, Opera15, Chrome24)
  5478. collator = new Intl.Collator(this.env.locale.replace('_', '-'));
  5479. }
  5480. catch (e) {};
  5481. // sort folders
  5482. folders.sort(function(a, b) {
  5483. var i, f1, f2,
  5484. path1 = a[0].split(ref.env.delimiter),
  5485. path2 = b[0].split(ref.env.delimiter),
  5486. len = path1.length;
  5487. for (i=0; i<len; i++) {
  5488. f1 = path1[i];
  5489. f2 = path2[i];
  5490. if (f1 !== f2) {
  5491. if (f2 === undefined)
  5492. return 1;
  5493. if (collator)
  5494. return collator.compare(f1, f2);
  5495. else
  5496. return f1 < f2 ? -1 : 1;
  5497. }
  5498. else if (i == len-1) {
  5499. return -1
  5500. }
  5501. }
  5502. });
  5503. for (n in folders) {
  5504. p = folders[n][3];
  5505. // protected folder
  5506. if (folders[n][2]) {
  5507. tmp_name = p + this.env.delimiter;
  5508. // prefix namespace cannot have subfolders (#1488349)
  5509. if (tmp_name == this.env.prefix_ns)
  5510. continue;
  5511. slist.push(p);
  5512. tmp = tmp_name;
  5513. }
  5514. // protected folder's child
  5515. else if (tmp && p.startsWith(tmp))
  5516. slist.push(p);
  5517. // other
  5518. else {
  5519. list.push(p);
  5520. tmp = null;
  5521. }
  5522. }
  5523. // check if subfolder of a protected folder
  5524. for (n=0; n<slist.length; n++) {
  5525. if (id.startsWith(slist[n] + this.env.delimiter))
  5526. rowid = slist[n];
  5527. }
  5528. // find folder position after sorting
  5529. for (n=0; !rowid && n<list.length; n++) {
  5530. if (n && list[n] == id)
  5531. rowid = list[n-1];
  5532. }
  5533. // add row to the table
  5534. if (rowid && (n = this.subscription_list.get_item(rowid, true))) {
  5535. // find parent folder
  5536. if (pos = id.lastIndexOf(this.env.delimiter)) {
  5537. parent = id.substring(0, pos);
  5538. parent = this.subscription_list.get_item(parent, true);
  5539. // add required tree elements to the parent if not already there
  5540. if (!$('div.treetoggle', parent).length) {
  5541. $('<div>&nbsp;</div>').addClass('treetoggle collapsed').appendTo(parent);
  5542. }
  5543. if (!$('ul', parent).length) {
  5544. $('<ul>').css('display', 'none').appendTo(parent);
  5545. }
  5546. }
  5547. if (parent && n == parent) {
  5548. $('ul:first', parent).append(row);
  5549. }
  5550. else {
  5551. while (p = $(n).parent().parent().get(0)) {
  5552. if (parent && p == parent)
  5553. break;
  5554. if (!$(p).is('li.mailbox'))
  5555. break;
  5556. n = p;
  5557. }
  5558. $(n).after(row);
  5559. }
  5560. }
  5561. else {
  5562. list_element.append(row);
  5563. }
  5564. // add subfolders
  5565. $.extend(this.env.subscriptionrows, subfolders || {});
  5566. // update list widget
  5567. this.subscription_list.reset(true);
  5568. this.subscription_select();
  5569. // expand parent
  5570. if (parent) {
  5571. this.subscription_list.expand(this.folder_id2name(parent.id));
  5572. }
  5573. row = row.show().get(0);
  5574. if (row.scrollIntoView)
  5575. row.scrollIntoView();
  5576. return row;
  5577. };
  5578. // replace an existing table row with a new folder line (with subfolders)
  5579. this.replace_folder_row = function(oldid, id, name, display_name, is_protected, class_name)
  5580. {
  5581. if (!this.gui_objects.subscriptionlist) {
  5582. if (this.is_framed()) {
  5583. // @FIXME: for some reason this 'parent' variable need to be prefixed with 'window.'
  5584. return window.parent.rcmail.replace_folder_row(oldid, id, name, display_name, is_protected, class_name);
  5585. }
  5586. return false;
  5587. }
  5588. // reset searching
  5589. if (this.subscription_list.is_search()) {
  5590. this.subscription_select();
  5591. this.subscription_list.reset_search();
  5592. }
  5593. var subfolders = {},
  5594. row = this.subscription_list.get_item(oldid, true),
  5595. parent = $(row).parent(),
  5596. old_folder = this.env.subscriptionrows[oldid],
  5597. prefix_len_id = oldid.length,
  5598. prefix_len_name = old_folder[0].length,
  5599. subscribed = $('input[name="_subscribed[]"]:first', row).prop('checked');
  5600. // no renaming, only update class_name
  5601. if (oldid == id) {
  5602. $(row).attr('class', class_name || '');
  5603. return;
  5604. }
  5605. // update subfolders
  5606. $('li', row).each(function() {
  5607. var fname = ref.folder_id2name(this.id),
  5608. folder = ref.env.subscriptionrows[fname],
  5609. newid = id + fname.slice(prefix_len_id);
  5610. this.id = 'rcmli' + ref.html_identifier_encode(newid);
  5611. $('input[name="_subscribed[]"]:first', this).val(newid);
  5612. folder[0] = name + folder[0].slice(prefix_len_name);
  5613. subfolders[newid] = folder;
  5614. delete ref.env.subscriptionrows[fname];
  5615. });
  5616. // get row off the list
  5617. row = $(row).detach();
  5618. delete this.env.subscriptionrows[oldid];
  5619. // remove parent list/toggle elements if not needed
  5620. if (parent.get(0) != this.gui_objects.subscriptionlist && !$('li', parent).length) {
  5621. $('ul,div.treetoggle', parent.parent()).remove();
  5622. }
  5623. // move the existing table row
  5624. this.add_folder_row(id, name, display_name, is_protected, subscribed, class_name, row, subfolders);
  5625. };
  5626. // remove the table row of a specific mailbox from the table
  5627. this.remove_folder_row = function(folder)
  5628. {
  5629. // reset searching
  5630. if (this.subscription_list.is_search()) {
  5631. this.subscription_select();
  5632. this.subscription_list.reset_search();
  5633. }
  5634. var list = [], row = this.subscription_list.get_item(folder, true);
  5635. // get subfolders if any
  5636. $('li', row).each(function() { list.push(ref.folder_id2name(this.id)); });
  5637. // remove folder row (and subfolders)
  5638. this.subscription_list.remove(folder);
  5639. // update local list variable
  5640. list.push(folder);
  5641. $.each(list, function(i, v) { delete ref.env.subscriptionrows[v]; });
  5642. };
  5643. this.subscribe = function(folder)
  5644. {
  5645. if (folder) {
  5646. var lock = this.display_message(this.get_label('foldersubscribing'), 'loading');
  5647. this.http_post('subscribe', {_mbox: folder}, lock);
  5648. }
  5649. };
  5650. this.unsubscribe = function(folder)
  5651. {
  5652. if (folder) {
  5653. var lock = this.display_message(this.get_label('folderunsubscribing'), 'loading');
  5654. this.http_post('unsubscribe', {_mbox: folder}, lock);
  5655. }
  5656. };
  5657. // when user select a folder in manager
  5658. this.show_folder = function(folder, path, force)
  5659. {
  5660. var win, target = window,
  5661. url = '&_action=edit-folder&_mbox='+urlencode(folder);
  5662. if (path)
  5663. url += '&_path='+urlencode(path);
  5664. if (win = this.get_frame_window(this.env.contentframe)) {
  5665. target = win;
  5666. url += '&_framed=1';
  5667. }
  5668. if (String(target.location.href).indexOf(url) >= 0 && !force)
  5669. this.show_contentframe(true);
  5670. else
  5671. this.location_href(this.env.comm_path+url, target, true);
  5672. };
  5673. // disables subscription checkbox (for protected folder)
  5674. this.disable_subscription = function(folder)
  5675. {
  5676. var row = this.subscription_list.get_item(folder, true);
  5677. if (row)
  5678. $('input[name="_subscribed[]"]:first', row).prop('disabled', true);
  5679. };
  5680. // resets state of subscription checkbox (e.g. on error)
  5681. this.reset_subscription = function(folder, state)
  5682. {
  5683. var row = this.subscription_list.get_item(folder, true);
  5684. if (row)
  5685. $('input[name="_subscribed[]"]:first', row).prop('checked', state);
  5686. };
  5687. this.folder_size = function(folder)
  5688. {
  5689. var lock = this.set_busy(true, 'loading');
  5690. this.http_post('folder-size', {_mbox: folder}, lock);
  5691. };
  5692. this.folder_size_update = function(size)
  5693. {
  5694. $('#folder-size').replaceWith(size);
  5695. };
  5696. // filter folders by namespace
  5697. this.folder_filter = function(prefix)
  5698. {
  5699. this.subscription_list.reset_search();
  5700. this.subscription_list.container.children('li').each(function() {
  5701. var i, folder = ref.folder_id2name(this.id);
  5702. // show all folders
  5703. if (prefix == '---') {
  5704. }
  5705. // got namespace prefix
  5706. else if (prefix) {
  5707. if (folder !== prefix) {
  5708. $(this).data('filtered', true).hide();
  5709. return
  5710. }
  5711. }
  5712. // no namespace prefix, filter out all other namespaces
  5713. else {
  5714. // first get all namespace roots
  5715. for (i in ref.env.ns_roots) {
  5716. if (folder === ref.env.ns_roots[i]) {
  5717. $(this).data('filtered', true).hide();
  5718. return;
  5719. }
  5720. }
  5721. }
  5722. $(this).removeData('filtered').show();
  5723. });
  5724. };
  5725. /*********************************************************/
  5726. /********* GUI functionality *********/
  5727. /*********************************************************/
  5728. var init_button = function(cmd, prop)
  5729. {
  5730. var elm = document.getElementById(prop.id);
  5731. if (!elm)
  5732. return;
  5733. var preload = false;
  5734. if (prop.type == 'image') {
  5735. elm = elm.parentNode;
  5736. preload = true;
  5737. }
  5738. elm._command = cmd;
  5739. elm._id = prop.id;
  5740. if (prop.sel) {
  5741. elm.onmousedown = function(e) { return ref.button_sel(this._command, this._id); };
  5742. elm.onmouseup = function(e) { return ref.button_out(this._command, this._id); };
  5743. if (preload)
  5744. new Image().src = prop.sel;
  5745. }
  5746. if (prop.over) {
  5747. elm.onmouseover = function(e) { return ref.button_over(this._command, this._id); };
  5748. elm.onmouseout = function(e) { return ref.button_out(this._command, this._id); };
  5749. if (preload)
  5750. new Image().src = prop.over;
  5751. }
  5752. };
  5753. // set event handlers on registered buttons
  5754. this.init_buttons = function()
  5755. {
  5756. for (var cmd in this.buttons) {
  5757. if (typeof cmd !== 'string')
  5758. continue;
  5759. for (var i=0; i<this.buttons[cmd].length; i++) {
  5760. init_button(cmd, this.buttons[cmd][i]);
  5761. }
  5762. }
  5763. };
  5764. // set button to a specific state
  5765. this.set_button = function(command, state)
  5766. {
  5767. var n, button, obj, $obj, a_buttons = this.buttons[command],
  5768. len = a_buttons ? a_buttons.length : 0;
  5769. for (n=0; n<len; n++) {
  5770. button = a_buttons[n];
  5771. obj = document.getElementById(button.id);
  5772. if (!obj || button.status === state)
  5773. continue;
  5774. // get default/passive setting of the button
  5775. if (button.type == 'image' && !button.status) {
  5776. button.pas = obj._original_src ? obj._original_src : obj.src;
  5777. // respect PNG fix on IE browsers
  5778. if (obj.runtimeStyle && obj.runtimeStyle.filter && obj.runtimeStyle.filter.match(/src=['"]([^'"]+)['"]/))
  5779. button.pas = RegExp.$1;
  5780. }
  5781. else if (!button.status)
  5782. button.pas = String(obj.className);
  5783. button.status = state;
  5784. // set image according to button state
  5785. if (button.type == 'image' && button[state]) {
  5786. obj.src = button[state];
  5787. }
  5788. // set class name according to button state
  5789. else if (button[state] !== undefined) {
  5790. obj.className = button[state];
  5791. }
  5792. // disable/enable input buttons
  5793. if (button.type == 'input') {
  5794. obj.disabled = state == 'pas';
  5795. }
  5796. else if (button.type == 'uibutton') {
  5797. button.status = state;
  5798. $(obj).button('option', 'disabled', state == 'pas');
  5799. }
  5800. else {
  5801. $obj = $(obj);
  5802. $obj
  5803. .attr('tabindex', state == 'pas' || state == 'sel' ? '-1' : ($obj.attr('data-tabindex') || '0'))
  5804. .attr('aria-disabled', state == 'pas' || state == 'sel' ? 'true' : 'false');
  5805. }
  5806. }
  5807. };
  5808. // display a specific alttext
  5809. this.set_alttext = function(command, label)
  5810. {
  5811. var n, button, obj, link, a_buttons = this.buttons[command],
  5812. len = a_buttons ? a_buttons.length : 0;
  5813. for (n=0; n<len; n++) {
  5814. button = a_buttons[n];
  5815. obj = document.getElementById(button.id);
  5816. if (button.type == 'image' && obj) {
  5817. obj.setAttribute('alt', this.get_label(label));
  5818. if ((link = obj.parentNode) && link.tagName.toLowerCase() == 'a')
  5819. link.setAttribute('title', this.get_label(label));
  5820. }
  5821. else if (obj)
  5822. obj.setAttribute('title', this.get_label(label));
  5823. }
  5824. };
  5825. // mouse over button
  5826. this.button_over = function(command, id)
  5827. {
  5828. this.button_event(command, id, 'over');
  5829. };
  5830. // mouse down on button
  5831. this.button_sel = function(command, id)
  5832. {
  5833. this.button_event(command, id, 'sel');
  5834. };
  5835. // mouse out of button
  5836. this.button_out = function(command, id)
  5837. {
  5838. this.button_event(command, id, 'act');
  5839. };
  5840. // event of button
  5841. this.button_event = function(command, id, event)
  5842. {
  5843. var n, button, obj, a_buttons = this.buttons[command],
  5844. len = a_buttons ? a_buttons.length : 0;
  5845. for (n=0; n<len; n++) {
  5846. button = a_buttons[n];
  5847. if (button.id == id && button.status == 'act') {
  5848. if (button[event] && (obj = document.getElementById(button.id))) {
  5849. obj[button.type == 'image' ? 'src' : 'className'] = button[event];
  5850. }
  5851. if (event == 'sel') {
  5852. this.buttons_sel[id] = command;
  5853. }
  5854. }
  5855. }
  5856. };
  5857. // write to the document/window title
  5858. this.set_pagetitle = function(title)
  5859. {
  5860. if (title && document.title)
  5861. document.title = title;
  5862. };
  5863. // display a system message, list of types in common.css (below #message definition)
  5864. this.display_message = function(msg, type, timeout, key)
  5865. {
  5866. // pass command to parent window
  5867. if (this.is_framed())
  5868. return parent.rcmail.display_message(msg, type, timeout);
  5869. if (!this.gui_objects.message) {
  5870. // save message in order to display after page loaded
  5871. if (type != 'loading')
  5872. this.pending_message = [msg, type, timeout, key];
  5873. return 1;
  5874. }
  5875. if (!type)
  5876. type = 'notice';
  5877. if (!key)
  5878. key = this.html_identifier(msg);
  5879. var date = new Date(),
  5880. id = type + date.getTime();
  5881. if (!timeout) {
  5882. switch (type) {
  5883. case 'error':
  5884. case 'warning':
  5885. timeout = this.message_time * 2;
  5886. break;
  5887. case 'uploading':
  5888. timeout = 0;
  5889. break;
  5890. default:
  5891. timeout = this.message_time;
  5892. }
  5893. }
  5894. if (type == 'loading') {
  5895. key = 'loading';
  5896. timeout = this.env.request_timeout * 1000;
  5897. if (!msg)
  5898. msg = this.get_label('loading');
  5899. }
  5900. // The same message is already displayed
  5901. if (this.messages[key]) {
  5902. // replace label
  5903. if (this.messages[key].obj)
  5904. this.messages[key].obj.html(msg);
  5905. // store label in stack
  5906. if (type == 'loading') {
  5907. this.messages[key].labels.push({'id': id, 'msg': msg});
  5908. }
  5909. // add element and set timeout
  5910. this.messages[key].elements.push(id);
  5911. setTimeout(function() { ref.hide_message(id, type == 'loading'); }, timeout);
  5912. return id;
  5913. }
  5914. // create DOM object and display it
  5915. var obj = $('<div>').addClass(type).html(msg).data('key', key),
  5916. cont = $(this.gui_objects.message).append(obj).show();
  5917. this.messages[key] = {'obj': obj, 'elements': [id]};
  5918. if (type == 'loading') {
  5919. this.messages[key].labels = [{'id': id, 'msg': msg}];
  5920. }
  5921. else if (type != 'uploading') {
  5922. obj.click(function() { return ref.hide_message(obj); })
  5923. .attr('role', 'alert');
  5924. }
  5925. this.triggerEvent('message', { message:msg, type:type, timeout:timeout, object:obj });
  5926. if (timeout > 0)
  5927. setTimeout(function() { ref.hide_message(id, type != 'loading'); }, timeout);
  5928. return id;
  5929. };
  5930. // make a message to disapear
  5931. this.hide_message = function(obj, fade)
  5932. {
  5933. // pass command to parent window
  5934. if (this.is_framed())
  5935. return parent.rcmail.hide_message(obj, fade);
  5936. if (!this.gui_objects.message)
  5937. return;
  5938. var k, n, i, o, m = this.messages;
  5939. // Hide message by object, don't use for 'loading'!
  5940. if (typeof obj === 'object') {
  5941. o = $(obj);
  5942. k = o.data('key');
  5943. this.hide_message_object(o, fade);
  5944. if (m[k])
  5945. delete m[k];
  5946. }
  5947. // Hide message by id
  5948. else {
  5949. for (k in m) {
  5950. for (n in m[k].elements) {
  5951. if (m[k] && m[k].elements[n] == obj) {
  5952. m[k].elements.splice(n, 1);
  5953. // hide DOM element if last instance is removed
  5954. if (!m[k].elements.length) {
  5955. this.hide_message_object(m[k].obj, fade);
  5956. delete m[k];
  5957. }
  5958. // set pending action label for 'loading' message
  5959. else if (k == 'loading') {
  5960. for (i in m[k].labels) {
  5961. if (m[k].labels[i].id == obj) {
  5962. delete m[k].labels[i];
  5963. }
  5964. else {
  5965. o = m[k].labels[i].msg;
  5966. m[k].obj.html(o);
  5967. }
  5968. }
  5969. }
  5970. }
  5971. }
  5972. }
  5973. }
  5974. };
  5975. // hide message object and remove from the DOM
  5976. this.hide_message_object = function(o, fade)
  5977. {
  5978. if (fade)
  5979. o.fadeOut(600, function() {$(this).remove(); });
  5980. else
  5981. o.hide().remove();
  5982. };
  5983. // remove all messages immediately
  5984. this.clear_messages = function()
  5985. {
  5986. // pass command to parent window
  5987. if (this.is_framed())
  5988. return parent.rcmail.clear_messages();
  5989. var k, n, m = this.messages;
  5990. for (k in m)
  5991. for (n in m[k].elements)
  5992. if (m[k].obj)
  5993. this.hide_message_object(m[k].obj);
  5994. this.messages = {};
  5995. };
  5996. // display uploading message with progress indicator
  5997. // data should contain: name, total, current, percent, text
  5998. this.display_progress = function(data)
  5999. {
  6000. if (!data || !data.name)
  6001. return;
  6002. var msg = this.messages['progress' + data.name];
  6003. if (!data.label)
  6004. data.label = this.get_label('uploadingmany');
  6005. if (!msg) {
  6006. if (!data.percent || data.percent < 100)
  6007. this.display_message(data.label, 'uploading', 0, 'progress' + data.name);
  6008. return;
  6009. }
  6010. if (!data.total || data.percent >= 100) {
  6011. this.hide_message(msg.obj);
  6012. return;
  6013. }
  6014. if (data.text)
  6015. data.label += ' ' + data.text;
  6016. msg.obj.text(data.label);
  6017. };
  6018. // open a jquery UI dialog with the given content
  6019. this.show_popup_dialog = function(content, title, buttons, options)
  6020. {
  6021. // forward call to parent window
  6022. if (this.is_framed()) {
  6023. return parent.rcmail.show_popup_dialog(content, title, buttons, options);
  6024. }
  6025. var popup = $('<div class="popup">');
  6026. if (typeof content == 'object')
  6027. popup.append(content);
  6028. else
  6029. popup.html(content);
  6030. options = $.extend({
  6031. title: title,
  6032. buttons: buttons,
  6033. modal: true,
  6034. resizable: true,
  6035. width: 500,
  6036. close: function(event, ui) { $(this).remove(); }
  6037. }, options || {});
  6038. popup.dialog(options);
  6039. // resize and center popup
  6040. var win = $(window), w = win.width(), h = win.height(),
  6041. width = popup.width(), height = popup.height();
  6042. popup.dialog('option', {
  6043. height: Math.min(h - 40, height + 75 + (buttons ? 50 : 0)),
  6044. width: Math.min(w - 20, width + 36)
  6045. });
  6046. // assign special classes to dialog buttons
  6047. $.each(options.button_classes || [], function(i, v) {
  6048. if (v) $($('.ui-dialog-buttonpane button.ui-button', popup.parent()).get(i)).addClass(v);
  6049. });
  6050. return popup;
  6051. };
  6052. // enable/disable buttons for page shifting
  6053. this.set_page_buttons = function()
  6054. {
  6055. this.enable_command('nextpage', 'lastpage', this.env.pagecount > this.env.current_page);
  6056. this.enable_command('previouspage', 'firstpage', this.env.current_page > 1);
  6057. this.update_pagejumper();
  6058. };
  6059. // mark a mailbox as selected and set environment variable
  6060. this.select_folder = function(name, prefix, encode)
  6061. {
  6062. if (this.savedsearchlist) {
  6063. this.savedsearchlist.select('');
  6064. }
  6065. if (this.treelist) {
  6066. this.treelist.select(name);
  6067. }
  6068. else if (this.gui_objects.folderlist) {
  6069. $('li.selected', this.gui_objects.folderlist).removeClass('selected');
  6070. $(this.get_folder_li(name, prefix, encode)).addClass('selected');
  6071. // trigger event hook
  6072. this.triggerEvent('selectfolder', { folder:name, prefix:prefix });
  6073. }
  6074. };
  6075. // adds a class to selected folder
  6076. this.mark_folder = function(name, class_name, prefix, encode)
  6077. {
  6078. $(this.get_folder_li(name, prefix, encode)).addClass(class_name);
  6079. this.triggerEvent('markfolder', {folder: name, mark: class_name, status: true});
  6080. };
  6081. // adds a class to selected folder
  6082. this.unmark_folder = function(name, class_name, prefix, encode)
  6083. {
  6084. $(this.get_folder_li(name, prefix, encode)).removeClass(class_name);
  6085. this.triggerEvent('markfolder', {folder: name, mark: class_name, status: false});
  6086. };
  6087. // helper method to find a folder list item
  6088. this.get_folder_li = function(name, prefix, encode)
  6089. {
  6090. if (!prefix)
  6091. prefix = 'rcmli';
  6092. if (this.gui_objects.folderlist) {
  6093. name = this.html_identifier(name, encode);
  6094. return document.getElementById(prefix+name);
  6095. }
  6096. };
  6097. // for reordering column array (Konqueror workaround)
  6098. // and for setting some message list global variables
  6099. this.set_message_coltypes = function(listcols, repl, smart_col)
  6100. {
  6101. var list = this.message_list,
  6102. thead = list ? list.thead : null,
  6103. repl, cell, col, n, len, tr;
  6104. this.env.listcols = listcols;
  6105. if (!this.env.coltypes)
  6106. this.env.coltypes = {};
  6107. // replace old column headers
  6108. if (thead) {
  6109. if (repl) {
  6110. thead.innerHTML = '';
  6111. tr = document.createElement('tr');
  6112. for (c=0, len=repl.length; c < len; c++) {
  6113. cell = document.createElement('th');
  6114. cell.innerHTML = repl[c].html || '';
  6115. if (repl[c].id) cell.id = repl[c].id;
  6116. if (repl[c].className) cell.className = repl[c].className;
  6117. tr.appendChild(cell);
  6118. }
  6119. thead.appendChild(tr);
  6120. }
  6121. for (n=0, len=this.env.listcols.length; n<len; n++) {
  6122. col = this.env.listcols[n];
  6123. if ((cell = thead.rows[0].cells[n]) && (col == 'from' || col == 'to' || col == 'fromto')) {
  6124. $(cell).attr('rel', col).find('span,a').text(this.get_label(col == 'fromto' ? smart_col : col));
  6125. }
  6126. }
  6127. }
  6128. this.env.subject_col = null;
  6129. this.env.flagged_col = null;
  6130. this.env.status_col = null;
  6131. if (this.env.coltypes.folder)
  6132. this.env.coltypes.folder.hidden = !(this.env.search_request || this.env.search_id) || this.env.search_scope == 'base';
  6133. if ((n = $.inArray('subject', this.env.listcols)) >= 0) {
  6134. this.env.subject_col = n;
  6135. if (list)
  6136. list.subject_col = n;
  6137. }
  6138. if ((n = $.inArray('flag', this.env.listcols)) >= 0)
  6139. this.env.flagged_col = n;
  6140. if ((n = $.inArray('status', this.env.listcols)) >= 0)
  6141. this.env.status_col = n;
  6142. if (list) {
  6143. list.hide_column('folder', (this.env.coltypes.folder && this.env.coltypes.folder.hidden) || $.inArray('folder', this.env.listcols) < 0);
  6144. list.init_header();
  6145. }
  6146. };
  6147. // replace content of row count display
  6148. this.set_rowcount = function(text, mbox)
  6149. {
  6150. // #1487752
  6151. if (mbox && mbox != this.env.mailbox)
  6152. return false;
  6153. $(this.gui_objects.countdisplay).html(text);
  6154. // update page navigation buttons
  6155. this.set_page_buttons();
  6156. };
  6157. // replace content of mailboxname display
  6158. this.set_mailboxname = function(content)
  6159. {
  6160. if (this.gui_objects.mailboxname && content)
  6161. this.gui_objects.mailboxname.innerHTML = content;
  6162. };
  6163. // replace content of quota display
  6164. this.set_quota = function(content)
  6165. {
  6166. if (this.gui_objects.quotadisplay && content && content.type == 'text')
  6167. $(this.gui_objects.quotadisplay).text((content.percent||0) + '%').attr('title', content.title);
  6168. this.triggerEvent('setquota', content);
  6169. this.env.quota_content = content;
  6170. };
  6171. // update trash folder state
  6172. this.set_trash_count = function(count)
  6173. {
  6174. this[(count ? 'un' : '') + 'mark_folder'](this.env.trash_mailbox, 'empty', '', true);
  6175. };
  6176. // update the mailboxlist
  6177. this.set_unread_count = function(mbox, count, set_title, mark)
  6178. {
  6179. if (!this.gui_objects.mailboxlist)
  6180. return false;
  6181. this.env.unread_counts[mbox] = count;
  6182. this.set_unread_count_display(mbox, set_title);
  6183. if (mark)
  6184. this.mark_folder(mbox, mark, '', true);
  6185. else if (!count)
  6186. this.unmark_folder(mbox, 'recent', '', true);
  6187. };
  6188. // update the mailbox count display
  6189. this.set_unread_count_display = function(mbox, set_title)
  6190. {
  6191. var reg, link, text_obj, item, mycount, childcount, div;
  6192. if (item = this.get_folder_li(mbox, '', true)) {
  6193. mycount = this.env.unread_counts[mbox] ? this.env.unread_counts[mbox] : 0;
  6194. link = $(item).children('a').eq(0);
  6195. text_obj = link.children('span.unreadcount');
  6196. if (!text_obj.length && mycount)
  6197. text_obj = $('<span>').addClass('unreadcount').appendTo(link);
  6198. reg = /\s+\([0-9]+\)$/i;
  6199. childcount = 0;
  6200. if ((div = item.getElementsByTagName('div')[0]) &&
  6201. div.className.match(/collapsed/)) {
  6202. // add children's counters
  6203. for (var k in this.env.unread_counts)
  6204. if (k.startsWith(mbox + this.env.delimiter))
  6205. childcount += this.env.unread_counts[k];
  6206. }
  6207. if (mycount && text_obj.length)
  6208. text_obj.html(this.env.unreadwrap.replace(/%[sd]/, mycount));
  6209. else if (text_obj.length)
  6210. text_obj.remove();
  6211. // set parent's display
  6212. reg = new RegExp(RegExp.escape(this.env.delimiter) + '[^' + RegExp.escape(this.env.delimiter) + ']+$');
  6213. if (mbox.match(reg))
  6214. this.set_unread_count_display(mbox.replace(reg, ''), false);
  6215. // set the right classes
  6216. if ((mycount+childcount)>0)
  6217. $(item).addClass('unread');
  6218. else
  6219. $(item).removeClass('unread');
  6220. }
  6221. // set unread count to window title
  6222. reg = /^\([0-9]+\)\s+/i;
  6223. if (set_title && document.title) {
  6224. var new_title = '',
  6225. doc_title = String(document.title);
  6226. if (mycount && doc_title.match(reg))
  6227. new_title = doc_title.replace(reg, '('+mycount+') ');
  6228. else if (mycount)
  6229. new_title = '('+mycount+') '+doc_title;
  6230. else
  6231. new_title = doc_title.replace(reg, '');
  6232. this.set_pagetitle(new_title);
  6233. }
  6234. };
  6235. // display fetched raw headers
  6236. this.set_headers = function(content)
  6237. {
  6238. if (this.gui_objects.all_headers_row && this.gui_objects.all_headers_box && content)
  6239. $(this.gui_objects.all_headers_box).html(content).show();
  6240. };
  6241. // display all-headers row and fetch raw message headers
  6242. this.show_headers = function(props, elem)
  6243. {
  6244. if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box || !this.env.uid)
  6245. return;
  6246. $(elem).removeClass('show-headers').addClass('hide-headers');
  6247. $(this.gui_objects.all_headers_row).show();
  6248. elem.onclick = function() { ref.command('hide-headers', '', elem); };
  6249. // fetch headers only once
  6250. if (!this.gui_objects.all_headers_box.innerHTML) {
  6251. this.http_post('headers', {_uid: this.env.uid, _mbox: this.env.mailbox},
  6252. this.display_message(this.get_label('loading'), 'loading')
  6253. );
  6254. }
  6255. };
  6256. // hide all-headers row
  6257. this.hide_headers = function(props, elem)
  6258. {
  6259. if (!this.gui_objects.all_headers_row || !this.gui_objects.all_headers_box)
  6260. return;
  6261. $(elem).removeClass('hide-headers').addClass('show-headers');
  6262. $(this.gui_objects.all_headers_row).hide();
  6263. elem.onclick = function() { ref.command('show-headers', '', elem); };
  6264. };
  6265. // create folder selector popup, position and display it
  6266. this.folder_selector = function(event, callback)
  6267. {
  6268. var container = this.folder_selector_element;
  6269. if (!container) {
  6270. var rows = [],
  6271. delim = this.env.delimiter,
  6272. ul = $('<ul class="toolbarmenu">'),
  6273. link = document.createElement('a');
  6274. container = $('<div id="folder-selector" class="popupmenu"></div>');
  6275. link.href = '#';
  6276. link.className = 'icon';
  6277. // loop over sorted folders list
  6278. $.each(this.env.mailboxes_list, function() {
  6279. var n = 0, s = 0,
  6280. folder = ref.env.mailboxes[this],
  6281. id = folder.id,
  6282. a = $(link.cloneNode(false)),
  6283. row = $('<li>');
  6284. if (folder.virtual)
  6285. a.addClass('virtual').attr('aria-disabled', 'true').attr('tabindex', '-1');
  6286. else
  6287. a.addClass('active').data('id', folder.id);
  6288. if (folder['class'])
  6289. a.addClass(folder['class']);
  6290. // calculate/set indentation level
  6291. while ((s = id.indexOf(delim, s)) >= 0) {
  6292. n++; s++;
  6293. }
  6294. a.css('padding-left', n ? (n * 16) + 'px' : 0);
  6295. // add folder name element
  6296. a.append($('<span>').text(folder.name));
  6297. row.append(a);
  6298. rows.push(row);
  6299. });
  6300. ul.append(rows).appendTo(container);
  6301. // temporarily show element to calculate its size
  6302. container.css({left: '-1000px', top: '-1000px'})
  6303. .appendTo($('body')).show();
  6304. // set max-height if the list is long
  6305. if (rows.length > 10)
  6306. container.css('max-height', $('li', container)[0].offsetHeight * 10 + 9);
  6307. // register delegate event handler for folder item clicks
  6308. container.on('click', 'a.active', function(e){
  6309. container.data('callback')($(this).data('id'));
  6310. return false;
  6311. });
  6312. this.folder_selector_element = container;
  6313. }
  6314. container.data('callback', callback);
  6315. // position menu on the screen
  6316. this.show_menu('folder-selector', true, event);
  6317. };
  6318. /***********************************************/
  6319. /********* popup menu functions *********/
  6320. /***********************************************/
  6321. // Show/hide a specific popup menu
  6322. this.show_menu = function(prop, show, event)
  6323. {
  6324. var name = typeof prop == 'object' ? prop.menu : prop,
  6325. obj = $('#'+name),
  6326. ref = event && event.target ? $(event.target) : $(obj.attr('rel') || '#'+name+'link'),
  6327. keyboard = rcube_event.is_keyboard(event),
  6328. align = obj.attr('data-align') || '',
  6329. stack = false;
  6330. // find "real" button element
  6331. if (ref.get(0).tagName != 'A' && ref.closest('a').length)
  6332. ref = ref.closest('a');
  6333. if (typeof prop == 'string')
  6334. prop = { menu:name };
  6335. // let plugins or skins provide the menu element
  6336. if (!obj.length) {
  6337. obj = this.triggerEvent('menu-get', { name:name, props:prop, originalEvent:event });
  6338. }
  6339. if (!obj || !obj.length) {
  6340. // just delegate the action to subscribers
  6341. return this.triggerEvent(show === false ? 'menu-close' : 'menu-open', { name:name, props:prop, originalEvent:event });
  6342. }
  6343. // move element to top for proper absolute positioning
  6344. obj.appendTo(document.body);
  6345. if (typeof show == 'undefined')
  6346. show = obj.is(':visible') ? false : true;
  6347. if (show && ref.length) {
  6348. var win = $(window),
  6349. pos = ref.offset(),
  6350. above = align.indexOf('bottom') >= 0;
  6351. stack = ref.attr('role') == 'menuitem' || ref.closest('[role=menuitem]').length > 0;
  6352. ref.offsetWidth = ref.outerWidth();
  6353. ref.offsetHeight = ref.outerHeight();
  6354. if (!above && pos.top + ref.offsetHeight + obj.height() > win.height()) {
  6355. above = true;
  6356. }
  6357. if (align.indexOf('right') >= 0) {
  6358. pos.left = pos.left + ref.outerWidth() - obj.width();
  6359. }
  6360. else if (stack) {
  6361. pos.left = pos.left + ref.offsetWidth - 5;
  6362. pos.top -= ref.offsetHeight;
  6363. }
  6364. if (pos.left + obj.width() > win.width()) {
  6365. pos.left = win.width() - obj.width() - 12;
  6366. }
  6367. pos.top = Math.max(0, pos.top + (above ? -obj.height() : ref.offsetHeight));
  6368. obj.css({ left:pos.left+'px', top:pos.top+'px' });
  6369. }
  6370. // add menu to stack
  6371. if (show) {
  6372. // truncate stack down to the one containing the ref link
  6373. for (var i = this.menu_stack.length - 1; stack && i >= 0; i--) {
  6374. if (!$(ref).parents('#'+this.menu_stack[i]).length && $(event.target).parent().attr('role') != 'menuitem')
  6375. this.hide_menu(this.menu_stack[i], event);
  6376. }
  6377. if (stack && this.menu_stack.length) {
  6378. obj.data('parent', $.last(this.menu_stack));
  6379. obj.css('z-index', ($('#'+$.last(this.menu_stack)).css('z-index') || 0) + 1);
  6380. }
  6381. else if (!stack && this.menu_stack.length) {
  6382. this.hide_menu(this.menu_stack[0], event);
  6383. }
  6384. obj.show().attr('aria-hidden', 'false').data('opener', ref.attr('aria-expanded', 'true').get(0));
  6385. this.triggerEvent('menu-open', { name:name, obj:obj, props:prop, originalEvent:event });
  6386. this.menu_stack.push(name);
  6387. this.menu_keyboard_active = show && keyboard;
  6388. if (this.menu_keyboard_active) {
  6389. this.focused_menu = name;
  6390. obj.find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
  6391. }
  6392. }
  6393. else { // close menu
  6394. this.hide_menu(name, event);
  6395. }
  6396. return show;
  6397. };
  6398. // hide the given popup menu (and it's childs)
  6399. this.hide_menu = function(name, event)
  6400. {
  6401. if (!this.menu_stack.length) {
  6402. // delegate to subscribers
  6403. this.triggerEvent('menu-close', { name:name, props:{ menu:name }, originalEvent:event });
  6404. return;
  6405. }
  6406. var obj, keyboard = rcube_event.is_keyboard(event);
  6407. for (var j=this.menu_stack.length-1; j >= 0; j--) {
  6408. obj = $('#' + this.menu_stack[j]).hide().attr('aria-hidden', 'true').data('parent', false);
  6409. this.triggerEvent('menu-close', { name:this.menu_stack[j], obj:obj, props:{ menu:this.menu_stack[j] }, originalEvent:event });
  6410. if (this.menu_stack[j] == name) {
  6411. j = -1; // stop loop
  6412. if (obj.data('opener')) {
  6413. $(obj.data('opener')).attr('aria-expanded', 'false');
  6414. if (keyboard)
  6415. obj.data('opener').focus();
  6416. }
  6417. }
  6418. this.menu_stack.pop();
  6419. }
  6420. // focus previous menu in stack
  6421. if (this.menu_stack.length && keyboard) {
  6422. this.menu_keyboard_active = true;
  6423. this.focused_menu = $.last(this.menu_stack);
  6424. if (!obj || !obj.data('opener'))
  6425. $('#'+this.focused_menu).find('a,input:not(:disabled)').not('[aria-disabled=true]').first().focus();
  6426. }
  6427. else {
  6428. this.focused_menu = null;
  6429. this.menu_keyboard_active = false;
  6430. }
  6431. }
  6432. // position a menu element on the screen in relation to other object
  6433. this.element_position = function(element, obj)
  6434. {
  6435. var obj = $(obj), win = $(window),
  6436. width = obj.outerWidth(),
  6437. height = obj.outerHeight(),
  6438. menu_pos = obj.data('menu-pos'),
  6439. win_height = win.height(),
  6440. elem_height = $(element).height(),
  6441. elem_width = $(element).width(),
  6442. pos = obj.offset(),
  6443. top = pos.top,
  6444. left = pos.left + width;
  6445. if (menu_pos == 'bottom') {
  6446. top += height;
  6447. left -= width;
  6448. }
  6449. else
  6450. left -= 5;
  6451. if (top + elem_height > win_height) {
  6452. top -= elem_height - height;
  6453. if (top < 0)
  6454. top = Math.max(0, (win_height - elem_height) / 2);
  6455. }
  6456. if (left + elem_width > win.width())
  6457. left -= elem_width + width;
  6458. element.css({left: left + 'px', top: top + 'px'});
  6459. };
  6460. // initialize HTML editor
  6461. this.editor_init = function(config, id)
  6462. {
  6463. this.editor = new rcube_text_editor(config, id);
  6464. };
  6465. /********************************************************/
  6466. /********* html to text conversion functions *********/
  6467. /********************************************************/
  6468. this.html2plain = function(html, func)
  6469. {
  6470. return this.format_converter(html, 'html', func);
  6471. };
  6472. this.plain2html = function(plain, func)
  6473. {
  6474. return this.format_converter(plain, 'plain', func);
  6475. };
  6476. this.format_converter = function(text, format, func)
  6477. {
  6478. // warn the user (if converted content is not empty)
  6479. if (!text
  6480. || (format == 'html' && !(text.replace(/<[^>]+>|&nbsp;|\xC2\xA0|\s/g, '')).length)
  6481. || (format != 'html' && !(text.replace(/\xC2\xA0|\s/g, '')).length)
  6482. ) {
  6483. // without setTimeout() here, textarea is filled with initial (onload) content
  6484. if (func)
  6485. setTimeout(function() { func(''); }, 50);
  6486. return true;
  6487. }
  6488. var confirmed = this.env.editor_warned || confirm(this.get_label('editorwarning'));
  6489. this.env.editor_warned = true;
  6490. if (!confirmed)
  6491. return false;
  6492. var url = '?_task=utils&_action=' + (format == 'html' ? 'html2text' : 'text2html'),
  6493. lock = this.set_busy(true, 'converting');
  6494. $.ajax({ type: 'POST', url: url, data: text, contentType: 'application/octet-stream',
  6495. error: function(o, status, err) { ref.http_error(o, status, err, lock); },
  6496. success: function(data) {
  6497. ref.set_busy(false, null, lock);
  6498. if (func) func(data);
  6499. }
  6500. });
  6501. return true;
  6502. };
  6503. /********************************************************/
  6504. /********* remote request methods *********/
  6505. /********************************************************/
  6506. // compose a valid url with the given parameters
  6507. this.url = function(action, query)
  6508. {
  6509. var querystring = typeof query === 'string' ? query : '';
  6510. if (typeof action !== 'string')
  6511. query = action;
  6512. else if (!query || typeof query !== 'object')
  6513. query = {};
  6514. if (action)
  6515. query._action = action;
  6516. else if (this.env.action)
  6517. query._action = this.env.action;
  6518. var url = this.env.comm_path, k, param = {};
  6519. // overwrite task name
  6520. if (action && action.match(/([a-z0-9_-]+)\/([a-z0-9-_.]+)/)) {
  6521. query._action = RegExp.$2;
  6522. url = url.replace(/\_task=[a-z0-9_-]+/, '_task=' + RegExp.$1);
  6523. }
  6524. // remove undefined values
  6525. for (k in query) {
  6526. if (query[k] !== undefined && query[k] !== null)
  6527. param[k] = query[k];
  6528. }
  6529. if (param = $.param(param))
  6530. url += (url.indexOf('?') > -1 ? '&' : '?') + param;
  6531. if (querystring)
  6532. url += (url.indexOf('?') > -1 ? '&' : '?') + querystring;
  6533. return url;
  6534. };
  6535. this.redirect = function(url, lock)
  6536. {
  6537. if (lock || lock === null)
  6538. this.set_busy(true);
  6539. if (this.is_framed()) {
  6540. parent.rcmail.redirect(url, lock);
  6541. }
  6542. else {
  6543. if (this.env.extwin) {
  6544. if (typeof url == 'string')
  6545. url += (url.indexOf('?') < 0 ? '?' : '&') + '_extwin=1';
  6546. else
  6547. url._extwin = 1;
  6548. }
  6549. this.location_href(url, window);
  6550. }
  6551. };
  6552. this.goto_url = function(action, query, lock, secure)
  6553. {
  6554. var url = this.url(action, query)
  6555. if (secure) url = this.secure_url(url);
  6556. this.redirect(url, lock);
  6557. };
  6558. this.location_href = function(url, target, frame)
  6559. {
  6560. if (frame)
  6561. this.lock_frame();
  6562. if (typeof url == 'object')
  6563. url = this.env.comm_path + '&' + $.param(url);
  6564. // simulate real link click to force IE to send referer header
  6565. if (bw.ie && target == window)
  6566. $('<a>').attr('href', url).appendTo(document.body).get(0).click();
  6567. else
  6568. target.location.href = url;
  6569. // reset keep-alive interval
  6570. this.start_keepalive();
  6571. };
  6572. // update browser location to remember current view
  6573. this.update_state = function(query)
  6574. {
  6575. if (window.history.replaceState)
  6576. try {
  6577. // This may throw security exception in Firefox (#5400)
  6578. window.history.replaceState({}, document.title, rcmail.url('', query));
  6579. }
  6580. catch(e) { /* ignore */ };
  6581. };
  6582. // send a http request to the server
  6583. this.http_request = function(action, data, lock, type)
  6584. {
  6585. if (type != 'POST')
  6586. type = 'GET';
  6587. if (typeof data !== 'object')
  6588. data = rcube_parse_query(data);
  6589. data._remote = 1;
  6590. data._unlock = lock ? lock : 0;
  6591. // trigger plugin hook
  6592. var result = this.triggerEvent('request' + action, data);
  6593. // abort if one of the handlers returned false
  6594. if (result === false) {
  6595. if (data._unlock)
  6596. this.set_busy(false, null, data._unlock);
  6597. return false;
  6598. }
  6599. else if (result !== undefined) {
  6600. data = result;
  6601. if (data._action) {
  6602. action = data._action;
  6603. delete data._action;
  6604. }
  6605. }
  6606. var url = this.url(action);
  6607. // reset keep-alive interval
  6608. this.start_keepalive();
  6609. // send request
  6610. return $.ajax({
  6611. type: type, url: url, data: data, dataType: 'json',
  6612. success: function(data) { ref.http_response(data); },
  6613. error: function(o, status, err) { ref.http_error(o, status, err, lock, action); }
  6614. });
  6615. };
  6616. // send a http GET request to the server
  6617. this.http_get = this.http_request;
  6618. // send a http POST request to the server
  6619. this.http_post = function(action, data, lock)
  6620. {
  6621. return this.http_request(action, data, lock, 'POST');
  6622. };
  6623. // aborts ajax request
  6624. this.abort_request = function(r)
  6625. {
  6626. if (r.request)
  6627. r.request.abort();
  6628. if (r.lock)
  6629. this.set_busy(false, null, r.lock);
  6630. };
  6631. // handle HTTP response
  6632. this.http_response = function(response)
  6633. {
  6634. if (!response)
  6635. return;
  6636. if (response.unlock)
  6637. this.set_busy(false);
  6638. this.triggerEvent('responsebefore', {response: response});
  6639. this.triggerEvent('responsebefore'+response.action, {response: response});
  6640. // set env vars
  6641. if (response.env)
  6642. this.set_env(response.env);
  6643. var i;
  6644. // we have labels to add
  6645. if (typeof response.texts === 'object') {
  6646. for (i in response.texts)
  6647. if (typeof response.texts[i] === 'string')
  6648. this.add_label(i, response.texts[i]);
  6649. }
  6650. // if we get javascript code from server -> execute it
  6651. if (response.exec) {
  6652. eval(response.exec);
  6653. }
  6654. // execute callback functions of plugins
  6655. if (response.callbacks && response.callbacks.length) {
  6656. for (i=0; i < response.callbacks.length; i++)
  6657. this.triggerEvent(response.callbacks[i][0], response.callbacks[i][1]);
  6658. }
  6659. // process the response data according to the sent action
  6660. switch (response.action) {
  6661. case 'delete':
  6662. if (this.task == 'addressbook') {
  6663. var sid, uid = this.contact_list.get_selection(), writable = false;
  6664. if (uid && this.contact_list.rows[uid]) {
  6665. // search results, get source ID from record ID
  6666. if (this.env.source == '') {
  6667. sid = String(uid).replace(/^[^-]+-/, '');
  6668. writable = sid && this.env.address_sources[sid] && !this.env.address_sources[sid].readonly;
  6669. }
  6670. else {
  6671. writable = !this.env.address_sources[this.env.source].readonly;
  6672. }
  6673. }
  6674. this.enable_command('compose', (uid && this.contact_list.rows[uid]));
  6675. this.enable_command('delete', 'edit', writable);
  6676. this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
  6677. this.enable_command('export-selected', 'print', false);
  6678. }
  6679. case 'move':
  6680. if (this.env.action == 'show') {
  6681. // re-enable commands on move/delete error
  6682. this.enable_command(this.env.message_commands, true);
  6683. if (!this.env.list_post)
  6684. this.enable_command('reply-list', false);
  6685. }
  6686. else if (this.task == 'addressbook') {
  6687. this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
  6688. }
  6689. case 'purge':
  6690. case 'expunge':
  6691. if (this.task == 'mail') {
  6692. if (!this.env.exists) {
  6693. // clear preview pane content
  6694. if (this.env.contentframe)
  6695. this.show_contentframe(false);
  6696. // disable commands useless when mailbox is empty
  6697. this.enable_command(this.env.message_commands, 'purge', 'expunge',
  6698. 'select-all', 'select-none', 'expand-all', 'expand-unread', 'collapse-all', false);
  6699. }
  6700. if (this.message_list)
  6701. this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:this.message_list.rowcount });
  6702. }
  6703. break;
  6704. case 'refresh':
  6705. case 'check-recent':
  6706. // update message flags
  6707. $.each(this.env.recent_flags || {}, function(uid, flags) {
  6708. ref.set_message(uid, 'deleted', flags.deleted);
  6709. ref.set_message(uid, 'replied', flags.answered);
  6710. ref.set_message(uid, 'unread', !flags.seen);
  6711. ref.set_message(uid, 'forwarded', flags.forwarded);
  6712. ref.set_message(uid, 'flagged', flags.flagged);
  6713. });
  6714. delete this.env.recent_flags;
  6715. case 'getunread':
  6716. case 'search':
  6717. this.env.qsearch = null;
  6718. case 'list':
  6719. if (this.task == 'mail') {
  6720. var is_multifolder = this.is_multifolder_listing(),
  6721. list = this.message_list,
  6722. uid = this.env.list_uid;
  6723. this.enable_command('show', 'select-all', 'select-none', this.env.messagecount > 0);
  6724. this.enable_command('expunge', this.env.exists && !is_multifolder);
  6725. this.enable_command('purge', this.purge_mailbox_test() && !is_multifolder);
  6726. this.enable_command('import-messages', !is_multifolder);
  6727. this.enable_command('expand-all', 'expand-unread', 'collapse-all', this.env.threading && this.env.messagecount && !is_multifolder);
  6728. if (list) {
  6729. if (response.action == 'list' || response.action == 'search') {
  6730. // highlight message row when we're back from message page
  6731. if (uid) {
  6732. if (!list.rows[uid])
  6733. uid += '-' + this.env.mailbox;
  6734. if (list.rows[uid]) {
  6735. list.select(uid);
  6736. }
  6737. delete this.env.list_uid;
  6738. }
  6739. this.enable_command('set-listmode', this.env.threads && !is_multifolder);
  6740. if (list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
  6741. list.focus();
  6742. // trigger 'select' so all dependent actions update its state
  6743. // e.g. plugins use this event to activate buttons (#1490647)
  6744. list.triggerEvent('select');
  6745. }
  6746. if (response.action != 'getunread')
  6747. this.triggerEvent('listupdate', { folder:this.env.mailbox, rowcount:list.rowcount });
  6748. }
  6749. }
  6750. else if (this.task == 'addressbook') {
  6751. this.enable_command('export', (this.contact_list && this.contact_list.rowcount > 0));
  6752. if (response.action == 'list' || response.action == 'search') {
  6753. this.enable_command('search-create', this.env.source == '');
  6754. this.enable_command('search-delete', this.env.search_id);
  6755. this.update_group_commands();
  6756. if (this.contact_list.rowcount > 0 && !$(document.activeElement).is('input,textarea'))
  6757. this.contact_list.focus();
  6758. this.triggerEvent('listupdate', { folder:this.env.source, rowcount:this.contact_list.rowcount });
  6759. }
  6760. }
  6761. break;
  6762. case 'list-contacts':
  6763. case 'search-contacts':
  6764. if (this.contact_list && this.contact_list.rowcount > 0)
  6765. this.contact_list.focus();
  6766. break;
  6767. }
  6768. if (response.unlock)
  6769. this.hide_message(response.unlock);
  6770. this.triggerEvent('responseafter', {response: response});
  6771. this.triggerEvent('responseafter'+response.action, {response: response});
  6772. // reset keep-alive interval
  6773. this.start_keepalive();
  6774. };
  6775. // handle HTTP request errors
  6776. this.http_error = function(request, status, err, lock, action)
  6777. {
  6778. var errmsg = request.statusText;
  6779. this.set_busy(false, null, lock);
  6780. request.abort();
  6781. // don't display error message on page unload (#1488547)
  6782. if (this.unload)
  6783. return;
  6784. if (request.status && errmsg)
  6785. this.display_message(this.get_label('servererror') + ' (' + errmsg + ')', 'error');
  6786. else if (status == 'timeout')
  6787. this.display_message(this.get_label('requesttimedout'), 'error');
  6788. else if (request.status == 0 && status != 'abort')
  6789. this.display_message(this.get_label('connerror'), 'error');
  6790. // redirect to url specified in location header if not empty
  6791. var location_url = request.getResponseHeader("Location");
  6792. if (location_url && this.env.action != 'compose') // don't redirect on compose screen, contents might get lost (#1488926)
  6793. this.redirect(location_url);
  6794. // 403 Forbidden response (CSRF prevention) - reload the page.
  6795. // In case there's a new valid session it will be used, otherwise
  6796. // login form will be presented (#1488960).
  6797. if (request.status == 403) {
  6798. (this.is_framed() ? parent : window).location.reload();
  6799. return;
  6800. }
  6801. // re-send keep-alive requests after 30 seconds
  6802. if (action == 'keep-alive')
  6803. setTimeout(function(){ ref.keep_alive(); ref.start_keepalive(); }, 30000);
  6804. };
  6805. // handler for session errors detected on the server
  6806. this.session_error = function(redirect_url)
  6807. {
  6808. this.env.server_error = 401;
  6809. // save message in local storage and do not redirect
  6810. if (this.env.action == 'compose') {
  6811. this.save_compose_form_local();
  6812. this.compose_skip_unsavedcheck = true;
  6813. }
  6814. else if (redirect_url) {
  6815. setTimeout(function(){ ref.redirect(redirect_url, true); }, 2000);
  6816. }
  6817. };
  6818. // callback when an iframe finished loading
  6819. this.iframe_loaded = function(unlock)
  6820. {
  6821. this.set_busy(false, null, unlock);
  6822. if (this.submit_timer)
  6823. clearTimeout(this.submit_timer);
  6824. };
  6825. /**
  6826. Send multi-threaded parallel HTTP requests to the server for a list if items.
  6827. The string '%' in either a GET query or POST parameters will be replaced with the respective item value.
  6828. This is the argument object expected: {
  6829. items: ['foo','bar','gna'], // list of items to send requests for
  6830. action: 'task/some-action', // Roudncube action to call
  6831. query: { q:'%s' }, // GET query parameters
  6832. postdata: { source:'%s' }, // POST data (sends a POST request if present)
  6833. threads: 3, // max. number of concurrent requests
  6834. onresponse: function(data){ }, // Callback function called for every response received from server
  6835. whendone: function(alldata){ } // Callback function called when all requests have been sent
  6836. }
  6837. */
  6838. this.multi_thread_http_request = function(prop)
  6839. {
  6840. var i, item, reqid = new Date().getTime(),
  6841. threads = prop.threads || 1;
  6842. prop.reqid = reqid;
  6843. prop.running = 0;
  6844. prop.requests = [];
  6845. prop.result = [];
  6846. prop._items = $.extend([], prop.items); // copy items
  6847. if (!prop.lock)
  6848. prop.lock = this.display_message(this.get_label('loading'), 'loading');
  6849. // add the request arguments to the jobs pool
  6850. this.http_request_jobs[reqid] = prop;
  6851. // start n threads
  6852. for (i=0; i < threads; i++) {
  6853. item = prop._items.shift();
  6854. if (item === undefined)
  6855. break;
  6856. prop.running++;
  6857. prop.requests.push(this.multi_thread_send_request(prop, item));
  6858. }
  6859. return reqid;
  6860. };
  6861. // helper method to send an HTTP request with the given iterator value
  6862. this.multi_thread_send_request = function(prop, item)
  6863. {
  6864. var k, postdata, query;
  6865. // replace %s in post data
  6866. if (prop.postdata) {
  6867. postdata = {};
  6868. for (k in prop.postdata) {
  6869. postdata[k] = String(prop.postdata[k]).replace('%s', item);
  6870. }
  6871. postdata._reqid = prop.reqid;
  6872. }
  6873. // replace %s in query
  6874. else if (typeof prop.query == 'string') {
  6875. query = prop.query.replace('%s', item);
  6876. query += '&_reqid=' + prop.reqid;
  6877. }
  6878. else if (typeof prop.query == 'object' && prop.query) {
  6879. query = {};
  6880. for (k in prop.query) {
  6881. query[k] = String(prop.query[k]).replace('%s', item);
  6882. }
  6883. query._reqid = prop.reqid;
  6884. }
  6885. // send HTTP GET or POST request
  6886. return postdata ? this.http_post(prop.action, postdata) : this.http_request(prop.action, query);
  6887. };
  6888. // callback function for multi-threaded http responses
  6889. this.multi_thread_http_response = function(data, reqid)
  6890. {
  6891. var prop = this.http_request_jobs[reqid];
  6892. if (!prop || prop.running <= 0 || prop.cancelled)
  6893. return;
  6894. prop.running--;
  6895. // trigger response callback
  6896. if (prop.onresponse && typeof prop.onresponse == 'function') {
  6897. prop.onresponse(data);
  6898. }
  6899. prop.result = $.extend(prop.result, data);
  6900. // send next request if prop.items is not yet empty
  6901. var item = prop._items.shift();
  6902. if (item !== undefined) {
  6903. prop.running++;
  6904. prop.requests.push(this.multi_thread_send_request(prop, item));
  6905. }
  6906. // trigger whendone callback and mark this request as done
  6907. else if (prop.running == 0) {
  6908. if (prop.whendone && typeof prop.whendone == 'function') {
  6909. prop.whendone(prop.result);
  6910. }
  6911. this.set_busy(false, '', prop.lock);
  6912. // remove from this.http_request_jobs pool
  6913. delete this.http_request_jobs[reqid];
  6914. }
  6915. };
  6916. // abort a running multi-thread request with the given identifier
  6917. this.multi_thread_request_abort = function(reqid)
  6918. {
  6919. var prop = this.http_request_jobs[reqid];
  6920. if (prop) {
  6921. for (var i=0; prop.running > 0 && i < prop.requests.length; i++) {
  6922. if (prop.requests[i].abort)
  6923. prop.requests[i].abort();
  6924. }
  6925. prop.running = 0;
  6926. prop.cancelled = true;
  6927. this.set_busy(false, '', prop.lock);
  6928. }
  6929. };
  6930. // post the given form to a hidden iframe
  6931. this.async_upload_form = function(form, action, onload)
  6932. {
  6933. // create hidden iframe
  6934. var ts = new Date().getTime(),
  6935. frame_name = 'rcmupload' + ts,
  6936. frame = this.async_upload_form_frame(frame_name);
  6937. // upload progress support
  6938. if (this.env.upload_progress_name) {
  6939. var fname = this.env.upload_progress_name,
  6940. field = $('input[name='+fname+']', form);
  6941. if (!field.length) {
  6942. field = $('<input>').attr({type: 'hidden', name: fname});
  6943. field.prependTo(form);
  6944. }
  6945. field.val(ts);
  6946. }
  6947. // handle upload errors by parsing iframe content in onload
  6948. frame.on('load', {ts:ts}, onload);
  6949. $(form).attr({
  6950. target: frame_name,
  6951. action: this.url(action, {_id: this.env.compose_id || '', _uploadid: ts, _from: this.env.action}),
  6952. method: 'POST'})
  6953. .attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
  6954. .submit();
  6955. return frame_name;
  6956. };
  6957. // create iframe element for files upload
  6958. this.async_upload_form_frame = function(name)
  6959. {
  6960. return $('<iframe>').attr({name: name, style: 'border: none; width: 0; height: 0; visibility: hidden'})
  6961. .appendTo(document.body);
  6962. };
  6963. // html5 file-drop API
  6964. this.document_drag_hover = function(e, over)
  6965. {
  6966. // don't e.preventDefault() here to not block text dragging on the page (#1490619)
  6967. $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('active');
  6968. };
  6969. this.file_drag_hover = function(e, over)
  6970. {
  6971. e.preventDefault();
  6972. e.stopPropagation();
  6973. $(this.gui_objects.filedrop)[(over?'addClass':'removeClass')]('hover');
  6974. };
  6975. // handler when files are dropped to a designated area.
  6976. // compose a multipart form data and submit it to the server
  6977. this.file_dropped = function(e)
  6978. {
  6979. // abort event and reset UI
  6980. this.file_drag_hover(e, false);
  6981. // prepare multipart form data composition
  6982. var uri, files = e.target.files || e.dataTransfer.files,
  6983. formdata = window.FormData ? new FormData() : null,
  6984. fieldname = (this.env.filedrop.fieldname || '_file') + (this.env.filedrop.single ? '' : '[]'),
  6985. boundary = '------multipartformboundary' + (new Date).getTime(),
  6986. dashdash = '--', crlf = '\r\n',
  6987. multipart = dashdash + boundary + crlf,
  6988. args = {_id: this.env.compose_id || this.env.cid || '', _remote: 1, _from: this.env.action};
  6989. if (!files || !files.length) {
  6990. // Roundcube attachment, pass its uri to the backend and attach
  6991. if (uri = e.dataTransfer.getData('roundcube-uri')) {
  6992. var ts = new Date().getTime(),
  6993. // jQuery way to escape filename (#1490530)
  6994. content = $('<span>').text(e.dataTransfer.getData('roundcube-name') || this.get_label('attaching')).html();
  6995. args._uri = uri;
  6996. args._uploadid = ts;
  6997. // add to attachments list
  6998. if (!this.add2attachment_list(ts, {name: '', html: content, classname: 'uploading', complete: false}))
  6999. this.file_upload_id = this.set_busy(true, 'attaching');
  7000. this.http_post(this.env.filedrop.action || 'upload', args);
  7001. }
  7002. return;
  7003. }
  7004. // inline function to submit the files to the server
  7005. var submit_data = function() {
  7006. var multiple = files.length > 1,
  7007. ts = new Date().getTime(),
  7008. // jQuery way to escape filename (#1490530)
  7009. content = $('<span>').text(multiple ? ref.get_label('uploadingmany') : files[0].name).html();
  7010. // add to attachments list
  7011. if (!ref.add2attachment_list(ts, { name:'', html:content, classname:'uploading', complete:false }))
  7012. ref.file_upload_id = ref.set_busy(true, 'uploading');
  7013. // complete multipart content and post request
  7014. multipart += dashdash + boundary + dashdash + crlf;
  7015. args._uploadid = ts;
  7016. $.ajax({
  7017. type: 'POST',
  7018. dataType: 'json',
  7019. url: ref.url(ref.env.filedrop.action || 'upload', args),
  7020. contentType: formdata ? false : 'multipart/form-data; boundary=' + boundary,
  7021. processData: false,
  7022. timeout: 0, // disable default timeout set in ajaxSetup()
  7023. data: formdata || multipart,
  7024. headers: {'X-Roundcube-Request': ref.env.request_token},
  7025. xhr: function() { var xhr = jQuery.ajaxSettings.xhr(); if (!formdata && xhr.sendAsBinary) xhr.send = xhr.sendAsBinary; return xhr; },
  7026. success: function(data){ ref.http_response(data); },
  7027. error: function(o, status, err) { ref.http_error(o, status, err, null, 'attachment'); }
  7028. });
  7029. };
  7030. // get contents of all dropped files
  7031. var last = this.env.filedrop.single ? 0 : files.length - 1;
  7032. for (var j=0, i=0, f; j <= last && (f = files[i]); i++) {
  7033. if (!f.name) f.name = f.fileName;
  7034. if (!f.size) f.size = f.fileSize;
  7035. if (!f.type) f.type = 'application/octet-stream';
  7036. // file name contains non-ASCII characters, do UTF8-binary string conversion.
  7037. if (!formdata && /[^\x20-\x7E]/.test(f.name))
  7038. f.name_bin = unescape(encodeURIComponent(f.name));
  7039. // filter by file type if requested
  7040. if (this.env.filedrop.filter && !f.type.match(new RegExp(this.env.filedrop.filter))) {
  7041. // TODO: show message to user
  7042. continue;
  7043. }
  7044. // do it the easy way with FormData (FF 4+, Chrome 5+, Safari 5+)
  7045. if (formdata) {
  7046. formdata.append(fieldname, f);
  7047. if (j == last)
  7048. return submit_data();
  7049. }
  7050. // use FileReader supporetd by Firefox 3.6
  7051. else if (window.FileReader) {
  7052. var reader = new FileReader();
  7053. // closure to pass file properties to async callback function
  7054. reader.onload = (function(file, j) {
  7055. return function(e) {
  7056. multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
  7057. multipart += '; filename="' + (f.name_bin || file.name) + '"' + crlf;
  7058. multipart += 'Content-Length: ' + file.size + crlf;
  7059. multipart += 'Content-Type: ' + file.type + crlf + crlf;
  7060. multipart += reader.result + crlf;
  7061. multipart += dashdash + boundary + crlf;
  7062. if (j == last) // we're done, submit the data
  7063. return submit_data();
  7064. }
  7065. })(f,j);
  7066. reader.readAsBinaryString(f);
  7067. }
  7068. // Firefox 3
  7069. else if (f.getAsBinary) {
  7070. multipart += 'Content-Disposition: form-data; name="' + fieldname + '"';
  7071. multipart += '; filename="' + (f.name_bin || f.name) + '"' + crlf;
  7072. multipart += 'Content-Length: ' + f.size + crlf;
  7073. multipart += 'Content-Type: ' + f.type + crlf + crlf;
  7074. multipart += f.getAsBinary() + crlf;
  7075. multipart += dashdash + boundary +crlf;
  7076. if (j == last)
  7077. return submit_data();
  7078. }
  7079. j++;
  7080. }
  7081. };
  7082. // starts interval for keep-alive signal
  7083. this.start_keepalive = function()
  7084. {
  7085. if (!this.env.session_lifetime || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
  7086. return;
  7087. if (this._keepalive)
  7088. clearInterval(this._keepalive);
  7089. // use Math to prevent from an integer overflow (#5273)
  7090. // maximum interval is 15 minutes, minimum is 30 seconds
  7091. var interval = Math.min(1800, this.env.session_lifetime) * 0.5 * 1000;
  7092. this._keepalive = setInterval(function() { ref.keep_alive(); }, interval < 30000 ? 30000 : interval);
  7093. };
  7094. // starts interval for refresh signal
  7095. this.start_refresh = function()
  7096. {
  7097. if (!this.env.refresh_interval || this.env.framed || this.env.extwin || this.task == 'login' || this.env.action == 'print')
  7098. return;
  7099. if (this._refresh)
  7100. clearInterval(this._refresh);
  7101. this._refresh = setInterval(function(){ ref.refresh(); }, this.env.refresh_interval * 1000);
  7102. };
  7103. // sends keep-alive signal
  7104. this.keep_alive = function()
  7105. {
  7106. if (!this.busy)
  7107. this.http_request('keep-alive');
  7108. };
  7109. // sends refresh signal
  7110. this.refresh = function()
  7111. {
  7112. if (this.busy) {
  7113. // try again after 10 seconds
  7114. setTimeout(function(){ ref.refresh(); ref.start_refresh(); }, 10000);
  7115. return;
  7116. }
  7117. var params = {}, lock = this.set_busy(true, 'refreshing');
  7118. if (this.task == 'mail' && this.gui_objects.mailboxlist)
  7119. params = this.check_recent_params();
  7120. params._last = Math.floor(this.env.lastrefresh.getTime() / 1000);
  7121. this.env.lastrefresh = new Date();
  7122. // plugins should bind to 'requestrefresh' event to add own params
  7123. this.http_post('refresh', params, lock);
  7124. };
  7125. // returns check-recent request parameters
  7126. this.check_recent_params = function()
  7127. {
  7128. var params = {_mbox: this.env.mailbox};
  7129. if (this.gui_objects.mailboxlist)
  7130. params._folderlist = 1;
  7131. if (this.gui_objects.quotadisplay)
  7132. params._quota = 1;
  7133. if (this.env.search_request)
  7134. params._search = this.env.search_request;
  7135. if (this.gui_objects.messagelist) {
  7136. params._list = 1;
  7137. // message uids for flag updates check
  7138. params._uids = $.map(this.message_list.rows, function(row, uid) { return uid; }).join(',');
  7139. }
  7140. return params;
  7141. };
  7142. /********************************************************/
  7143. /********* helper methods *********/
  7144. /********************************************************/
  7145. /**
  7146. * Quote html entities
  7147. */
  7148. this.quote_html = function(str)
  7149. {
  7150. return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
  7151. };
  7152. // get window.opener.rcmail if available
  7153. this.opener = function(deep, filter)
  7154. {
  7155. var i, win = window.opener;
  7156. // catch Error: Permission denied to access property rcmail
  7157. try {
  7158. if (win && !win.closed) {
  7159. // try parent of the opener window, e.g. preview frame
  7160. if (deep && (!win.rcmail || win.rcmail.env.framed) && win.parent && win.parent.rcmail)
  7161. win = win.parent;
  7162. if (win.rcmail && filter)
  7163. for (i in filter)
  7164. if (win.rcmail.env[i] != filter[i])
  7165. return;
  7166. return win.rcmail;
  7167. }
  7168. }
  7169. catch (e) {}
  7170. };
  7171. // check if we're in show mode or if we have a unique selection
  7172. // and return the message uid
  7173. this.get_single_uid = function()
  7174. {
  7175. var uid = this.env.uid || (this.message_list ? this.message_list.get_single_selection() : null);
  7176. var result = ref.triggerEvent('get_single_uid', { uid: uid });
  7177. return result || uid;
  7178. };
  7179. // same as above but for contacts
  7180. this.get_single_cid = function()
  7181. {
  7182. var cid = this.env.cid || (this.contact_list ? this.contact_list.get_single_selection() : null);
  7183. var result = ref.triggerEvent('get_single_cid', { cid: cid });
  7184. return result || cid;
  7185. };
  7186. // get the IMP mailbox of the message with the given UID
  7187. this.get_message_mailbox = function(uid)
  7188. {
  7189. var msg = (this.env.messages && uid ? this.env.messages[uid] : null) || {};
  7190. return msg.mbox || this.env.mailbox;
  7191. };
  7192. // build request parameters from single message id (maybe with mailbox name)
  7193. this.params_from_uid = function(uid, params)
  7194. {
  7195. if (!params)
  7196. params = {};
  7197. params._uid = String(uid).split('-')[0];
  7198. params._mbox = this.get_message_mailbox(uid);
  7199. return params;
  7200. };
  7201. // gets cursor position
  7202. this.get_caret_pos = function(obj)
  7203. {
  7204. if (obj.selectionEnd !== undefined)
  7205. return obj.selectionEnd;
  7206. return obj.value.length;
  7207. };
  7208. // moves cursor to specified position
  7209. this.set_caret_pos = function(obj, pos)
  7210. {
  7211. try {
  7212. if (obj.setSelectionRange)
  7213. obj.setSelectionRange(pos, pos);
  7214. }
  7215. catch(e) {} // catch Firefox exception if obj is hidden
  7216. };
  7217. // get selected text from an input field
  7218. this.get_input_selection = function(obj)
  7219. {
  7220. var start = 0, end = 0, normalizedValue = '';
  7221. if (typeof obj.selectionStart == "number" && typeof obj.selectionEnd == "number") {
  7222. normalizedValue = obj.value;
  7223. start = obj.selectionStart;
  7224. end = obj.selectionEnd;
  7225. }
  7226. return {start: start, end: end, text: normalizedValue.substr(start, end-start)};
  7227. };
  7228. // disable/enable all fields of a form
  7229. this.lock_form = function(form, lock)
  7230. {
  7231. if (!form || !form.elements)
  7232. return;
  7233. var n, len, elm;
  7234. if (lock)
  7235. this.disabled_form_elements = [];
  7236. for (n=0, len=form.elements.length; n<len; n++) {
  7237. elm = form.elements[n];
  7238. if (elm.type == 'hidden')
  7239. continue;
  7240. // remember which elem was disabled before lock
  7241. if (lock && elm.disabled)
  7242. this.disabled_form_elements.push(elm);
  7243. else if (lock || $.inArray(elm, this.disabled_form_elements) < 0)
  7244. elm.disabled = lock;
  7245. }
  7246. };
  7247. this.mailto_handler_uri = function()
  7248. {
  7249. return location.href.split('?')[0] + '?_task=mail&_action=compose&_to=%s';
  7250. };
  7251. this.register_protocol_handler = function(name)
  7252. {
  7253. try {
  7254. window.navigator.registerProtocolHandler('mailto', this.mailto_handler_uri(), name);
  7255. }
  7256. catch(e) {
  7257. this.display_message(String(e), 'error');
  7258. }
  7259. };
  7260. this.check_protocol_handler = function(name, elem)
  7261. {
  7262. var nav = window.navigator;
  7263. if (!nav || (typeof nav.registerProtocolHandler != 'function')) {
  7264. $(elem).addClass('disabled').click(function(){ return false; });
  7265. }
  7266. else if (typeof nav.isProtocolHandlerRegistered == 'function') {
  7267. var status = nav.isProtocolHandlerRegistered('mailto', this.mailto_handler_uri());
  7268. if (status)
  7269. $(elem).parent().find('.mailtoprotohandler-status').html(status);
  7270. }
  7271. else {
  7272. $(elem).click(function() { ref.register_protocol_handler(name); return false; });
  7273. }
  7274. };
  7275. // Checks browser capabilities eg. PDF support, TIF support
  7276. this.browser_capabilities_check = function()
  7277. {
  7278. if (!this.env.browser_capabilities)
  7279. this.env.browser_capabilities = {};
  7280. $.each(['pdf', 'flash', 'tif'], function() {
  7281. if (ref.env.browser_capabilities[this] === undefined)
  7282. ref.env.browser_capabilities[this] = ref[this + '_support_check']();
  7283. });
  7284. };
  7285. // Returns browser capabilities string
  7286. this.browser_capabilities = function()
  7287. {
  7288. if (!this.env.browser_capabilities)
  7289. return '';
  7290. var n, ret = [];
  7291. for (n in this.env.browser_capabilities)
  7292. ret.push(n + '=' + this.env.browser_capabilities[n]);
  7293. return ret.join();
  7294. };
  7295. this.tif_support_check = function()
  7296. {
  7297. window.setTimeout(function() {
  7298. var img = new Image();
  7299. img.onload = function() { ref.env.browser_capabilities.tif = 1; };
  7300. img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
  7301. img.src = ref.assets_path('program/resources/blank.tif');
  7302. }, 10);
  7303. return 0;
  7304. };
  7305. this.pdf_support_check = function()
  7306. {
  7307. var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
  7308. plugins = navigator.plugins,
  7309. len = plugins.length,
  7310. regex = /Adobe Reader|PDF|Acrobat/i;
  7311. if (plugin && plugin.enabledPlugin)
  7312. return 1;
  7313. if ('ActiveXObject' in window) {
  7314. try {
  7315. if (plugin = new ActiveXObject("AcroPDF.PDF"))
  7316. return 1;
  7317. }
  7318. catch (e) {}
  7319. try {
  7320. if (plugin = new ActiveXObject("PDF.PdfCtrl"))
  7321. return 1;
  7322. }
  7323. catch (e) {}
  7324. }
  7325. for (i=0; i<len; i++) {
  7326. plugin = plugins[i];
  7327. if (typeof plugin === 'String') {
  7328. if (regex.test(plugin))
  7329. return 1;
  7330. }
  7331. else if (plugin.name && regex.test(plugin.name))
  7332. return 1;
  7333. }
  7334. window.setTimeout(function() {
  7335. $('<object>').css({position: 'absolute', left: '-10000px'})
  7336. .attr({data: ref.assets_path('program/resources/dummy.pdf'), width: 1, height: 1, type: 'application/pdf'})
  7337. .load(function() { ref.env.browser_capabilities.pdf = 1; })
  7338. .error(function() { ref.env.browser_capabilities.pdf = 0; })
  7339. .appendTo($('body'));
  7340. }, 10);
  7341. return 0;
  7342. };
  7343. this.flash_support_check = function()
  7344. {
  7345. var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
  7346. if (plugin && plugin.enabledPlugin)
  7347. return 1;
  7348. if ('ActiveXObject' in window) {
  7349. try {
  7350. if (plugin = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
  7351. return 1;
  7352. }
  7353. catch (e) {}
  7354. }
  7355. return 0;
  7356. };
  7357. this.assets_path = function(path)
  7358. {
  7359. if (this.env.assets_path && !path.startsWith(this.env.assets_path)) {
  7360. path = this.env.assets_path + path;
  7361. }
  7362. return path;
  7363. };
  7364. // Cookie setter
  7365. this.set_cookie = function(name, value, expires)
  7366. {
  7367. setCookie(name, value, expires, this.env.cookie_path, this.env.cookie_domain, this.env.cookie_secure);
  7368. };
  7369. this.get_local_storage_prefix = function()
  7370. {
  7371. if (!this.local_storage_prefix)
  7372. this.local_storage_prefix = 'roundcube.' + (this.env.user_id || 'anonymous') + '.';
  7373. return this.local_storage_prefix;
  7374. };
  7375. // wrapper for localStorage.getItem(key)
  7376. this.local_storage_get_item = function(key, deflt, encrypted)
  7377. {
  7378. var item, result;
  7379. // TODO: add encryption
  7380. try {
  7381. item = localStorage.getItem(this.get_local_storage_prefix() + key);
  7382. result = JSON.parse(item);
  7383. }
  7384. catch (e) { }
  7385. return result || deflt || null;
  7386. };
  7387. // wrapper for localStorage.setItem(key, data)
  7388. this.local_storage_set_item = function(key, data, encrypted)
  7389. {
  7390. // try/catch to handle no localStorage support, but also error
  7391. // in Safari-in-private-browsing-mode where localStorage exists
  7392. // but can't be used (#1489996)
  7393. try {
  7394. // TODO: add encryption
  7395. localStorage.setItem(this.get_local_storage_prefix() + key, JSON.stringify(data));
  7396. return true;
  7397. }
  7398. catch (e) {
  7399. return false;
  7400. }
  7401. };
  7402. // wrapper for localStorage.removeItem(key)
  7403. this.local_storage_remove_item = function(key)
  7404. {
  7405. try {
  7406. localStorage.removeItem(this.get_local_storage_prefix() + key);
  7407. return true;
  7408. }
  7409. catch (e) {
  7410. return false;
  7411. }
  7412. };
  7413. this.print_dialog = function()
  7414. {
  7415. if (bw.safari)
  7416. setTimeout('window.print()', 10);
  7417. else
  7418. window.print();
  7419. };
  7420. } // end object rcube_webmail
  7421. // some static methods
  7422. rcube_webmail.long_subject_title = function(elem, indent)
  7423. {
  7424. if (!elem.title) {
  7425. var $elem = $(elem);
  7426. if ($elem.width() + (indent || 0) * 15 > $elem.parent().width())
  7427. elem.title = rcube_webmail.subject_text(elem);
  7428. }
  7429. };
  7430. rcube_webmail.long_subject_title_ex = function(elem)
  7431. {
  7432. if (!elem.title) {
  7433. var $elem = $(elem),
  7434. txt = $.trim($elem.text()),
  7435. tmp = $('<span>').text(txt)
  7436. .css({'position': 'absolute', 'float': 'left', 'visibility': 'hidden',
  7437. 'font-size': $elem.css('font-size'), 'font-weight': $elem.css('font-weight')})
  7438. .appendTo($('body')),
  7439. w = tmp.width();
  7440. tmp.remove();
  7441. if (w + $('span.branch', $elem).width() * 15 > $elem.width())
  7442. elem.title = rcube_webmail.subject_text(elem);
  7443. }
  7444. };
  7445. rcube_webmail.subject_text = function(elem)
  7446. {
  7447. var t = $(elem).clone();
  7448. t.find('.skip-on-drag').remove();
  7449. return t.text();
  7450. };
  7451. // set event handlers on all iframe elements (and their contents)
  7452. rcube_webmail.set_iframe_events = function(events)
  7453. {
  7454. $('iframe').each(function() {
  7455. var frame = $(this);
  7456. $.each(events, function(event_name, event_handler) {
  7457. frame.on('load', function(e) {
  7458. try { $(this).contents().on(event_name, event_handler); }
  7459. catch (e) {/* catch possible permission error in IE */ }
  7460. });
  7461. try { frame.contents().on(event_name, event_handler); }
  7462. catch (e) {/* catch possible permission error in IE */ }
  7463. });
  7464. });
  7465. };
  7466. rcube_webmail.prototype.get_cookie = getCookie;
  7467. // copy event engine prototype
  7468. rcube_webmail.prototype.addEventListener = rcube_event_engine.prototype.addEventListener;
  7469. rcube_webmail.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener;
  7470. rcube_webmail.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent;