You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rcube_imap.php 139KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918391939203921392239233924392539263927392839293930393139323933393439353936393739383939394039413942394339443945394639473948394939503951395239533954395539563957395839593960396139623963396439653966396739683969397039713972397339743975397639773978397939803981398239833984398539863987398839893990399139923993399439953996399739983999400040014002400340044005400640074008400940104011401240134014401540164017401840194020402140224023402440254026402740284029403040314032403340344035403640374038403940404041404240434044404540464047404840494050405140524053405440554056405740584059406040614062406340644065406640674068406940704071407240734074407540764077407840794080408140824083408440854086408740884089409040914092409340944095409640974098409941004101410241034104410541064107410841094110411141124113411441154116411741184119412041214122412341244125412641274128412941304131413241334134413541364137413841394140414141424143414441454146414741484149415041514152415341544155415641574158415941604161416241634164416541664167416841694170417141724173417441754176417741784179418041814182418341844185418641874188418941904191419241934194419541964197419841994200420142024203420442054206420742084209421042114212421342144215421642174218421942204221422242234224422542264227422842294230423142324233423442354236423742384239424042414242424342444245424642474248424942504251425242534254425542564257425842594260426142624263426442654266426742684269427042714272427342744275427642774278427942804281428242834284428542864287428842894290429142924293429442954296429742984299430043014302430343044305430643074308430943104311431243134314431543164317431843194320432143224323432443254326432743284329433043314332433343344335433643374338433943404341434243434344434543464347434843494350435143524353435443554356435743584359436043614362436343644365436643674368436943704371437243734374437543764377437843794380438143824383438443854386438743884389439043914392
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2005-2012, The Roundcube Dev Team |
  6. | Copyright (C) 2011-2012, Kolab Systems AG |
  7. | |
  8. | Licensed under the GNU General Public License version 3 or |
  9. | any later version with exceptions for skins & plugins. |
  10. | See the README file for a full license statement. |
  11. | |
  12. | PURPOSE: |
  13. | IMAP Storage Engine |
  14. +-----------------------------------------------------------------------+
  15. | Author: Thomas Bruederli <roundcube@gmail.com> |
  16. | Author: Aleksander Machniak <alec@alec.pl> |
  17. +-----------------------------------------------------------------------+
  18. */
  19. /**
  20. * Interface class for accessing an IMAP server
  21. *
  22. * @package Framework
  23. * @subpackage Storage
  24. * @author Thomas Bruederli <roundcube@gmail.com>
  25. * @author Aleksander Machniak <alec@alec.pl>
  26. */
  27. class rcube_imap extends rcube_storage
  28. {
  29. /**
  30. * Instance of rcube_imap_generic
  31. *
  32. * @var rcube_imap_generic
  33. */
  34. public $conn;
  35. /**
  36. * Instance of rcube_imap_cache
  37. *
  38. * @var rcube_imap_cache
  39. */
  40. protected $mcache;
  41. /**
  42. * Instance of rcube_cache
  43. *
  44. * @var rcube_cache
  45. */
  46. protected $cache;
  47. /**
  48. * Internal (in-memory) cache
  49. *
  50. * @var array
  51. */
  52. protected $icache = array();
  53. protected $plugins;
  54. protected $delimiter;
  55. protected $namespace;
  56. protected $sort_field = '';
  57. protected $sort_order = 'DESC';
  58. protected $struct_charset;
  59. protected $search_set;
  60. protected $search_string = '';
  61. protected $search_charset = '';
  62. protected $search_sort_field = '';
  63. protected $search_threads = false;
  64. protected $search_sorted = false;
  65. protected $options = array('auth_type' => 'check');
  66. protected $caching = false;
  67. protected $messages_caching = false;
  68. protected $threading = false;
  69. /**
  70. * Object constructor.
  71. */
  72. public function __construct()
  73. {
  74. $this->conn = new rcube_imap_generic();
  75. $this->plugins = rcube::get_instance()->plugins;
  76. // Set namespace and delimiter from session,
  77. // so some methods would work before connection
  78. if (isset($_SESSION['imap_namespace'])) {
  79. $this->namespace = $_SESSION['imap_namespace'];
  80. }
  81. if (isset($_SESSION['imap_delimiter'])) {
  82. $this->delimiter = $_SESSION['imap_delimiter'];
  83. }
  84. }
  85. /**
  86. * Magic getter for backward compat.
  87. *
  88. * @deprecated.
  89. */
  90. public function __get($name)
  91. {
  92. if (isset($this->{$name})) {
  93. return $this->{$name};
  94. }
  95. }
  96. /**
  97. * Connect to an IMAP server
  98. *
  99. * @param string $host Host to connect
  100. * @param string $user Username for IMAP account
  101. * @param string $pass Password for IMAP account
  102. * @param integer $port Port to connect to
  103. * @param string $use_ssl SSL schema (either ssl or tls) or null if plain connection
  104. *
  105. * @return boolean True on success, False on failure
  106. */
  107. public function connect($host, $user, $pass, $port=143, $use_ssl=null)
  108. {
  109. // check for OpenSSL support in PHP build
  110. if ($use_ssl && extension_loaded('openssl')) {
  111. $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
  112. }
  113. else if ($use_ssl) {
  114. rcube::raise_error(array('code' => 403, 'type' => 'imap',
  115. 'file' => __FILE__, 'line' => __LINE__,
  116. 'message' => "OpenSSL not available"), true, false);
  117. $port = 143;
  118. }
  119. $this->options['port'] = $port;
  120. if ($this->options['debug']) {
  121. $this->set_debug(true);
  122. $this->options['ident'] = array(
  123. 'name' => 'Roundcube',
  124. 'version' => RCUBE_VERSION,
  125. 'php' => PHP_VERSION,
  126. 'os' => PHP_OS,
  127. 'command' => $_SERVER['REQUEST_URI'],
  128. );
  129. }
  130. $attempt = 0;
  131. do {
  132. $data = $this->plugins->exec_hook('storage_connect',
  133. array_merge($this->options, array('host' => $host, 'user' => $user,
  134. 'attempt' => ++$attempt)));
  135. if (!empty($data['pass'])) {
  136. $pass = $data['pass'];
  137. }
  138. $this->conn->connect($data['host'], $data['user'], $pass, $data);
  139. } while(!$this->conn->connected() && $data['retry']);
  140. $config = array(
  141. 'host' => $data['host'],
  142. 'user' => $data['user'],
  143. 'password' => $pass,
  144. 'port' => $port,
  145. 'ssl' => $use_ssl,
  146. );
  147. $this->options = array_merge($this->options, $config);
  148. $this->connect_done = true;
  149. if ($this->conn->connected()) {
  150. // check for session identifier
  151. $session = null;
  152. if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) {
  153. $session = $m[1];
  154. }
  155. // get namespace and delimiter
  156. $this->set_env();
  157. // trigger post-connect hook
  158. $this->plugins->exec_hook('storage_connected', array(
  159. 'host' => $host, 'user' => $user, 'session' => $session
  160. ));
  161. return true;
  162. }
  163. // write error log
  164. else if ($this->conn->error) {
  165. if ($pass && $user) {
  166. $message = sprintf("Login failed for %s from %s. %s",
  167. $user, rcube_utils::remote_ip(), $this->conn->error);
  168. rcube::raise_error(array('code' => 403, 'type' => 'imap',
  169. 'file' => __FILE__, 'line' => __LINE__,
  170. 'message' => $message), true, false);
  171. }
  172. }
  173. return false;
  174. }
  175. /**
  176. * Close IMAP connection.
  177. * Usually done on script shutdown
  178. */
  179. public function close()
  180. {
  181. $this->connect_done = false;
  182. $this->conn->closeConnection();
  183. if ($this->mcache) {
  184. $this->mcache->close();
  185. }
  186. }
  187. /**
  188. * Check connection state, connect if not connected.
  189. *
  190. * @return bool Connection state.
  191. */
  192. public function check_connection()
  193. {
  194. // Establish connection if it wasn't done yet
  195. if (!$this->connect_done && !empty($this->options['user'])) {
  196. return $this->connect(
  197. $this->options['host'],
  198. $this->options['user'],
  199. $this->options['password'],
  200. $this->options['port'],
  201. $this->options['ssl']
  202. );
  203. }
  204. return $this->is_connected();
  205. }
  206. /**
  207. * Checks IMAP connection.
  208. *
  209. * @return boolean TRUE on success, FALSE on failure
  210. */
  211. public function is_connected()
  212. {
  213. return $this->conn->connected();
  214. }
  215. /**
  216. * Returns code of last error
  217. *
  218. * @return int Error code
  219. */
  220. public function get_error_code()
  221. {
  222. return $this->conn->errornum;
  223. }
  224. /**
  225. * Returns text of last error
  226. *
  227. * @return string Error string
  228. */
  229. public function get_error_str()
  230. {
  231. return $this->conn->error;
  232. }
  233. /**
  234. * Returns code of last command response
  235. *
  236. * @return int Response code
  237. */
  238. public function get_response_code()
  239. {
  240. switch ($this->conn->resultcode) {
  241. case 'NOPERM':
  242. return self::NOPERM;
  243. case 'READ-ONLY':
  244. return self::READONLY;
  245. case 'TRYCREATE':
  246. return self::TRYCREATE;
  247. case 'INUSE':
  248. return self::INUSE;
  249. case 'OVERQUOTA':
  250. return self::OVERQUOTA;
  251. case 'ALREADYEXISTS':
  252. return self::ALREADYEXISTS;
  253. case 'NONEXISTENT':
  254. return self::NONEXISTENT;
  255. case 'CONTACTADMIN':
  256. return self::CONTACTADMIN;
  257. default:
  258. return self::UNKNOWN;
  259. }
  260. }
  261. /**
  262. * Activate/deactivate debug mode
  263. *
  264. * @param boolean $dbg True if IMAP conversation should be logged
  265. */
  266. public function set_debug($dbg = true)
  267. {
  268. $this->options['debug'] = $dbg;
  269. $this->conn->setDebug($dbg, array($this, 'debug_handler'));
  270. }
  271. /**
  272. * Set internal folder reference.
  273. * All operations will be perfomed on this folder.
  274. *
  275. * @param string $folder Folder name
  276. */
  277. public function set_folder($folder)
  278. {
  279. $this->folder = $folder;
  280. }
  281. /**
  282. * Save a search result for future message listing methods
  283. *
  284. * @param array $set Search set, result from rcube_imap::get_search_set():
  285. * 0 - searching criteria, string
  286. * 1 - search result, rcube_result_index|rcube_result_thread
  287. * 2 - searching character set, string
  288. * 3 - sorting field, string
  289. * 4 - true if sorted, bool
  290. */
  291. public function set_search_set($set)
  292. {
  293. $set = (array)$set;
  294. $this->search_string = $set[0];
  295. $this->search_set = $set[1];
  296. $this->search_charset = $set[2];
  297. $this->search_sort_field = $set[3];
  298. $this->search_sorted = $set[4];
  299. $this->search_threads = is_a($this->search_set, 'rcube_result_thread');
  300. if (is_a($this->search_set, 'rcube_result_multifolder')) {
  301. $this->set_threading(false);
  302. }
  303. }
  304. /**
  305. * Return the saved search set as hash array
  306. *
  307. * @return array Search set
  308. */
  309. public function get_search_set()
  310. {
  311. if (empty($this->search_set)) {
  312. return null;
  313. }
  314. return array(
  315. $this->search_string,
  316. $this->search_set,
  317. $this->search_charset,
  318. $this->search_sort_field,
  319. $this->search_sorted,
  320. );
  321. }
  322. /**
  323. * Returns the IMAP server's capability.
  324. *
  325. * @param string $cap Capability name
  326. *
  327. * @return mixed Capability value or TRUE if supported, FALSE if not
  328. */
  329. public function get_capability($cap)
  330. {
  331. $cap = strtoupper($cap);
  332. $sess_key = "STORAGE_$cap";
  333. if (!isset($_SESSION[$sess_key])) {
  334. if (!$this->check_connection()) {
  335. return false;
  336. }
  337. $_SESSION[$sess_key] = $this->conn->getCapability($cap);
  338. }
  339. return $_SESSION[$sess_key];
  340. }
  341. /**
  342. * Checks the PERMANENTFLAGS capability of the current folder
  343. * and returns true if the given flag is supported by the IMAP server
  344. *
  345. * @param string $flag Permanentflag name
  346. *
  347. * @return boolean True if this flag is supported
  348. */
  349. public function check_permflag($flag)
  350. {
  351. $flag = strtoupper($flag);
  352. $perm_flags = $this->get_permflags($this->folder);
  353. $imap_flag = $this->conn->flags[$flag];
  354. return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
  355. }
  356. /**
  357. * Returns PERMANENTFLAGS of the specified folder
  358. *
  359. * @param string $folder Folder name
  360. *
  361. * @return array Flags
  362. */
  363. public function get_permflags($folder)
  364. {
  365. if (!strlen($folder)) {
  366. return array();
  367. }
  368. if (!$this->check_connection()) {
  369. return array();
  370. }
  371. if ($this->conn->select($folder)) {
  372. $permflags = $this->conn->data['PERMANENTFLAGS'];
  373. }
  374. else {
  375. return array();
  376. }
  377. if (!is_array($permflags)) {
  378. $permflags = array();
  379. }
  380. return $permflags;
  381. }
  382. /**
  383. * Returns the delimiter that is used by the IMAP server for folder separation
  384. *
  385. * @return string Delimiter string
  386. */
  387. public function get_hierarchy_delimiter()
  388. {
  389. return $this->delimiter;
  390. }
  391. /**
  392. * Get namespace
  393. *
  394. * @param string $name Namespace array index: personal, other, shared, prefix
  395. *
  396. * @return array Namespace data
  397. */
  398. public function get_namespace($name = null)
  399. {
  400. $ns = $this->namespace;
  401. if ($name) {
  402. return isset($ns[$name]) ? $ns[$name] : null;
  403. }
  404. unset($ns['prefix_in'], $ns['prefix_out']);
  405. return $ns;
  406. }
  407. /**
  408. * Sets delimiter and namespaces
  409. */
  410. protected function set_env()
  411. {
  412. if ($this->delimiter !== null && $this->namespace !== null) {
  413. return;
  414. }
  415. $config = rcube::get_instance()->config;
  416. $imap_personal = $config->get('imap_ns_personal');
  417. $imap_other = $config->get('imap_ns_other');
  418. $imap_shared = $config->get('imap_ns_shared');
  419. $imap_delimiter = $config->get('imap_delimiter');
  420. if (!$this->check_connection()) {
  421. return;
  422. }
  423. $ns = $this->conn->getNamespace();
  424. // Set namespaces (NAMESPACE supported)
  425. if (is_array($ns)) {
  426. $this->namespace = $ns;
  427. }
  428. else {
  429. $this->namespace = array(
  430. 'personal' => NULL,
  431. 'other' => NULL,
  432. 'shared' => NULL,
  433. );
  434. }
  435. if ($imap_delimiter) {
  436. $this->delimiter = $imap_delimiter;
  437. }
  438. if (empty($this->delimiter)) {
  439. $this->delimiter = $this->namespace['personal'][0][1];
  440. }
  441. if (empty($this->delimiter)) {
  442. $this->delimiter = $this->conn->getHierarchyDelimiter();
  443. }
  444. if (empty($this->delimiter)) {
  445. $this->delimiter = '/';
  446. }
  447. // Overwrite namespaces
  448. if ($imap_personal !== null) {
  449. $this->namespace['personal'] = NULL;
  450. foreach ((array)$imap_personal as $dir) {
  451. $this->namespace['personal'][] = array($dir, $this->delimiter);
  452. }
  453. }
  454. if ($imap_other !== null) {
  455. $this->namespace['other'] = NULL;
  456. foreach ((array)$imap_other as $dir) {
  457. if ($dir) {
  458. $this->namespace['other'][] = array($dir, $this->delimiter);
  459. }
  460. }
  461. }
  462. if ($imap_shared !== null) {
  463. $this->namespace['shared'] = NULL;
  464. foreach ((array)$imap_shared as $dir) {
  465. if ($dir) {
  466. $this->namespace['shared'][] = array($dir, $this->delimiter);
  467. }
  468. }
  469. }
  470. // Find personal namespace prefix(es) for self::mod_folder()
  471. if (is_array($this->namespace['personal']) && !empty($this->namespace['personal'])) {
  472. // There can be more than one namespace root,
  473. // - for prefix_out get the first one but only
  474. // if there is only one root
  475. // - for prefix_in get the first one but only
  476. // if there is no non-prefixed namespace root (#5403)
  477. $roots = array();
  478. foreach ($this->namespace['personal'] as $ns) {
  479. $roots[] = $ns[0];
  480. }
  481. if (!in_array('', $roots)) {
  482. $this->namespace['prefix_in'] = $roots[0];
  483. }
  484. if (count($roots) == 1) {
  485. $this->namespace['prefix_out'] = $roots[0];
  486. }
  487. }
  488. $_SESSION['imap_namespace'] = $this->namespace;
  489. $_SESSION['imap_delimiter'] = $this->delimiter;
  490. }
  491. /**
  492. * Returns IMAP server vendor name
  493. *
  494. * @return string Vendor name
  495. * @since 1.2
  496. */
  497. public function get_vendor()
  498. {
  499. if ($_SESSION['imap_vendor'] !== null) {
  500. return $_SESSION['imap_vendor'];
  501. }
  502. $config = rcube::get_instance()->config;
  503. $imap_vendor = $config->get('imap_vendor');
  504. if ($imap_vendor) {
  505. return $imap_vendor;
  506. }
  507. if (!$this->check_connection()) {
  508. return;
  509. }
  510. if (($ident = $this->conn->data['ID']) === null) {
  511. $ident = $this->conn->id(array(
  512. 'name' => 'Roundcube',
  513. 'version' => RCUBE_VERSION,
  514. 'php' => PHP_VERSION,
  515. 'os' => PHP_OS,
  516. ));
  517. }
  518. $vendor = (string) (!empty($ident) ? $ident['name'] : '');
  519. $ident = strtolower($vendor . ' ' . $this->conn->data['GREETING']);
  520. $vendors = array('cyrus', 'dovecot', 'uw-imap', 'gmail', 'hmail');
  521. foreach ($vendors as $v) {
  522. if (strpos($ident, $v) !== false) {
  523. $vendor = $v;
  524. break;
  525. }
  526. }
  527. return $_SESSION['imap_vendor'] = $vendor;
  528. }
  529. /**
  530. * Get message count for a specific folder
  531. *
  532. * @param string $folder Folder name
  533. * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
  534. * @param boolean $force Force reading from server and update cache
  535. * @param boolean $status Enables storing folder status info (max UID/count),
  536. * required for folder_status()
  537. *
  538. * @return int Number of messages
  539. */
  540. public function count($folder='', $mode='ALL', $force=false, $status=true)
  541. {
  542. if (!strlen($folder)) {
  543. $folder = $this->folder;
  544. }
  545. return $this->countmessages($folder, $mode, $force, $status);
  546. }
  547. /**
  548. * Protected method for getting number of messages
  549. *
  550. * @param string $folder Folder name
  551. * @param string $mode Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
  552. * @param boolean $force Force reading from server and update cache
  553. * @param boolean $status Enables storing folder status info (max UID/count),
  554. * required for folder_status()
  555. * @param boolean $no_search Ignore current search result
  556. *
  557. * @return int Number of messages
  558. * @see rcube_imap::count()
  559. */
  560. protected function countmessages($folder, $mode = 'ALL', $force = false, $status = true, $no_search = false)
  561. {
  562. $mode = strtoupper($mode);
  563. // Count search set, assume search set is always up-to-date (don't check $force flag)
  564. // @TODO: this could be handled in more reliable way, e.g. a separate method
  565. // maybe in rcube_imap_search
  566. if (!$no_search && $this->search_string && $folder == $this->folder) {
  567. if ($mode == 'ALL') {
  568. return $this->search_set->count_messages();
  569. }
  570. else if ($mode == 'THREADS') {
  571. return $this->search_set->count();
  572. }
  573. }
  574. // EXISTS is a special alias for ALL, it allows to get the number
  575. // of all messages in a folder also when search is active and with
  576. // any skip_deleted setting
  577. $a_folder_cache = $this->get_cache('messagecount');
  578. // return cached value
  579. if (!$force && is_array($a_folder_cache[$folder]) && isset($a_folder_cache[$folder][$mode])) {
  580. return $a_folder_cache[$folder][$mode];
  581. }
  582. if (!is_array($a_folder_cache[$folder])) {
  583. $a_folder_cache[$folder] = array();
  584. }
  585. if ($mode == 'THREADS') {
  586. $res = $this->threads($folder);
  587. $count = $res->count();
  588. if ($status) {
  589. $msg_count = $res->count_messages();
  590. $this->set_folder_stats($folder, 'cnt', $msg_count);
  591. $this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0);
  592. }
  593. }
  594. // Need connection here
  595. else if (!$this->check_connection()) {
  596. return 0;
  597. }
  598. // RECENT count is fetched a bit different
  599. else if ($mode == 'RECENT') {
  600. $count = $this->conn->countRecent($folder);
  601. }
  602. // use SEARCH for message counting
  603. else if ($mode != 'EXISTS' && !empty($this->options['skip_deleted'])) {
  604. $search_str = "ALL UNDELETED";
  605. $keys = array('COUNT');
  606. if ($mode == 'UNSEEN') {
  607. $search_str .= " UNSEEN";
  608. }
  609. else {
  610. if ($this->messages_caching) {
  611. $keys[] = 'ALL';
  612. }
  613. if ($status) {
  614. $keys[] = 'MAX';
  615. }
  616. }
  617. // @TODO: if $mode == 'ALL' we could try to use cache index here
  618. // get message count using (E)SEARCH
  619. // not very performant but more precise (using UNDELETED)
  620. $index = $this->conn->search($folder, $search_str, true, $keys);
  621. $count = $index->count();
  622. if ($mode == 'ALL') {
  623. // Cache index data, will be used in index_direct()
  624. $this->icache['undeleted_idx'] = $index;
  625. if ($status) {
  626. $this->set_folder_stats($folder, 'cnt', $count);
  627. $this->set_folder_stats($folder, 'maxuid', $index->max());
  628. }
  629. }
  630. }
  631. else {
  632. if ($mode == 'UNSEEN') {
  633. $count = $this->conn->countUnseen($folder);
  634. }
  635. else {
  636. $count = $this->conn->countMessages($folder);
  637. if ($status && $mode == 'ALL') {
  638. $this->set_folder_stats($folder, 'cnt', $count);
  639. $this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0);
  640. }
  641. }
  642. }
  643. $a_folder_cache[$folder][$mode] = (int)$count;
  644. // write back to cache
  645. $this->update_cache('messagecount', $a_folder_cache);
  646. return (int)$count;
  647. }
  648. /**
  649. * Public method for listing message flags
  650. *
  651. * @param string $folder Folder name
  652. * @param array $uids Message UIDs
  653. * @param int $mod_seq Optional MODSEQ value (of last flag update)
  654. *
  655. * @return array Indexed array with message flags
  656. */
  657. public function list_flags($folder, $uids, $mod_seq = null)
  658. {
  659. if (!strlen($folder)) {
  660. $folder = $this->folder;
  661. }
  662. if (!$this->check_connection()) {
  663. return array();
  664. }
  665. // @TODO: when cache was synchronized in this request
  666. // we might already have asked for flag updates, use it.
  667. $flags = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
  668. $result = array();
  669. if (!empty($flags)) {
  670. foreach ($flags as $message) {
  671. $result[$message->uid] = $message->flags;
  672. }
  673. }
  674. return $result;
  675. }
  676. /**
  677. * Public method for listing headers
  678. *
  679. * @param string $folder Folder name
  680. * @param int $page Current page to list
  681. * @param string $sort_field Header field to sort by
  682. * @param string $sort_order Sort order [ASC|DESC]
  683. * @param int $slice Number of slice items to extract from result array
  684. *
  685. * @return array Indexed array with message header objects
  686. */
  687. public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  688. {
  689. if (!strlen($folder)) {
  690. $folder = $this->folder;
  691. }
  692. return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
  693. }
  694. /**
  695. * protected method for listing message headers
  696. *
  697. * @param string $folder Folder name
  698. * @param int $page Current page to list
  699. * @param string $sort_field Header field to sort by
  700. * @param string $sort_order Sort order [ASC|DESC]
  701. * @param int $slice Number of slice items to extract from result array
  702. *
  703. * @return array Indexed array with message header objects
  704. * @see rcube_imap::list_messages
  705. */
  706. protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
  707. {
  708. if (!strlen($folder)) {
  709. return array();
  710. }
  711. $this->set_sort_order($sort_field, $sort_order);
  712. $page = $page ? $page : $this->list_page;
  713. // use saved message set
  714. if ($this->search_string) {
  715. return $this->list_search_messages($folder, $page, $slice);
  716. }
  717. if ($this->threading) {
  718. return $this->list_thread_messages($folder, $page, $slice);
  719. }
  720. // get UIDs of all messages in the folder, sorted
  721. $index = $this->index($folder, $this->sort_field, $this->sort_order);
  722. if ($index->is_empty()) {
  723. return array();
  724. }
  725. $from = ($page-1) * $this->page_size;
  726. $to = $from + $this->page_size;
  727. $index->slice($from, $to - $from);
  728. if ($slice) {
  729. $index->slice(-$slice, $slice);
  730. }
  731. // fetch reqested messages headers
  732. $a_index = $index->get();
  733. $a_msg_headers = $this->fetch_headers($folder, $a_index);
  734. return array_values($a_msg_headers);
  735. }
  736. /**
  737. * protected method for listing message headers using threads
  738. *
  739. * @param string $folder Folder name
  740. * @param int $page Current page to list
  741. * @param int $slice Number of slice items to extract from result array
  742. *
  743. * @return array Indexed array with message header objects
  744. * @see rcube_imap::list_messages
  745. */
  746. protected function list_thread_messages($folder, $page, $slice=0)
  747. {
  748. // get all threads (not sorted)
  749. if ($mcache = $this->get_mcache_engine()) {
  750. $threads = $mcache->get_thread($folder);
  751. }
  752. else {
  753. $threads = $this->threads($folder);
  754. }
  755. return $this->fetch_thread_headers($folder, $threads, $page, $slice);
  756. }
  757. /**
  758. * Method for fetching threads data
  759. *
  760. * @param string $folder Folder name
  761. *
  762. * @return rcube_imap_thread Thread data object
  763. */
  764. function threads($folder)
  765. {
  766. if ($mcache = $this->get_mcache_engine()) {
  767. // don't store in self's internal cache, cache has it's own internal cache
  768. return $mcache->get_thread($folder);
  769. }
  770. if (!empty($this->icache['threads'])) {
  771. if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
  772. return $this->icache['threads'];
  773. }
  774. }
  775. // get all threads
  776. $result = $this->threads_direct($folder);
  777. // add to internal (fast) cache
  778. return $this->icache['threads'] = $result;
  779. }
  780. /**
  781. * Method for direct fetching of threads data
  782. *
  783. * @param string $folder Folder name
  784. *
  785. * @return rcube_imap_thread Thread data object
  786. */
  787. function threads_direct($folder)
  788. {
  789. if (!$this->check_connection()) {
  790. return new rcube_result_thread();
  791. }
  792. // get all threads
  793. return $this->conn->thread($folder, $this->threading,
  794. $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
  795. }
  796. /**
  797. * protected method for fetching threaded messages headers
  798. *
  799. * @param string $folder Folder name
  800. * @param rcube_result_thread $threads Threads data object
  801. * @param int $page List page number
  802. * @param int $slice Number of threads to slice
  803. *
  804. * @return array Messages headers
  805. */
  806. protected function fetch_thread_headers($folder, $threads, $page, $slice=0)
  807. {
  808. // Sort thread structure
  809. $this->sort_threads($threads);
  810. $from = ($page-1) * $this->page_size;
  811. $to = $from + $this->page_size;
  812. $threads->slice($from, $to - $from);
  813. if ($slice) {
  814. $threads->slice(-$slice, $slice);
  815. }
  816. // Get UIDs of all messages in all threads
  817. $a_index = $threads->get();
  818. // fetch reqested headers from server
  819. $a_msg_headers = $this->fetch_headers($folder, $a_index);
  820. unset($a_index);
  821. // Set depth, has_children and unread_children fields in headers
  822. $this->set_thread_flags($a_msg_headers, $threads);
  823. return array_values($a_msg_headers);
  824. }
  825. /**
  826. * protected method for setting threaded messages flags:
  827. * depth, has_children and unread_children
  828. *
  829. * @param array $headers Reference to headers array indexed by message UID
  830. * @param rcube_result_thread $threads Threads data object
  831. *
  832. * @return array Message headers array indexed by message UID
  833. */
  834. protected function set_thread_flags(&$headers, $threads)
  835. {
  836. $parents = array();
  837. list ($msg_depth, $msg_children) = $threads->get_thread_data();
  838. foreach ($headers as $uid => $header) {
  839. $depth = $msg_depth[$uid];
  840. $parents = array_slice($parents, 0, $depth);
  841. if (!empty($parents)) {
  842. $headers[$uid]->parent_uid = end($parents);
  843. if (empty($header->flags['SEEN']))
  844. $headers[$parents[0]]->unread_children++;
  845. }
  846. array_push($parents, $uid);
  847. $headers[$uid]->depth = $depth;
  848. $headers[$uid]->has_children = $msg_children[$uid];
  849. }
  850. }
  851. /**
  852. * protected method for listing a set of message headers (search results)
  853. *
  854. * @param string $folder Folder name
  855. * @param int $page Current page to list
  856. * @param int $slice Number of slice items to extract from result array
  857. *
  858. * @return array Indexed array with message header objects
  859. */
  860. protected function list_search_messages($folder, $page, $slice=0)
  861. {
  862. if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
  863. return array();
  864. }
  865. // gather messages from a multi-folder search
  866. if ($this->search_set->multi) {
  867. $page_size = $this->page_size;
  868. $sort_field = $this->sort_field;
  869. $search_set = $this->search_set;
  870. // prepare paging
  871. $cnt = $search_set->count();
  872. $from = ($page-1) * $page_size;
  873. $to = $from + $page_size;
  874. $slice_length = min($page_size, $cnt - $from);
  875. // fetch resultset headers, sort and slice them
  876. if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) {
  877. $this->sort_field = null;
  878. $this->page_size = 1000; // fetch up to 1000 matching messages per folder
  879. $this->threading = false;
  880. $a_msg_headers = array();
  881. foreach ($search_set->sets as $resultset) {
  882. if (!$resultset->is_empty()) {
  883. $this->search_set = $resultset;
  884. $this->search_threads = $resultset instanceof rcube_result_thread;
  885. $a_headers = $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1);
  886. $a_msg_headers = array_merge($a_msg_headers, $a_headers);
  887. unset($a_headers);
  888. }
  889. }
  890. // sort headers
  891. if (!empty($a_msg_headers)) {
  892. $a_msg_headers = rcube_imap_generic::sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
  893. }
  894. // store (sorted) message index
  895. $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
  896. // only return the requested part of the set
  897. $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
  898. }
  899. else {
  900. if ($this->sort_order != $search_set->get_parameters('ORDER')) {
  901. $search_set->revert();
  902. }
  903. // slice resultset first...
  904. $fetch = array();
  905. foreach (array_slice($search_set->get(), $from, $slice_length) as $msg_id) {
  906. list($uid, $folder) = explode('-', $msg_id, 2);
  907. $fetch[$folder][] = $uid;
  908. }
  909. // ... and fetch the requested set of headers
  910. $a_msg_headers = array();
  911. foreach ($fetch as $folder => $a_index) {
  912. $a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
  913. }
  914. }
  915. if ($slice) {
  916. $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
  917. }
  918. // restore members
  919. $this->sort_field = $sort_field;
  920. $this->page_size = $page_size;
  921. $this->search_set = $search_set;
  922. return $a_msg_headers;
  923. }
  924. // use saved messages from searching
  925. if ($this->threading) {
  926. return $this->list_search_thread_messages($folder, $page, $slice);
  927. }
  928. // search set is threaded, we need a new one
  929. if ($this->search_threads) {
  930. $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  931. }
  932. $index = clone $this->search_set;
  933. $from = ($page-1) * $this->page_size;
  934. $to = $from + $this->page_size;
  935. // return empty array if no messages found
  936. if ($index->is_empty()) {
  937. return array();
  938. }
  939. // quickest method (default sorting)
  940. if (!$this->search_sort_field && !$this->sort_field) {
  941. $got_index = true;
  942. }
  943. // sorted messages, so we can first slice array and then fetch only wanted headers
  944. else if ($this->search_sorted) { // SORT searching result
  945. $got_index = true;
  946. // reset search set if sorting field has been changed
  947. if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
  948. $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  949. $index = clone $this->search_set;
  950. // return empty array if no messages found
  951. if ($index->is_empty()) {
  952. return array();
  953. }
  954. }
  955. }
  956. if ($got_index) {
  957. if ($this->sort_order != $index->get_parameters('ORDER')) {
  958. $index->revert();
  959. }
  960. // get messages uids for one page
  961. $index->slice($from, $to-$from);
  962. if ($slice) {
  963. $index->slice(-$slice, $slice);
  964. }
  965. // fetch headers
  966. $a_index = $index->get();
  967. $a_msg_headers = $this->fetch_headers($folder, $a_index);
  968. return array_values($a_msg_headers);
  969. }
  970. // SEARCH result, need sorting
  971. $cnt = $index->count();
  972. // 300: experimantal value for best result
  973. if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
  974. // use memory less expensive (and quick) method for big result set
  975. $index = clone $this->index('', $this->sort_field, $this->sort_order);
  976. // get messages uids for one page...
  977. $index->slice($from, min($cnt-$from, $this->page_size));
  978. if ($slice) {
  979. $index->slice(-$slice, $slice);
  980. }
  981. // ...and fetch headers
  982. $a_index = $index->get();
  983. $a_msg_headers = $this->fetch_headers($folder, $a_index);
  984. return array_values($a_msg_headers);
  985. }
  986. else {
  987. // for small result set we can fetch all messages headers
  988. $a_index = $index->get();
  989. $a_msg_headers = $this->fetch_headers($folder, $a_index, false);
  990. // return empty array if no messages found
  991. if (!is_array($a_msg_headers) || empty($a_msg_headers)) {
  992. return array();
  993. }
  994. // if not already sorted
  995. $a_msg_headers = rcube_imap_generic::sortHeaders(
  996. $a_msg_headers, $this->sort_field, $this->sort_order);
  997. // only return the requested part of the set
  998. $slice_length = min($this->page_size, $cnt - ($to > $cnt ? $from : $to));
  999. $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
  1000. if ($slice) {
  1001. $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
  1002. }
  1003. return $a_msg_headers;
  1004. }
  1005. }
  1006. /**
  1007. * protected method for listing a set of threaded message headers (search results)
  1008. *
  1009. * @param string $folder Folder name
  1010. * @param int $page Current page to list
  1011. * @param int $slice Number of slice items to extract from result array
  1012. *
  1013. * @return array Indexed array with message header objects
  1014. * @see rcube_imap::list_search_messages()
  1015. */
  1016. protected function list_search_thread_messages($folder, $page, $slice=0)
  1017. {
  1018. // update search_set if previous data was fetched with disabled threading
  1019. if (!$this->search_threads) {
  1020. if ($this->search_set->is_empty()) {
  1021. return array();
  1022. }
  1023. $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
  1024. }
  1025. return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
  1026. }
  1027. /**
  1028. * Fetches messages headers (by UID)
  1029. *
  1030. * @param string $folder Folder name
  1031. * @param array $msgs Message UIDs
  1032. * @param bool $sort Enables result sorting by $msgs
  1033. * @param bool $force Disables cache use
  1034. *
  1035. * @return array Messages headers indexed by UID
  1036. */
  1037. function fetch_headers($folder, $msgs, $sort = true, $force = false)
  1038. {
  1039. if (empty($msgs)) {
  1040. return array();
  1041. }
  1042. if (!$force && ($mcache = $this->get_mcache_engine())) {
  1043. $headers = $mcache->get_messages($folder, $msgs);
  1044. }
  1045. else if (!$this->check_connection()) {
  1046. return array();
  1047. }
  1048. else {
  1049. // fetch reqested headers from server
  1050. $headers = $this->conn->fetchHeaders(
  1051. $folder, $msgs, true, false, $this->get_fetch_headers());
  1052. }
  1053. if (empty($headers)) {
  1054. return array();
  1055. }
  1056. foreach ($headers as $h) {
  1057. $h->folder = $folder;
  1058. $a_msg_headers[$h->uid] = $h;
  1059. }
  1060. if ($sort) {
  1061. // use this class for message sorting
  1062. $sorter = new rcube_message_header_sorter();
  1063. $sorter->set_index($msgs);
  1064. $sorter->sort_headers($a_msg_headers);
  1065. }
  1066. return $a_msg_headers;
  1067. }
  1068. /**
  1069. * Returns current status of a folder (compared to the last time use)
  1070. *
  1071. * We compare the maximum UID to determine the number of
  1072. * new messages because the RECENT flag is not reliable.
  1073. *
  1074. * @param string $folder Folder name
  1075. * @param array $diff Difference data
  1076. *
  1077. * @return int Folder status
  1078. */
  1079. public function folder_status($folder = null, &$diff = array())
  1080. {
  1081. if (!strlen($folder)) {
  1082. $folder = $this->folder;
  1083. }
  1084. $old = $this->get_folder_stats($folder);
  1085. // refresh message count -> will update
  1086. $this->countmessages($folder, 'ALL', true, true, true);
  1087. $result = 0;
  1088. if (empty($old)) {
  1089. return $result;
  1090. }
  1091. $new = $this->get_folder_stats($folder);
  1092. // got new messages
  1093. if ($new['maxuid'] > $old['maxuid']) {
  1094. $result += 1;
  1095. // get new message UIDs range, that can be used for example
  1096. // to get the data of these messages
  1097. $diff['new'] = ($old['maxuid'] + 1 < $new['maxuid'] ? ($old['maxuid']+1).':' : '') . $new['maxuid'];
  1098. }
  1099. // some messages has been deleted
  1100. if ($new['cnt'] < $old['cnt']) {
  1101. $result += 2;
  1102. }
  1103. // @TODO: optional checking for messages flags changes (?)
  1104. // @TODO: UIDVALIDITY checking
  1105. return $result;
  1106. }
  1107. /**
  1108. * Stores folder statistic data in session
  1109. * @TODO: move to separate DB table (cache?)
  1110. *
  1111. * @param string $folder Folder name
  1112. * @param string $name Data name
  1113. * @param mixed $data Data value
  1114. */
  1115. protected function set_folder_stats($folder, $name, $data)
  1116. {
  1117. $_SESSION['folders'][$folder][$name] = $data;
  1118. }
  1119. /**
  1120. * Gets folder statistic data
  1121. *
  1122. * @param string $folder Folder name
  1123. *
  1124. * @return array Stats data
  1125. */
  1126. protected function get_folder_stats($folder)
  1127. {
  1128. if ($_SESSION['folders'][$folder]) {
  1129. return (array) $_SESSION['folders'][$folder];
  1130. }
  1131. return array();
  1132. }
  1133. /**
  1134. * Return sorted list of message UIDs
  1135. *
  1136. * @param string $folder Folder to get index from
  1137. * @param string $sort_field Sort column
  1138. * @param string $sort_order Sort order [ASC, DESC]
  1139. * @param bool $no_threads Get not threaded index
  1140. * @param bool $no_search Get index not limited to search result (optionally)
  1141. *
  1142. * @return rcube_result_index|rcube_result_thread List of messages (UIDs)
  1143. */
  1144. public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
  1145. $no_threads = false, $no_search = false
  1146. ) {
  1147. if (!$no_threads && $this->threading) {
  1148. return $this->thread_index($folder, $sort_field, $sort_order);
  1149. }
  1150. $this->set_sort_order($sort_field, $sort_order);
  1151. if (!strlen($folder)) {
  1152. $folder = $this->folder;
  1153. }
  1154. // we have a saved search result, get index from there
  1155. if ($this->search_string) {
  1156. if ($this->search_set->is_empty()) {
  1157. return new rcube_result_index($folder, '* SORT');
  1158. }
  1159. if ($this->search_set instanceof rcube_result_multifolder) {
  1160. $index = $this->search_set;
  1161. $index->folder = $folder;
  1162. // TODO: handle changed sorting
  1163. }
  1164. // search result is an index with the same sorting?
  1165. else if (($this->search_set instanceof rcube_result_index)
  1166. && ((!$this->sort_field && !$this->search_sorted) ||
  1167. ($this->search_sorted && $this->search_sort_field == $this->sort_field))
  1168. ) {
  1169. $index = $this->search_set;
  1170. }
  1171. // $no_search is enabled when we are not interested in
  1172. // fetching index for search result, e.g. to sort
  1173. // threaded search result we can use full mailbox index.
  1174. // This makes possible to use index from cache
  1175. else if (!$no_search) {
  1176. if (!$this->sort_field) {
  1177. // No sorting needed, just build index from the search result
  1178. // @TODO: do we need to sort by UID here?
  1179. $search = $this->search_set->get_compressed();
  1180. $index = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
  1181. }
  1182. else {
  1183. $index = $this->index_direct($folder, $this->search_charset,
  1184. $this->sort_field, $this->search_set);
  1185. }
  1186. }
  1187. if (isset($index)) {
  1188. if ($this->sort_order != $index->get_parameters('ORDER')) {
  1189. $index->revert();
  1190. }
  1191. return $index;
  1192. }
  1193. }
  1194. // check local cache
  1195. if ($mcache = $this->get_mcache_engine()) {
  1196. return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
  1197. }
  1198. // fetch from IMAP server
  1199. return $this->index_direct($folder, $this->sort_field, $this->sort_order);
  1200. }
  1201. /**
  1202. * Return sorted list of message UIDs ignoring current search settings.
  1203. * Doesn't uses cache by default.
  1204. *
  1205. * @param string $folder Folder to get index from
  1206. * @param string $sort_field Sort column
  1207. * @param string $sort_order Sort order [ASC, DESC]
  1208. * @param rcube_result_* $search Optional messages set to limit the result
  1209. *
  1210. * @return rcube_result_index Sorted list of message UIDs
  1211. */
  1212. public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
  1213. {
  1214. if (!empty($search)) {
  1215. $search = $search->get_compressed();
  1216. }
  1217. // use message index sort as default sorting
  1218. if (!$sort_field) {
  1219. // use search result from count() if possible
  1220. if (empty($search) && $this->options['skip_deleted']
  1221. && !empty($this->icache['undeleted_idx'])
  1222. && $this->icache['undeleted_idx']->get_parameters('ALL') !== null
  1223. && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
  1224. ) {
  1225. $index = $this->icache['undeleted_idx'];
  1226. }
  1227. else if (!$this->check_connection()) {
  1228. return new rcube_result_index();
  1229. }
  1230. else {
  1231. $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
  1232. if ($search) {
  1233. $query = trim($query . ' UID ' . $search);
  1234. }
  1235. $index = $this->conn->search($folder, $query, true);
  1236. }
  1237. }
  1238. else if (!$this->check_connection()) {
  1239. return new rcube_result_index();
  1240. }
  1241. // fetch complete message index
  1242. else {
  1243. if ($this->get_capability('SORT')) {
  1244. $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
  1245. if ($search) {
  1246. $query = trim($query . ' UID ' . $search);
  1247. }
  1248. $index = $this->conn->sort($folder, $sort_field, $query, true);
  1249. }
  1250. if (empty($index) || $index->is_error()) {
  1251. $index = $this->conn->index($folder, $search ? $search : "1:*",
  1252. $sort_field, $this->options['skip_deleted'],
  1253. $search ? true : false, true);
  1254. }
  1255. }
  1256. if ($sort_order != $index->get_parameters('ORDER')) {
  1257. $index->revert();
  1258. }
  1259. return $index;
  1260. }
  1261. /**
  1262. * Return index of threaded message UIDs
  1263. *
  1264. * @param string $folder Folder to get index from
  1265. * @param string $sort_field Sort column
  1266. * @param string $sort_order Sort order [ASC, DESC]
  1267. *
  1268. * @return rcube_result_thread Message UIDs
  1269. */
  1270. public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL)
  1271. {
  1272. if (!strlen($folder)) {
  1273. $folder = $this->folder;
  1274. }
  1275. // we have a saved search result, get index from there
  1276. if ($this->search_string && $this->search_threads && $folder == $this->folder) {
  1277. $threads = $this->search_set;
  1278. }
  1279. else {
  1280. // get all threads (default sort order)
  1281. $threads = $this->threads($folder);
  1282. }
  1283. $this->set_sort_order($sort_field, $sort_order);
  1284. $this->sort_threads($threads);
  1285. return $threads;
  1286. }
  1287. /**
  1288. * Sort threaded result, using THREAD=REFS method if available.
  1289. * If not, use any method and re-sort the result in THREAD=REFS way.
  1290. *
  1291. * @param rcube_result_thread $threads Threads result set
  1292. */
  1293. protected function sort_threads($threads)
  1294. {
  1295. if ($threads->is_empty()) {
  1296. return;
  1297. }
  1298. // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
  1299. // THREAD=REFERENCES: sorting by sent date of root message
  1300. // THREAD=REFS: sorting by the most recent date in each thread
  1301. if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
  1302. $sortby = $this->sort_field ? $this->sort_field : 'date';
  1303. $index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
  1304. if (!$index->is_empty()) {
  1305. $threads->sort($index);
  1306. }
  1307. }
  1308. else if ($this->sort_order != $threads->get_parameters('ORDER')) {
  1309. $threads->revert();
  1310. }
  1311. }
  1312. /**
  1313. * Invoke search request to IMAP server
  1314. *
  1315. * @param string $folder Folder name to search in
  1316. * @param string $search Search criteria
  1317. * @param string $charset Search charset
  1318. * @param string $sort_field Header field to sort by
  1319. *
  1320. * @return rcube_result_index Search result object
  1321. * @todo: Search criteria should be provided in non-IMAP format, eg. array
  1322. */
  1323. public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
  1324. {
  1325. if (!$search) {
  1326. $search = 'ALL';
  1327. }
  1328. if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
  1329. $folder = $this->folder;
  1330. }
  1331. $plugin = $this->plugins->exec_hook('imap_search_before', array(
  1332. 'folder' => $folder,
  1333. 'search' => $search,
  1334. 'charset' => $charset,
  1335. 'sort_field' => $sort_field,
  1336. 'threading' => $this->threading,
  1337. ));
  1338. $folder = $plugin['folder'];
  1339. $search = $plugin['search'];
  1340. $charset = $plugin['charset'];
  1341. $sort_field = $plugin['sort_field'];
  1342. $results = $plugin['result'];
  1343. // multi-folder search
  1344. if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
  1345. // connect IMAP to have all the required classes and settings loaded
  1346. $this->check_connection();
  1347. // disable threading
  1348. $this->threading = false;
  1349. $searcher = new rcube_imap_search($this->options, $this->conn);
  1350. // set limit to not exceed the client's request timeout
  1351. $searcher->set_timelimit(60);
  1352. // continue existing incomplete search
  1353. if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) {
  1354. $searcher->set_results($this->search_set);
  1355. }
  1356. // execute the search
  1357. $results = $searcher->exec(
  1358. $folder,
  1359. $search,
  1360. $charset ? $charset : $this->default_charset,
  1361. $sort_field && $this->get_capability('SORT') ? $sort_field : null,
  1362. $this->threading
  1363. );
  1364. }
  1365. else if (!$results) {
  1366. $folder = is_array($folder) ? $folder[0] : $folder;
  1367. $search = is_array($search) ? $search[$folder] : $search;
  1368. $results = $this->search_index($folder, $search, $charset, $sort_field);
  1369. }
  1370. $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
  1371. $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
  1372. return $results;
  1373. }
  1374. /**
  1375. * Direct (real and simple) SEARCH request (without result sorting and caching).
  1376. *
  1377. * @param string $mailbox Mailbox name to search in
  1378. * @param string $str Search string
  1379. *
  1380. * @return rcube_result_index Search result (UIDs)
  1381. */
  1382. public function search_once($folder = null, $str = 'ALL')
  1383. {
  1384. if (!$this->check_connection()) {
  1385. return new rcube_result_index();
  1386. }
  1387. if (!$str) {
  1388. $str = 'ALL';
  1389. }
  1390. // multi-folder search
  1391. if (is_array($folder) && count($folder) > 1) {
  1392. $searcher = new rcube_imap_search($this->options, $this->conn);
  1393. $index = $searcher->exec($folder, $str, $this->default_charset);
  1394. }
  1395. else {
  1396. $folder = is_array($folder) ? $folder[0] : $folder;
  1397. if (!strlen($folder)) {
  1398. $folder = $this->folder;
  1399. }
  1400. $index = $this->conn->search($folder, $str, true);
  1401. }
  1402. return $index;
  1403. }
  1404. /**
  1405. * protected search method
  1406. *
  1407. * @param string $folder Folder name
  1408. * @param string $criteria Search criteria
  1409. * @param string $charset Charset
  1410. * @param string $sort_field Sorting field
  1411. *
  1412. * @return rcube_result_index|rcube_result_thread Search results (UIDs)
  1413. * @see rcube_imap::search()
  1414. */
  1415. protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
  1416. {
  1417. if (!$this->check_connection()) {
  1418. if ($this->threading) {
  1419. return new rcube_result_thread();
  1420. }
  1421. else {
  1422. return new rcube_result_index();
  1423. }
  1424. }
  1425. if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
  1426. $criteria = 'UNDELETED '.$criteria;
  1427. }
  1428. // unset CHARSET if criteria string is ASCII, this way
  1429. // SEARCH won't be re-sent after "unsupported charset" response
  1430. if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
  1431. $charset = 'US-ASCII';
  1432. }
  1433. if ($this->threading) {
  1434. $threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
  1435. // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
  1436. // but I've seen that Courier doesn't support UTF-8)
  1437. if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
  1438. $threads = $this->conn->thread($folder, $this->threading,
  1439. self::convert_criteria($criteria, $charset), true, 'US-ASCII');
  1440. }
  1441. return $threads;
  1442. }
  1443. if ($sort_field && $this->get_capability('SORT')) {
  1444. $charset = $charset ? $charset : $this->default_charset;
  1445. $messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
  1446. // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
  1447. // but I've seen Courier with disabled UTF-8 support)
  1448. if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
  1449. $messages = $this->conn->sort($folder, $sort_field,
  1450. self::convert_criteria($criteria, $charset), true, 'US-ASCII');
  1451. }
  1452. if (!$messages->is_error()) {
  1453. $this->search_sorted = true;
  1454. return $messages;
  1455. }
  1456. }
  1457. $messages = $this->conn->search($folder,
  1458. ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
  1459. // Error, try with US-ASCII (some servers may support only US-ASCII)
  1460. if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
  1461. $messages = $this->conn->search($folder,
  1462. self::convert_criteria($criteria, $charset), true);
  1463. }
  1464. $this->search_sorted = false;
  1465. return $messages;
  1466. }
  1467. /**
  1468. * Converts charset of search criteria string
  1469. *
  1470. * @param string $str Search string
  1471. * @param string $charset Original charset
  1472. * @param string $dest_charset Destination charset (default US-ASCII)
  1473. *
  1474. * @return string Search string
  1475. */
  1476. public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
  1477. {
  1478. // convert strings to US_ASCII
  1479. if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
  1480. $last = 0; $res = '';
  1481. foreach ($matches[1] as $m) {
  1482. $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
  1483. $string = substr($str, $string_offset - 1, $m[0]);
  1484. $string = rcube_charset::convert($string, $charset, $dest_charset);
  1485. if ($string === false || !strlen($string)) {
  1486. continue;
  1487. }
  1488. $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
  1489. $last = $m[0] + $string_offset - 1;
  1490. }
  1491. if ($last < strlen($str)) {
  1492. $res .= substr($str, $last, strlen($str)-$last);
  1493. }
  1494. }
  1495. // strings for conversion not found
  1496. else {
  1497. $res = $str;
  1498. }
  1499. return $res;
  1500. }
  1501. /**
  1502. * Refresh saved search set
  1503. *
  1504. * @return array Current search set
  1505. */
  1506. public function refresh_search()
  1507. {
  1508. if (!empty($this->search_string)) {
  1509. $this->search(
  1510. is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
  1511. $this->search_string,
  1512. $this->search_charset,
  1513. $this->search_sort_field
  1514. );
  1515. }
  1516. return $this->get_search_set();
  1517. }
  1518. /**
  1519. * Flag certain result subsets as 'incomplete'.
  1520. * For subsequent refresh_search() calls to only refresh the updated parts.
  1521. */
  1522. protected function set_search_dirty($folder)
  1523. {
  1524. if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
  1525. if ($subset = $this->search_set->get_set($folder)) {
  1526. $subset->incomplete = $this->search_set->incomplete = true;
  1527. }
  1528. }
  1529. }
  1530. /**
  1531. * Return message headers object of a specific message
  1532. *
  1533. * @param int $id Message UID
  1534. * @param string $folder Folder to read from
  1535. * @param bool $force True to skip cache
  1536. *
  1537. * @return rcube_message_header Message headers
  1538. */
  1539. public function get_message_headers($uid, $folder = null, $force = false)
  1540. {
  1541. // decode combined UID-folder identifier
  1542. if (preg_match('/^\d+-.+/', $uid)) {
  1543. list($uid, $folder) = explode('-', $uid, 2);
  1544. }
  1545. if (!strlen($folder)) {
  1546. $folder = $this->folder;
  1547. }
  1548. // get cached headers
  1549. if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
  1550. $headers = $mcache->get_message($folder, $uid);
  1551. }
  1552. else if (!$this->check_connection()) {
  1553. $headers = false;
  1554. }
  1555. else {
  1556. $headers = $this->conn->fetchHeader(
  1557. $folder, $uid, true, true, $this->get_fetch_headers());
  1558. if (is_object($headers))
  1559. $headers->folder = $folder;
  1560. }
  1561. return $headers;
  1562. }
  1563. /**
  1564. * Fetch message headers and body structure from the IMAP server and build
  1565. * an object structure.
  1566. *
  1567. * @param int $uid Message UID to fetch
  1568. * @param string $folder Folder to read from
  1569. *
  1570. * @return object rcube_message_header Message data
  1571. */
  1572. public function get_message($uid, $folder = null)
  1573. {
  1574. if (!strlen($folder)) {
  1575. $folder = $this->folder;
  1576. }
  1577. // decode combined UID-folder identifier
  1578. if (preg_match('/^\d+-.+/', $uid)) {
  1579. list($uid, $folder) = explode('-', $uid, 2);
  1580. }
  1581. // Check internal cache
  1582. if (!empty($this->icache['message'])) {
  1583. if (($headers = $this->icache['message']) && $headers->uid == $uid) {
  1584. return $headers;
  1585. }
  1586. }
  1587. $headers = $this->get_message_headers($uid, $folder);
  1588. // message doesn't exist?
  1589. if (empty($headers)) {
  1590. return null;
  1591. }
  1592. // structure might be cached
  1593. if (!empty($headers->structure)) {
  1594. return $headers;
  1595. }
  1596. $this->msg_uid = $uid;
  1597. if (!$this->check_connection()) {
  1598. return $headers;
  1599. }
  1600. if (empty($headers->bodystructure)) {
  1601. $headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
  1602. }
  1603. $structure = $headers->bodystructure;
  1604. if (empty($structure)) {
  1605. return $headers;
  1606. }
  1607. // set message charset from message headers
  1608. if ($headers->charset) {
  1609. $this->struct_charset = $headers->charset;
  1610. }
  1611. else {
  1612. $this->struct_charset = $this->structure_charset($structure);
  1613. }
  1614. $headers->ctype = @strtolower($headers->ctype);
  1615. // Here we can recognize malformed BODYSTRUCTURE and
  1616. // 1. [@TODO] parse the message in other way to create our own message structure
  1617. // 2. or just show the raw message body.
  1618. // Example of structure for malformed MIME message:
  1619. // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
  1620. if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
  1621. && strtolower($structure[0].'/'.$structure[1]) == 'text/plain'
  1622. ) {
  1623. // A special known case "Content-type: text" (#1488968)
  1624. if ($headers->ctype == 'text') {
  1625. $structure[1] = 'plain';
  1626. $headers->ctype = 'text/plain';
  1627. }
  1628. // we can handle single-part messages, by simple fix in structure (#1486898)
  1629. else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
  1630. $structure[0] = $m[1];
  1631. $structure[1] = $m[2];
  1632. }
  1633. else {
  1634. // Try to parse the message using rcube_mime_decode.
  1635. // We need a better solution, it parses message
  1636. // in memory, which wouldn't work for very big messages,
  1637. // (it uses up to 10x more memory than the message size)
  1638. // it's also buggy and not actively developed
  1639. if ($headers->size && rcube_utils::mem_check($headers->size * 10)) {
  1640. $raw_msg = $this->get_raw_body($uid);
  1641. $struct = rcube_mime::parse_message($raw_msg);
  1642. }
  1643. else {
  1644. return $headers;
  1645. }
  1646. }
  1647. }
  1648. if (empty($struct)) {
  1649. $struct = $this->structure_part($structure, 0, '', $headers);
  1650. }
  1651. // some workarounds on simple messages...
  1652. if (empty($struct->parts)) {
  1653. // ...don't trust given content-type
  1654. if (!empty($headers->ctype)) {
  1655. $struct->mime_id = '1';
  1656. $struct->mimetype = strtolower($headers->ctype);
  1657. list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
  1658. }
  1659. // ...and charset (there's a case described in #1488968 where invalid content-type
  1660. // results in invalid charset in BODYSTRUCTURE)
  1661. if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) {
  1662. $struct->charset = $headers->charset;
  1663. $struct->ctype_parameters['charset'] = $headers->charset;
  1664. }
  1665. }
  1666. $headers->structure = $struct;
  1667. return $this->icache['message'] = $headers;
  1668. }
  1669. /**
  1670. * Build message part object
  1671. *
  1672. * @param array $part
  1673. * @param int $count
  1674. * @param string $parent
  1675. */
  1676. protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null)
  1677. {
  1678. $struct = new rcube_message_part;
  1679. $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
  1680. // multipart
  1681. if (is_array($part[0])) {
  1682. $struct->ctype_primary = 'multipart';
  1683. /* RFC3501: BODYSTRUCTURE fields of multipart part
  1684. part1 array
  1685. part2 array
  1686. part3 array
  1687. ....
  1688. 1. subtype
  1689. 2. parameters (optional)
  1690. 3. description (optional)
  1691. 4. language (optional)
  1692. 5. location (optional)
  1693. */
  1694. // find first non-array entry
  1695. for ($i=1; $i<count($part); $i++) {
  1696. if (!is_array($part[$i])) {
  1697. $struct->ctype_secondary = strtolower($part[$i]);
  1698. // read content type parameters
  1699. if (is_array($part[$i+1])) {
  1700. $struct->ctype_parameters = array();
  1701. for ($j=0; $j<count($part[$i+1]); $j+=2) {
  1702. $param = strtolower($part[$i+1][$j]);
  1703. $struct->ctype_parameters[$param] = $part[$i+1][$j+1];
  1704. }
  1705. }
  1706. break;
  1707. }
  1708. }
  1709. $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
  1710. // build parts list for headers pre-fetching
  1711. for ($i=0; $i<count($part); $i++) {
  1712. if (!is_array($part[$i])) {
  1713. break;
  1714. }
  1715. // fetch message headers if message/rfc822
  1716. // or named part (could contain Content-Location header)
  1717. if (!is_array($part[$i][0])) {
  1718. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1719. if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
  1720. $mime_part_headers[] = $tmp_part_id;
  1721. }
  1722. else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
  1723. $mime_part_headers[] = $tmp_part_id;
  1724. }
  1725. }
  1726. }
  1727. // pre-fetch headers of all parts (in one command for better performance)
  1728. // @TODO: we could do this before _structure_part() call, to fetch
  1729. // headers for parts on all levels
  1730. if ($mime_part_headers) {
  1731. $mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder,
  1732. $this->msg_uid, $mime_part_headers);
  1733. }
  1734. $struct->parts = array();
  1735. for ($i=0, $count=0; $i<count($part); $i++) {
  1736. if (!is_array($part[$i])) {
  1737. break;
  1738. }
  1739. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1740. $struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id,
  1741. $mime_part_headers[$tmp_part_id]);
  1742. }
  1743. return $struct;
  1744. }
  1745. /* RFC3501: BODYSTRUCTURE fields of non-multipart part
  1746. 0. type
  1747. 1. subtype
  1748. 2. parameters
  1749. 3. id
  1750. 4. description
  1751. 5. encoding
  1752. 6. size
  1753. -- text
  1754. 7. lines
  1755. -- message/rfc822
  1756. 7. envelope structure
  1757. 8. body structure
  1758. 9. lines
  1759. --
  1760. x. md5 (optional)
  1761. x. disposition (optional)
  1762. x. language (optional)
  1763. x. location (optional)
  1764. */
  1765. // regular part
  1766. $struct->ctype_primary = strtolower($part[0]);
  1767. $struct->ctype_secondary = strtolower($part[1]);
  1768. $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
  1769. // read content type parameters
  1770. if (is_array($part[2])) {
  1771. $struct->ctype_parameters = array();
  1772. for ($i=0; $i<count($part[2]); $i+=2) {
  1773. $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
  1774. }
  1775. if (isset($struct->ctype_parameters['charset'])) {
  1776. $struct->charset = $struct->ctype_parameters['charset'];
  1777. }
  1778. }
  1779. // #1487700: workaround for lack of charset in malformed structure
  1780. if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
  1781. $struct->charset = $mime_headers->charset;
  1782. }
  1783. // read content encoding
  1784. if (!empty($part[5])) {
  1785. $struct->encoding = strtolower($part[5]);
  1786. $struct->headers['content-transfer-encoding'] = $struct->encoding;
  1787. }
  1788. // get part size
  1789. if (!empty($part[6])) {
  1790. $struct->size = intval($part[6]);
  1791. }
  1792. // read part disposition
  1793. $di = 8;
  1794. if ($struct->ctype_primary == 'text') {
  1795. $di += 1;
  1796. }
  1797. else if ($struct->mimetype == 'message/rfc822') {
  1798. $di += 3;
  1799. }
  1800. if (is_array($part[$di]) && count($part[$di]) == 2) {
  1801. $struct->disposition = strtolower($part[$di][0]);
  1802. if (is_array($part[$di][1])) {
  1803. for ($n=0; $n<count($part[$di][1]); $n+=2) {
  1804. $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
  1805. }
  1806. }
  1807. }
  1808. // get message/rfc822's child-parts
  1809. if (is_array($part[8]) && $di != 8) {
  1810. $struct->parts = array();
  1811. for ($i=0, $count=0; $i<count($part[8]); $i++) {
  1812. if (!is_array($part[8][$i])) {
  1813. break;
  1814. }
  1815. $struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id);
  1816. }
  1817. }
  1818. // get part ID
  1819. if (!empty($part[3])) {
  1820. $struct->content_id = $part[3];
  1821. $struct->headers['content-id'] = $part[3];
  1822. if (empty($struct->disposition)) {
  1823. $struct->disposition = 'inline';
  1824. }
  1825. }
  1826. // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
  1827. if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
  1828. if (empty($mime_headers)) {
  1829. $mime_headers = $this->conn->fetchPartHeader(
  1830. $this->folder, $this->msg_uid, true, $struct->mime_id);
  1831. }
  1832. if (is_string($mime_headers)) {
  1833. $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
  1834. }
  1835. else if (is_object($mime_headers)) {
  1836. $struct->headers = get_object_vars($mime_headers) + $struct->headers;
  1837. }
  1838. // get real content-type of message/rfc822
  1839. if ($struct->mimetype == 'message/rfc822') {
  1840. // single-part
  1841. if (!is_array($part[8][0])) {
  1842. $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
  1843. }
  1844. // multi-part
  1845. else {
  1846. for ($n=0; $n<count($part[8]); $n++) {
  1847. if (!is_array($part[8][$n])) {
  1848. break;
  1849. }
  1850. }
  1851. $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
  1852. }
  1853. }
  1854. if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
  1855. if (is_array($part[8]) && $di != 8) {
  1856. $struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id);
  1857. }
  1858. }
  1859. }
  1860. // normalize filename property
  1861. $this->set_part_filename($struct, $mime_headers);
  1862. return $struct;
  1863. }
  1864. /**
  1865. * Set attachment filename from message part structure
  1866. *
  1867. * @param rcube_message_part $part Part object
  1868. * @param string $headers Part's raw headers
  1869. */
  1870. protected function set_part_filename(&$part, $headers = null)
  1871. {
  1872. if (!empty($part->d_parameters['filename'])) {
  1873. $filename_mime = $part->d_parameters['filename'];
  1874. }
  1875. else if (!empty($part->d_parameters['filename*'])) {
  1876. $filename_encoded = $part->d_parameters['filename*'];
  1877. }
  1878. else if (!empty($part->ctype_parameters['name*'])) {
  1879. $filename_encoded = $part->ctype_parameters['name*'];
  1880. }
  1881. // RFC2231 value continuations
  1882. // TODO: this should be rewrited to support RFC2231 4.1 combinations
  1883. else if (!empty($part->d_parameters['filename*0'])) {
  1884. $i = 0;
  1885. while (isset($part->d_parameters['filename*'.$i])) {
  1886. $filename_mime .= $part->d_parameters['filename*'.$i];
  1887. $i++;
  1888. }
  1889. // some servers (eg. dovecot-1.x) have no support for parameter value continuations
  1890. // we must fetch and parse headers "manually"
  1891. if ($i<2) {
  1892. if (!$headers) {
  1893. $headers = $this->conn->fetchPartHeader(
  1894. $this->folder, $this->msg_uid, true, $part->mime_id);
  1895. }
  1896. $filename_mime = '';
  1897. $i = 0;
  1898. while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1899. $filename_mime .= $matches[1];
  1900. $i++;
  1901. }
  1902. }
  1903. }
  1904. else if (!empty($part->d_parameters['filename*0*'])) {
  1905. $i = 0;
  1906. while (isset($part->d_parameters['filename*'.$i.'*'])) {
  1907. $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
  1908. $i++;
  1909. }
  1910. if ($i<2) {
  1911. if (!$headers) {
  1912. $headers = $this->conn->fetchPartHeader(
  1913. $this->folder, $this->msg_uid, true, $part->mime_id);
  1914. }
  1915. $filename_encoded = '';
  1916. $i = 0; $matches = array();
  1917. while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1918. $filename_encoded .= $matches[1];
  1919. $i++;
  1920. }
  1921. }
  1922. }
  1923. else if (!empty($part->ctype_parameters['name*0'])) {
  1924. $i = 0;
  1925. while (isset($part->ctype_parameters['name*'.$i])) {
  1926. $filename_mime .= $part->ctype_parameters['name*'.$i];
  1927. $i++;
  1928. }
  1929. if ($i<2) {
  1930. if (!$headers) {
  1931. $headers = $this->conn->fetchPartHeader(
  1932. $this->folder, $this->msg_uid, true, $part->mime_id);
  1933. }
  1934. $filename_mime = '';
  1935. $i = 0; $matches = array();
  1936. while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1937. $filename_mime .= $matches[1];
  1938. $i++;
  1939. }
  1940. }
  1941. }
  1942. else if (!empty($part->ctype_parameters['name*0*'])) {
  1943. $i = 0;
  1944. while (isset($part->ctype_parameters['name*'.$i.'*'])) {
  1945. $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
  1946. $i++;
  1947. }
  1948. if ($i<2) {
  1949. if (!$headers) {
  1950. $headers = $this->conn->fetchPartHeader(
  1951. $this->folder, $this->msg_uid, true, $part->mime_id);
  1952. }
  1953. $filename_encoded = '';
  1954. $i = 0; $matches = array();
  1955. while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1956. $filename_encoded .= $matches[1];
  1957. $i++;
  1958. }
  1959. }
  1960. }
  1961. // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
  1962. else if (!empty($part->ctype_parameters['name'])) {
  1963. $filename_mime = $part->ctype_parameters['name'];
  1964. }
  1965. // Content-Disposition
  1966. else if (!empty($part->headers['content-description'])) {
  1967. $filename_mime = $part->headers['content-description'];
  1968. }
  1969. else {
  1970. return;
  1971. }
  1972. // decode filename
  1973. if (!empty($filename_mime)) {
  1974. if (!empty($part->charset)) {
  1975. $charset = $part->charset;
  1976. }
  1977. else if (!empty($this->struct_charset)) {
  1978. $charset = $this->struct_charset;
  1979. }
  1980. else {
  1981. $charset = rcube_charset::detect($filename_mime, $this->default_charset);
  1982. }
  1983. $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
  1984. }
  1985. else if (!empty($filename_encoded)) {
  1986. // decode filename according to RFC 2231, Section 4
  1987. if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
  1988. $filename_charset = $fmatches[1];
  1989. $filename_encoded = $fmatches[2];
  1990. }
  1991. $part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset);
  1992. }
  1993. }
  1994. /**
  1995. * Get charset name from message structure (first part)
  1996. *
  1997. * @param array $structure Message structure
  1998. *
  1999. * @return string Charset name
  2000. */
  2001. protected function structure_charset($structure)
  2002. {
  2003. while (is_array($structure)) {
  2004. if (is_array($structure[2]) && $structure[2][0] == 'charset') {
  2005. return $structure[2][1];
  2006. }
  2007. $structure = $structure[0];
  2008. }
  2009. }
  2010. /**
  2011. * Fetch message body of a specific message from the server
  2012. *
  2013. * @param int Message UID
  2014. * @param string Part number
  2015. * @param rcube_message_part Part object created by get_structure()
  2016. * @param mixed True to print part, resource to write part contents in
  2017. * @param resource File pointer to save the message part
  2018. * @param boolean Disables charset conversion
  2019. * @param int Only read this number of bytes
  2020. * @param boolean Enables formatting of text/* parts bodies
  2021. *
  2022. * @return string Message/part body if not printed
  2023. */
  2024. public function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null,
  2025. $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
  2026. {
  2027. if (!$this->check_connection()) {
  2028. return null;
  2029. }
  2030. // get part data if not provided
  2031. if (!is_object($o_part)) {
  2032. $structure = $this->conn->getStructure($this->folder, $uid, true);
  2033. $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
  2034. $o_part = new rcube_message_part;
  2035. $o_part->ctype_primary = $part_data['type'];
  2036. $o_part->encoding = $part_data['encoding'];
  2037. $o_part->charset = $part_data['charset'];
  2038. $o_part->size = $part_data['size'];
  2039. }
  2040. if ($o_part && $o_part->size) {
  2041. $formatted = $formatted && $o_part->ctype_primary == 'text';
  2042. $body = $this->conn->handlePartBody($this->folder, $uid, true,
  2043. $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
  2044. }
  2045. if ($fp || $print) {
  2046. return true;
  2047. }
  2048. // convert charset (if text or message part)
  2049. if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
  2050. // Remove NULL characters if any (#1486189)
  2051. if ($formatted && strpos($body, "\x00") !== false) {
  2052. $body = str_replace("\x00", '', $body);
  2053. }
  2054. if (!$skip_charset_conv) {
  2055. if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
  2056. // try to extract charset information from HTML meta tag (#1488125)
  2057. if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
  2058. $o_part->charset = strtoupper($m[1]);
  2059. }
  2060. else {
  2061. $o_part->charset = $this->default_charset;
  2062. }
  2063. }
  2064. $body = rcube_charset::convert($body, $o_part->charset);
  2065. }
  2066. }
  2067. return $body;
  2068. }
  2069. /**
  2070. * Returns the whole message source as string (or saves to a file)
  2071. *
  2072. * @param int $uid Message UID
  2073. * @param resource $fp File pointer to save the message
  2074. * @param string $part Optional message part ID
  2075. *
  2076. * @return string Message source string
  2077. */
  2078. public function get_raw_body($uid, $fp=null, $part = null)
  2079. {
  2080. if (!$this->check_connection()) {
  2081. return null;
  2082. }
  2083. return $this->conn->handlePartBody($this->folder, $uid,
  2084. true, $part, null, false, $fp);
  2085. }
  2086. /**
  2087. * Returns the message headers as string
  2088. *
  2089. * @param int $uid Message UID
  2090. * @param string $part Optional message part ID
  2091. *
  2092. * @return string Message headers string
  2093. */
  2094. public function get_raw_headers($uid, $part = null)
  2095. {
  2096. if (!$this->check_connection()) {
  2097. return null;
  2098. }
  2099. return $this->conn->fetchPartHeader($this->folder, $uid, true, $part);
  2100. }
  2101. /**
  2102. * Sends the whole message source to stdout
  2103. *
  2104. * @param int $uid Message UID
  2105. * @param bool $formatted Enables line-ending formatting
  2106. */
  2107. public function print_raw_body($uid, $formatted = true)
  2108. {
  2109. if (!$this->check_connection()) {
  2110. return;
  2111. }
  2112. $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
  2113. }
  2114. /**
  2115. * Set message flag to one or several messages
  2116. *
  2117. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2118. * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
  2119. * @param string $folder Folder name
  2120. * @param boolean $skip_cache True to skip message cache clean up
  2121. *
  2122. * @return boolean Operation status
  2123. */
  2124. public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
  2125. {
  2126. if (!strlen($folder)) {
  2127. $folder = $this->folder;
  2128. }
  2129. if (!$this->check_connection()) {
  2130. return false;
  2131. }
  2132. $flag = strtoupper($flag);
  2133. list($uids, $all_mode) = $this->parse_uids($uids);
  2134. if (strpos($flag, 'UN') === 0) {
  2135. $result = $this->conn->unflag($folder, $uids, substr($flag, 2));
  2136. }
  2137. else {
  2138. $result = $this->conn->flag($folder, $uids, $flag);
  2139. }
  2140. if ($result && !$skip_cache) {
  2141. // reload message headers if cached
  2142. // update flags instead removing from cache
  2143. if ($mcache = $this->get_mcache_engine()) {
  2144. $status = strpos($flag, 'UN') !== 0;
  2145. $mflag = preg_replace('/^UN/', '', $flag);
  2146. $mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
  2147. $mflag, $status);
  2148. }
  2149. // clear cached counters
  2150. if ($flag == 'SEEN' || $flag == 'UNSEEN') {
  2151. $this->clear_messagecount($folder, 'SEEN');
  2152. $this->clear_messagecount($folder, 'UNSEEN');
  2153. }
  2154. else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
  2155. $this->clear_messagecount($folder, 'DELETED');
  2156. // remove cached messages
  2157. if ($this->options['skip_deleted']) {
  2158. $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
  2159. }
  2160. }
  2161. $this->set_search_dirty($folder);
  2162. }
  2163. return $result;
  2164. }
  2165. /**
  2166. * Append a mail message (source) to a specific folder
  2167. *
  2168. * @param string $folder Target folder
  2169. * @param string|array $message The message source string or filename
  2170. * or array (of strings and file pointers)
  2171. * @param string $headers Headers string if $message contains only the body
  2172. * @param boolean $is_file True if $message is a filename
  2173. * @param array $flags Message flags
  2174. * @param mixed $date Message internal date
  2175. * @param bool $binary Enables BINARY append
  2176. *
  2177. * @return int|bool Appended message UID or True on success, False on error
  2178. */
  2179. public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false)
  2180. {
  2181. if (!strlen($folder)) {
  2182. $folder = $this->folder;
  2183. }
  2184. if (!$this->check_connection()) {
  2185. return false;
  2186. }
  2187. // make sure folder exists
  2188. if (!$this->folder_exists($folder)) {
  2189. return false;
  2190. }
  2191. $date = $this->date_format($date);
  2192. if ($is_file) {
  2193. $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary);
  2194. }
  2195. else {
  2196. $saved = $this->conn->append($folder, $message, $flags, $date, $binary);
  2197. }
  2198. if ($saved) {
  2199. // increase messagecount of the target folder
  2200. $this->set_messagecount($folder, 'ALL', 1);
  2201. $this->plugins->exec_hook('message_saved', array(
  2202. 'folder' => $folder,
  2203. 'message' => $message,
  2204. 'headers' => $headers,
  2205. 'is_file' => $is_file,
  2206. 'flags' => $flags,
  2207. 'date' => $date,
  2208. 'binary' => $binary,
  2209. 'result' => $saved,
  2210. ));
  2211. }
  2212. return $saved;
  2213. }
  2214. /**
  2215. * Move a message from one folder to another
  2216. *
  2217. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2218. * @param string $to_mbox Target folder
  2219. * @param string $from_mbox Source folder
  2220. *
  2221. * @return boolean True on success, False on error
  2222. */
  2223. public function move_message($uids, $to_mbox, $from_mbox='')
  2224. {
  2225. if (!strlen($from_mbox)) {
  2226. $from_mbox = $this->folder;
  2227. }
  2228. if ($to_mbox === $from_mbox) {
  2229. return false;
  2230. }
  2231. list($uids, $all_mode) = $this->parse_uids($uids);
  2232. // exit if no message uids are specified
  2233. if (empty($uids)) {
  2234. return false;
  2235. }
  2236. if (!$this->check_connection()) {
  2237. return false;
  2238. }
  2239. $config = rcube::get_instance()->config;
  2240. $to_trash = $to_mbox == $config->get('trash_mbox');
  2241. // flag messages as read before moving them
  2242. if ($to_trash && $config->get('read_when_deleted')) {
  2243. // don't flush cache (4th argument)
  2244. $this->set_flag($uids, 'SEEN', $from_mbox, true);
  2245. }
  2246. // move messages
  2247. $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
  2248. // when moving to Trash we make sure the folder exists
  2249. // as it's uncommon scenario we do this when MOVE fails, not before
  2250. if (!$moved && $to_trash && $this->get_response_code() == rcube_storage::TRYCREATE) {
  2251. if ($this->create_folder($to_mbox, true, 'trash')) {
  2252. $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
  2253. }
  2254. }
  2255. if ($moved) {
  2256. $this->clear_messagecount($from_mbox);
  2257. $this->clear_messagecount($to_mbox);
  2258. $this->set_search_dirty($from_mbox);
  2259. $this->set_search_dirty($to_mbox);
  2260. }
  2261. // moving failed
  2262. else if ($to_trash && $config->get('delete_always', false)) {
  2263. $moved = $this->delete_message($uids, $from_mbox);
  2264. }
  2265. if ($moved) {
  2266. // unset threads internal cache
  2267. unset($this->icache['threads']);
  2268. // remove message ids from search set
  2269. if ($this->search_set && $from_mbox == $this->folder) {
  2270. // threads are too complicated to just remove messages from set
  2271. if ($this->search_threads || $all_mode) {
  2272. $this->refresh_search();
  2273. }
  2274. else if (!$this->search_set->incomplete) {
  2275. $this->search_set->filter(explode(',', $uids), $this->folder);
  2276. }
  2277. }
  2278. // remove cached messages
  2279. // @TODO: do cache update instead of clearing it
  2280. $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
  2281. }
  2282. return $moved;
  2283. }
  2284. /**
  2285. * Copy a message from one folder to another
  2286. *
  2287. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2288. * @param string $to_mbox Target folder
  2289. * @param string $from_mbox Source folder
  2290. *
  2291. * @return boolean True on success, False on error
  2292. */
  2293. public function copy_message($uids, $to_mbox, $from_mbox='')
  2294. {
  2295. if (!strlen($from_mbox)) {
  2296. $from_mbox = $this->folder;
  2297. }
  2298. list($uids, $all_mode) = $this->parse_uids($uids);
  2299. // exit if no message uids are specified
  2300. if (empty($uids)) {
  2301. return false;
  2302. }
  2303. if (!$this->check_connection()) {
  2304. return false;
  2305. }
  2306. // copy messages
  2307. $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
  2308. if ($copied) {
  2309. $this->clear_messagecount($to_mbox);
  2310. }
  2311. return $copied;
  2312. }
  2313. /**
  2314. * Mark messages as deleted and expunge them
  2315. *
  2316. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2317. * @param string $folder Source folder
  2318. *
  2319. * @return boolean True on success, False on error
  2320. */
  2321. public function delete_message($uids, $folder='')
  2322. {
  2323. if (!strlen($folder)) {
  2324. $folder = $this->folder;
  2325. }
  2326. list($uids, $all_mode) = $this->parse_uids($uids);
  2327. // exit if no message uids are specified
  2328. if (empty($uids)) {
  2329. return false;
  2330. }
  2331. if (!$this->check_connection()) {
  2332. return false;
  2333. }
  2334. $deleted = $this->conn->flag($folder, $uids, 'DELETED');
  2335. if ($deleted) {
  2336. // send expunge command in order to have the deleted message
  2337. // really deleted from the folder
  2338. $this->expunge_message($uids, $folder, false);
  2339. $this->clear_messagecount($folder);
  2340. // unset threads internal cache
  2341. unset($this->icache['threads']);
  2342. $this->set_search_dirty($folder);
  2343. // remove message ids from search set
  2344. if ($this->search_set && $folder == $this->folder) {
  2345. // threads are too complicated to just remove messages from set
  2346. if ($this->search_threads || $all_mode) {
  2347. $this->refresh_search();
  2348. }
  2349. else if (!$this->search_set->incomplete) {
  2350. $this->search_set->filter(explode(',', $uids));
  2351. }
  2352. }
  2353. // remove cached messages
  2354. $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
  2355. }
  2356. return $deleted;
  2357. }
  2358. /**
  2359. * Send IMAP expunge command and clear cache
  2360. *
  2361. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2362. * @param string $folder Folder name
  2363. * @param boolean $clear_cache False if cache should not be cleared
  2364. *
  2365. * @return boolean True on success, False on failure
  2366. */
  2367. public function expunge_message($uids, $folder = null, $clear_cache = true)
  2368. {
  2369. if ($uids && $this->get_capability('UIDPLUS')) {
  2370. list($uids, $all_mode) = $this->parse_uids($uids);
  2371. }
  2372. else {
  2373. $uids = null;
  2374. }
  2375. if (!strlen($folder)) {
  2376. $folder = $this->folder;
  2377. }
  2378. if (!$this->check_connection()) {
  2379. return false;
  2380. }
  2381. // force folder selection and check if folder is writeable
  2382. // to prevent a situation when CLOSE is executed on closed
  2383. // or EXPUNGE on read-only folder
  2384. $result = $this->conn->select($folder);
  2385. if (!$result) {
  2386. return false;
  2387. }
  2388. if (!$this->conn->data['READ-WRITE']) {
  2389. $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
  2390. return false;
  2391. }
  2392. // CLOSE(+SELECT) should be faster than EXPUNGE
  2393. if (empty($uids) || $all_mode) {
  2394. $result = $this->conn->close();
  2395. }
  2396. else {
  2397. $result = $this->conn->expunge($folder, $uids);
  2398. }
  2399. if ($result && $clear_cache) {
  2400. $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
  2401. $this->clear_messagecount($folder);
  2402. }
  2403. return $result;
  2404. }
  2405. /* --------------------------------
  2406. * folder managment
  2407. * --------------------------------*/
  2408. /**
  2409. * Public method for listing subscribed folders.
  2410. *
  2411. * @param string $root Optional root folder
  2412. * @param string $name Optional name pattern
  2413. * @param string $filter Optional filter
  2414. * @param string $rights Optional ACL requirements
  2415. * @param bool $skip_sort Enable to return unsorted list (for better performance)
  2416. *
  2417. * @return array List of folders
  2418. */
  2419. public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
  2420. {
  2421. $cache_key = $root.':'.$name;
  2422. if (!empty($filter)) {
  2423. $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
  2424. }
  2425. $cache_key .= ':'.$rights;
  2426. $cache_key = 'mailboxes.'.md5($cache_key);
  2427. // get cached folder list
  2428. $a_mboxes = $this->get_cache($cache_key);
  2429. if (is_array($a_mboxes)) {
  2430. return $a_mboxes;
  2431. }
  2432. // Give plugins a chance to provide a list of folders
  2433. $data = $this->plugins->exec_hook('storage_folders',
  2434. array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
  2435. if (isset($data['folders'])) {
  2436. $a_mboxes = $data['folders'];
  2437. }
  2438. else {
  2439. $a_mboxes = $this->list_folders_subscribed_direct($root, $name);
  2440. }
  2441. if (!is_array($a_mboxes)) {
  2442. return array();
  2443. }
  2444. // filter folders list according to rights requirements
  2445. if ($rights && $this->get_capability('ACL')) {
  2446. $a_mboxes = $this->filter_rights($a_mboxes, $rights);
  2447. }
  2448. // INBOX should always be available
  2449. if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
  2450. && (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
  2451. ) {
  2452. array_unshift($a_mboxes, 'INBOX');
  2453. }
  2454. // sort folders (always sort for cache)
  2455. if (!$skip_sort || $this->cache) {
  2456. $a_mboxes = $this->sort_folder_list($a_mboxes);
  2457. }
  2458. // write folders list to cache
  2459. $this->update_cache($cache_key, $a_mboxes);
  2460. return $a_mboxes;
  2461. }
  2462. /**
  2463. * Method for direct folders listing (LSUB)
  2464. *
  2465. * @param string $root Optional root folder
  2466. * @param string $name Optional name pattern
  2467. *
  2468. * @return array List of subscribed folders
  2469. * @see rcube_imap::list_folders_subscribed()
  2470. */
  2471. public function list_folders_subscribed_direct($root='', $name='*')
  2472. {
  2473. if (!$this->check_connection()) {
  2474. return null;
  2475. }
  2476. $config = rcube::get_instance()->config;
  2477. // Server supports LIST-EXTENDED, we can use selection options
  2478. // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
  2479. $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
  2480. if ($list_extended) {
  2481. // This will also set folder options, LSUB doesn't do that
  2482. $result = $this->conn->listMailboxes($root, $name,
  2483. NULL, array('SUBSCRIBED'));
  2484. }
  2485. else {
  2486. // retrieve list of folders from IMAP server using LSUB
  2487. $result = $this->conn->listSubscribed($root, $name);
  2488. }
  2489. if (!is_array($result)) {
  2490. return array();
  2491. }
  2492. // #1486796: some server configurations doesn't return folders in all namespaces
  2493. if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
  2494. $this->list_folders_update($result, ($list_extended ? 'ext-' : '') . 'subscribed');
  2495. }
  2496. // Remove hidden folders
  2497. if ($config->get('imap_skip_hidden_folders')) {
  2498. $result = array_filter($result, function($v) { return $v[0] != '.'; });
  2499. }
  2500. if ($list_extended) {
  2501. // unsubscribe non-existent folders, remove from the list
  2502. if ($name == '*' && !empty($this->conn->data['LIST'])) {
  2503. foreach ($result as $idx => $folder) {
  2504. if (($opts = $this->conn->data['LIST'][$folder])
  2505. && in_array_nocase('\\NonExistent', $opts)
  2506. ) {
  2507. $this->conn->unsubscribe($folder);
  2508. unset($result[$idx]);
  2509. }
  2510. }
  2511. }
  2512. }
  2513. else {
  2514. // unsubscribe non-existent folders, remove them from the list
  2515. if (!empty($result) && $name == '*') {
  2516. $existing = $this->list_folders($root, $name);
  2517. $nonexisting = array_diff($result, $existing);
  2518. $result = array_diff($result, $nonexisting);
  2519. foreach ($nonexisting as $folder) {
  2520. $this->conn->unsubscribe($folder);
  2521. }
  2522. }
  2523. }
  2524. return $result;
  2525. }
  2526. /**
  2527. * Get a list of all folders available on the server
  2528. *
  2529. * @param string $root IMAP root dir
  2530. * @param string $name Optional name pattern
  2531. * @param mixed $filter Optional filter
  2532. * @param string $rights Optional ACL requirements
  2533. * @param bool $skip_sort Enable to return unsorted list (for better performance)
  2534. *
  2535. * @return array Indexed array with folder names
  2536. */
  2537. public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
  2538. {
  2539. $cache_key = $root.':'.$name;
  2540. if (!empty($filter)) {
  2541. $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
  2542. }
  2543. $cache_key .= ':'.$rights;
  2544. $cache_key = 'mailboxes.list.'.md5($cache_key);
  2545. // get cached folder list
  2546. $a_mboxes = $this->get_cache($cache_key);
  2547. if (is_array($a_mboxes)) {
  2548. return $a_mboxes;
  2549. }
  2550. // Give plugins a chance to provide a list of folders
  2551. $data = $this->plugins->exec_hook('storage_folders',
  2552. array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
  2553. if (isset($data['folders'])) {
  2554. $a_mboxes = $data['folders'];
  2555. }
  2556. else {
  2557. // retrieve list of folders from IMAP server
  2558. $a_mboxes = $this->list_folders_direct($root, $name);
  2559. }
  2560. if (!is_array($a_mboxes)) {
  2561. $a_mboxes = array();
  2562. }
  2563. // INBOX should always be available
  2564. if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
  2565. && (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
  2566. ) {
  2567. array_unshift($a_mboxes, 'INBOX');
  2568. }
  2569. // cache folder attributes
  2570. if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
  2571. $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
  2572. }
  2573. // filter folders list according to rights requirements
  2574. if ($rights && $this->get_capability('ACL')) {
  2575. $a_mboxes = $this->filter_rights($a_mboxes, $rights);
  2576. }
  2577. // filter folders and sort them
  2578. if (!$skip_sort) {
  2579. $a_mboxes = $this->sort_folder_list($a_mboxes);
  2580. }
  2581. // write folders list to cache
  2582. $this->update_cache($cache_key, $a_mboxes);
  2583. return $a_mboxes;
  2584. }
  2585. /**
  2586. * Method for direct folders listing (LIST)
  2587. *
  2588. * @param string $root Optional root folder
  2589. * @param string $name Optional name pattern
  2590. *
  2591. * @return array List of folders
  2592. * @see rcube_imap::list_folders()
  2593. */
  2594. public function list_folders_direct($root='', $name='*')
  2595. {
  2596. if (!$this->check_connection()) {
  2597. return null;
  2598. }
  2599. $result = $this->conn->listMailboxes($root, $name);
  2600. if (!is_array($result)) {
  2601. return array();
  2602. }
  2603. $config = rcube::get_instance()->config;
  2604. // #1486796: some server configurations doesn't return folders in all namespaces
  2605. if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
  2606. $this->list_folders_update($result);
  2607. }
  2608. // Remove hidden folders
  2609. if ($config->get('imap_skip_hidden_folders')) {
  2610. $result = array_filter($result, function($v) { return $v[0] != '.'; });
  2611. }
  2612. return $result;
  2613. }
  2614. /**
  2615. * Fix folders list by adding folders from other namespaces.
  2616. * Needed on some servers eg. Courier IMAP
  2617. *
  2618. * @param array $result Reference to folders list
  2619. * @param string $type Listing type (ext-subscribed, subscribed or all)
  2620. */
  2621. protected function list_folders_update(&$result, $type = null)
  2622. {
  2623. $namespace = $this->get_namespace();
  2624. $search = array();
  2625. // build list of namespace prefixes
  2626. foreach ((array)$namespace as $ns) {
  2627. if (is_array($ns)) {
  2628. foreach ($ns as $ns_data) {
  2629. if (strlen($ns_data[0])) {
  2630. $search[] = $ns_data[0];
  2631. }
  2632. }
  2633. }
  2634. }
  2635. if (!empty($search)) {
  2636. // go through all folders detecting namespace usage
  2637. foreach ($result as $folder) {
  2638. foreach ($search as $idx => $prefix) {
  2639. if (strpos($folder, $prefix) === 0) {
  2640. unset($search[$idx]);
  2641. }
  2642. }
  2643. if (empty($search)) {
  2644. break;
  2645. }
  2646. }
  2647. // get folders in hidden namespaces and add to the result
  2648. foreach ($search as $prefix) {
  2649. if ($type == 'ext-subscribed') {
  2650. $list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED'));
  2651. }
  2652. else if ($type == 'subscribed') {
  2653. $list = $this->conn->listSubscribed('', $prefix . '*');
  2654. }
  2655. else {
  2656. $list = $this->conn->listMailboxes('', $prefix . '*');
  2657. }
  2658. if (!empty($list)) {
  2659. $result = array_merge($result, $list);
  2660. }
  2661. }
  2662. }
  2663. }
  2664. /**
  2665. * Filter the given list of folders according to access rights
  2666. *
  2667. * For performance reasons we assume user has full rights
  2668. * on all personal folders.
  2669. */
  2670. protected function filter_rights($a_folders, $rights)
  2671. {
  2672. $regex = '/('.$rights.')/';
  2673. foreach ($a_folders as $idx => $folder) {
  2674. if ($this->folder_namespace($folder) == 'personal') {
  2675. continue;
  2676. }
  2677. $myrights = join('', (array)$this->my_rights($folder));
  2678. if ($myrights !== null && !preg_match($regex, $myrights)) {
  2679. unset($a_folders[$idx]);
  2680. }
  2681. }
  2682. return $a_folders;
  2683. }
  2684. /**
  2685. * Get mailbox quota information
  2686. *
  2687. * @param string $folder Folder name
  2688. *
  2689. * @return mixed Quota info or False if not supported
  2690. */
  2691. public function get_quota($folder = null)
  2692. {
  2693. if ($this->get_capability('QUOTA') && $this->check_connection()) {
  2694. return $this->conn->getQuota($folder);
  2695. }
  2696. return false;
  2697. }
  2698. /**
  2699. * Get folder size (size of all messages in a folder)
  2700. *
  2701. * @param string $folder Folder name
  2702. *
  2703. * @return int Folder size in bytes, False on error
  2704. */
  2705. public function folder_size($folder)
  2706. {
  2707. if (!strlen($folder)) {
  2708. return false;
  2709. }
  2710. if (!$this->check_connection()) {
  2711. return 0;
  2712. }
  2713. // On Cyrus we can use special folder annotation, which should be much faster
  2714. if ($this->get_vendor() == 'cyrus') {
  2715. $idx = '/shared/vendor/cmu/cyrus-imapd/size';
  2716. $result = $this->get_metadata($folder, $idx, array(), true);
  2717. if (!empty($result) && is_numeric($result[$folder][$idx])) {
  2718. return $result[$folder][$idx];
  2719. }
  2720. }
  2721. // @TODO: could we try to use QUOTA here?
  2722. $result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
  2723. if (is_array($result)) {
  2724. $result = array_sum($result);
  2725. }
  2726. return $result;
  2727. }
  2728. /**
  2729. * Subscribe to a specific folder(s)
  2730. *
  2731. * @param array $folders Folder name(s)
  2732. *
  2733. * @return boolean True on success
  2734. */
  2735. public function subscribe($folders)
  2736. {
  2737. // let this common function do the main work
  2738. return $this->change_subscription($folders, 'subscribe');
  2739. }
  2740. /**
  2741. * Unsubscribe folder(s)
  2742. *
  2743. * @param array $a_mboxes Folder name(s)
  2744. *
  2745. * @return boolean True on success
  2746. */
  2747. public function unsubscribe($folders)
  2748. {
  2749. // let this common function do the main work
  2750. return $this->change_subscription($folders, 'unsubscribe');
  2751. }
  2752. /**
  2753. * Create a new folder on the server and register it in local cache
  2754. *
  2755. * @param string $folder New folder name
  2756. * @param boolean $subscribe True if the new folder should be subscribed
  2757. * @param string $type Optional folder type (junk, trash, drafts, sent, archive)
  2758. *
  2759. * @return boolean True on success
  2760. */
  2761. public function create_folder($folder, $subscribe = false, $type = null)
  2762. {
  2763. if (!$this->check_connection()) {
  2764. return false;
  2765. }
  2766. $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
  2767. // try to subscribe it
  2768. if ($result) {
  2769. // clear cache
  2770. $this->clear_cache('mailboxes', true);
  2771. if ($subscribe) {
  2772. $this->subscribe($folder);
  2773. }
  2774. }
  2775. return $result;
  2776. }
  2777. /**
  2778. * Set a new name to an existing folder
  2779. *
  2780. * @param string $folder Folder to rename
  2781. * @param string $new_name New folder name
  2782. *
  2783. * @return boolean True on success
  2784. */
  2785. public function rename_folder($folder, $new_name)
  2786. {
  2787. if (!strlen($new_name)) {
  2788. return false;
  2789. }
  2790. if (!$this->check_connection()) {
  2791. return false;
  2792. }
  2793. $delm = $this->get_hierarchy_delimiter();
  2794. // get list of subscribed folders
  2795. if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
  2796. $a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
  2797. $subscribed = $this->folder_exists($folder, true);
  2798. }
  2799. else {
  2800. $a_subscribed = $this->list_folders_subscribed();
  2801. $subscribed = in_array($folder, $a_subscribed);
  2802. }
  2803. $result = $this->conn->renameFolder($folder, $new_name);
  2804. if ($result) {
  2805. // unsubscribe the old folder, subscribe the new one
  2806. if ($subscribed) {
  2807. $this->conn->unsubscribe($folder);
  2808. $this->conn->subscribe($new_name);
  2809. }
  2810. // check if folder children are subscribed
  2811. foreach ($a_subscribed as $c_subscribed) {
  2812. if (strpos($c_subscribed, $folder.$delm) === 0) {
  2813. $this->conn->unsubscribe($c_subscribed);
  2814. $this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
  2815. $new_name, $c_subscribed));
  2816. // clear cache
  2817. $this->clear_message_cache($c_subscribed);
  2818. }
  2819. }
  2820. // clear cache
  2821. $this->clear_message_cache($folder);
  2822. $this->clear_cache('mailboxes', true);
  2823. }
  2824. return $result;
  2825. }
  2826. /**
  2827. * Remove folder from server
  2828. *
  2829. * @param string $folder Folder name
  2830. *
  2831. * @return boolean True on success
  2832. */
  2833. function delete_folder($folder)
  2834. {
  2835. $delm = $this->get_hierarchy_delimiter();
  2836. if (!$this->check_connection()) {
  2837. return false;
  2838. }
  2839. // get list of folders
  2840. if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
  2841. $sub_mboxes = $this->list_folders('', $folder . $delm . '*');
  2842. }
  2843. else {
  2844. $sub_mboxes = $this->list_folders();
  2845. }
  2846. // send delete command to server
  2847. $result = $this->conn->deleteFolder($folder);
  2848. if ($result) {
  2849. // unsubscribe folder
  2850. $this->conn->unsubscribe($folder);
  2851. foreach ($sub_mboxes as $c_mbox) {
  2852. if (strpos($c_mbox, $folder.$delm) === 0) {
  2853. $this->conn->unsubscribe($c_mbox);
  2854. if ($this->conn->deleteFolder($c_mbox)) {
  2855. $this->clear_message_cache($c_mbox);
  2856. }
  2857. }
  2858. }
  2859. // clear folder-related cache
  2860. $this->clear_message_cache($folder);
  2861. $this->clear_cache('mailboxes', true);
  2862. }
  2863. return $result;
  2864. }
  2865. /**
  2866. * Detect special folder associations stored in storage backend
  2867. */
  2868. public function get_special_folders($forced = false)
  2869. {
  2870. $result = parent::get_special_folders();
  2871. $rcube = rcube::get_instance();
  2872. // Lock SPECIAL-USE after user preferences change (#4782)
  2873. if ($rcube->config->get('lock_special_folders')) {
  2874. return $result;
  2875. }
  2876. if (isset($this->icache['special-use'])) {
  2877. return array_merge($result, $this->icache['special-use']);
  2878. }
  2879. if (!$forced || !$this->get_capability('SPECIAL-USE')) {
  2880. return $result;
  2881. }
  2882. if (!$this->check_connection()) {
  2883. return $result;
  2884. }
  2885. $types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
  2886. $special = array();
  2887. // request \Subscribed flag in LIST response as performance improvement for folder_exists()
  2888. $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
  2889. if (!empty($folders)) {
  2890. foreach ($folders as $folder) {
  2891. if ($flags = $this->conn->data['LIST'][$folder]) {
  2892. foreach ($types as $type) {
  2893. if (in_array($type, $flags)) {
  2894. $type = strtolower(substr($type, 1));
  2895. $special[$type] = $folder;
  2896. }
  2897. }
  2898. }
  2899. }
  2900. }
  2901. $this->icache['special-use'] = $special;
  2902. unset($this->icache['special-folders']);
  2903. return array_merge($result, $special);
  2904. }
  2905. /**
  2906. * Set special folder associations stored in storage backend
  2907. */
  2908. public function set_special_folders($specials)
  2909. {
  2910. if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
  2911. return false;
  2912. }
  2913. if (!$this->check_connection()) {
  2914. return false;
  2915. }
  2916. $folders = $this->get_special_folders(true);
  2917. $old = (array) $this->icache['special-use'];
  2918. foreach ($specials as $type => $folder) {
  2919. if (in_array($type, rcube_storage::$folder_types)) {
  2920. $old_folder = $old[$type];
  2921. if ($old_folder !== $folder) {
  2922. // unset old-folder metadata
  2923. if ($old_folder !== null) {
  2924. $this->delete_metadata($old_folder, array('/private/specialuse'));
  2925. }
  2926. // set new folder metadata
  2927. if ($folder) {
  2928. $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
  2929. }
  2930. }
  2931. }
  2932. }
  2933. $this->icache['special-use'] = $specials;
  2934. unset($this->icache['special-folders']);
  2935. return true;
  2936. }
  2937. /**
  2938. * Checks if folder exists and is subscribed
  2939. *
  2940. * @param string $folder Folder name
  2941. * @param boolean $subscription Enable subscription checking
  2942. *
  2943. * @return boolean TRUE or FALSE
  2944. */
  2945. public function folder_exists($folder, $subscription = false)
  2946. {
  2947. if ($folder == 'INBOX') {
  2948. return true;
  2949. }
  2950. $key = $subscription ? 'subscribed' : 'existing';
  2951. if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
  2952. return true;
  2953. }
  2954. if (!$this->check_connection()) {
  2955. return false;
  2956. }
  2957. if ($subscription) {
  2958. // It's possible we already called LIST command, check LIST data
  2959. if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
  2960. && in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
  2961. ) {
  2962. $a_folders = array($folder);
  2963. }
  2964. else {
  2965. $a_folders = $this->conn->listSubscribed('', $folder);
  2966. }
  2967. }
  2968. else {
  2969. // It's possible we already called LIST command, check LIST data
  2970. if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
  2971. $a_folders = array($folder);
  2972. }
  2973. else {
  2974. $a_folders = $this->conn->listMailboxes('', $folder);
  2975. }
  2976. }
  2977. if (is_array($a_folders) && in_array($folder, $a_folders)) {
  2978. $this->icache[$key][] = $folder;
  2979. return true;
  2980. }
  2981. return false;
  2982. }
  2983. /**
  2984. * Returns the namespace where the folder is in
  2985. *
  2986. * @param string $folder Folder name
  2987. *
  2988. * @return string One of 'personal', 'other' or 'shared'
  2989. */
  2990. public function folder_namespace($folder)
  2991. {
  2992. if ($folder == 'INBOX') {
  2993. return 'personal';
  2994. }
  2995. foreach ($this->namespace as $type => $namespace) {
  2996. if (is_array($namespace)) {
  2997. foreach ($namespace as $ns) {
  2998. if ($len = strlen($ns[0])) {
  2999. if (($len > 1 && $folder == substr($ns[0], 0, -1))
  3000. || strpos($folder, $ns[0]) === 0
  3001. ) {
  3002. return $type;
  3003. }
  3004. }
  3005. }
  3006. }
  3007. }
  3008. return 'personal';
  3009. }
  3010. /**
  3011. * Modify folder name according to personal namespace prefix.
  3012. * For output it removes prefix of the personal namespace if it's possible.
  3013. * For input it adds the prefix. Use it before creating a folder in root
  3014. * of the folders tree.
  3015. *
  3016. * @param string $folder Folder name
  3017. * @param string $mode Mode name (out/in)
  3018. *
  3019. * @return string Folder name
  3020. */
  3021. public function mod_folder($folder, $mode = 'out')
  3022. {
  3023. $prefix = $this->namespace['prefix_' . $mode]; // see set_env()
  3024. if ($prefix === null || $prefix === ''
  3025. || !($prefix_len = strlen($prefix)) || !strlen($folder)
  3026. ) {
  3027. return $folder;
  3028. }
  3029. // remove prefix for output
  3030. if ($mode == 'out') {
  3031. if (substr($folder, 0, $prefix_len) === $prefix) {
  3032. return substr($folder, $prefix_len);
  3033. }
  3034. return $folder;
  3035. }
  3036. // add prefix for input (e.g. folder creation)
  3037. return $prefix . $folder;
  3038. }
  3039. /**
  3040. * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
  3041. *
  3042. * @param string $folder Folder name
  3043. * @param bool $force Set to True if attributes should be refreshed
  3044. *
  3045. * @return array Options list
  3046. */
  3047. public function folder_attributes($folder, $force=false)
  3048. {
  3049. // get attributes directly from LIST command
  3050. if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
  3051. $opts = $this->conn->data['LIST'][$folder];
  3052. }
  3053. // get cached folder attributes
  3054. else if (!$force) {
  3055. $opts = $this->get_cache('mailboxes.attributes');
  3056. $opts = $opts[$folder];
  3057. }
  3058. if (!is_array($opts)) {
  3059. if (!$this->check_connection()) {
  3060. return array();
  3061. }
  3062. $this->conn->listMailboxes('', $folder);
  3063. $opts = $this->conn->data['LIST'][$folder];
  3064. }
  3065. return is_array($opts) ? $opts : array();
  3066. }
  3067. /**
  3068. * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
  3069. * PERMANENTFLAGS, UIDNEXT, UNSEEN
  3070. *
  3071. * @param string $folder Folder name
  3072. *
  3073. * @return array Data
  3074. */
  3075. public function folder_data($folder)
  3076. {
  3077. if (!strlen($folder)) {
  3078. $folder = $this->folder !== null ? $this->folder : 'INBOX';
  3079. }
  3080. if ($this->conn->selected != $folder) {
  3081. if (!$this->check_connection()) {
  3082. return array();
  3083. }
  3084. if ($this->conn->select($folder)) {
  3085. $this->folder = $folder;
  3086. }
  3087. else {
  3088. return null;
  3089. }
  3090. }
  3091. $data = $this->conn->data;
  3092. // add (E)SEARCH result for ALL UNDELETED query
  3093. if (!empty($this->icache['undeleted_idx'])
  3094. && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
  3095. ) {
  3096. $data['UNDELETED'] = $this->icache['undeleted_idx'];
  3097. }
  3098. return $data;
  3099. }
  3100. /**
  3101. * Returns extended information about the folder
  3102. *
  3103. * @param string $folder Folder name
  3104. *
  3105. * @return array Data
  3106. */
  3107. public function folder_info($folder)
  3108. {
  3109. if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
  3110. return $this->icache['options'];
  3111. }
  3112. // get cached metadata
  3113. $cache_key = 'mailboxes.folder-info.' . $folder;
  3114. $cached = $this->get_cache($cache_key);
  3115. if (is_array($cached)) {
  3116. return $cached;
  3117. }
  3118. $acl = $this->get_capability('ACL');
  3119. $namespace = $this->get_namespace();
  3120. $options = array();
  3121. // check if the folder is a namespace prefix
  3122. if (!empty($namespace)) {
  3123. $mbox = $folder . $this->delimiter;
  3124. foreach ($namespace as $ns) {
  3125. if (!empty($ns)) {
  3126. foreach ($ns as $item) {
  3127. if ($item[0] === $mbox) {
  3128. $options['is_root'] = true;
  3129. break 2;
  3130. }
  3131. }
  3132. }
  3133. }
  3134. }
  3135. // check if the folder is other user virtual-root
  3136. if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
  3137. $parts = explode($this->delimiter, $folder);
  3138. if (count($parts) == 2) {
  3139. $mbox = $parts[0] . $this->delimiter;
  3140. foreach ($namespace['other'] as $item) {
  3141. if ($item[0] === $mbox) {
  3142. $options['is_root'] = true;
  3143. break;
  3144. }
  3145. }
  3146. }
  3147. }
  3148. $options['name'] = $folder;
  3149. $options['attributes'] = $this->folder_attributes($folder, true);
  3150. $options['namespace'] = $this->folder_namespace($folder);
  3151. $options['special'] = $this->is_special_folder($folder);
  3152. // Set 'noselect' flag
  3153. if (is_array($options['attributes'])) {
  3154. foreach ($options['attributes'] as $attrib) {
  3155. $attrib = strtolower($attrib);
  3156. if ($attrib == '\noselect' || $attrib == '\nonexistent') {
  3157. $options['noselect'] = true;
  3158. }
  3159. }
  3160. }
  3161. else {
  3162. $options['noselect'] = true;
  3163. }
  3164. // Get folder rights (MYRIGHTS)
  3165. if ($acl && ($rights = $this->my_rights($folder))) {
  3166. $options['rights'] = $rights;
  3167. }
  3168. // Set 'norename' flag
  3169. if (!empty($options['rights'])) {
  3170. $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
  3171. if (!$options['noselect']) {
  3172. $options['noselect'] = !in_array('r', $options['rights']);
  3173. }
  3174. }
  3175. else {
  3176. $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
  3177. }
  3178. // update caches
  3179. $this->icache['options'] = $options;
  3180. $this->update_cache($cache_key, $options);
  3181. return $options;
  3182. }
  3183. /**
  3184. * Synchronizes messages cache.
  3185. *
  3186. * @param string $folder Folder name
  3187. */
  3188. public function folder_sync($folder)
  3189. {
  3190. if ($mcache = $this->get_mcache_engine()) {
  3191. $mcache->synchronize($folder);
  3192. }
  3193. }
  3194. /**
  3195. * Get message header names for rcube_imap_generic::fetchHeader(s)
  3196. *
  3197. * @return string Space-separated list of header names
  3198. */
  3199. protected function get_fetch_headers()
  3200. {
  3201. if (!empty($this->options['fetch_headers'])) {
  3202. $headers = explode(' ', $this->options['fetch_headers']);
  3203. }
  3204. else {
  3205. $headers = array();
  3206. }
  3207. if ($this->messages_caching || $this->options['all_headers']) {
  3208. $headers = array_merge($headers, $this->all_headers);
  3209. }
  3210. return $headers;
  3211. }
  3212. /* -----------------------------------------
  3213. * ACL and METADATA/ANNOTATEMORE methods
  3214. * ----------------------------------------*/
  3215. /**
  3216. * Changes the ACL on the specified folder (SETACL)
  3217. *
  3218. * @param string $folder Folder name
  3219. * @param string $user User name
  3220. * @param string $acl ACL string
  3221. *
  3222. * @return boolean True on success, False on failure
  3223. * @since 0.5-beta
  3224. */
  3225. public function set_acl($folder, $user, $acl)
  3226. {
  3227. if (!$this->get_capability('ACL')) {
  3228. return false;
  3229. }
  3230. if (!$this->check_connection()) {
  3231. return false;
  3232. }
  3233. $this->clear_cache('mailboxes.folder-info.' . $folder);
  3234. return $this->conn->setACL($folder, $user, $acl);
  3235. }
  3236. /**
  3237. * Removes any <identifier,rights> pair for the
  3238. * specified user from the ACL for the specified
  3239. * folder (DELETEACL)
  3240. *
  3241. * @param string $folder Folder name
  3242. * @param string $user User name
  3243. *
  3244. * @return boolean True on success, False on failure
  3245. * @since 0.5-beta
  3246. */
  3247. public function delete_acl($folder, $user)
  3248. {
  3249. if (!$this->get_capability('ACL')) {
  3250. return false;
  3251. }
  3252. if (!$this->check_connection()) {
  3253. return false;
  3254. }
  3255. return $this->conn->deleteACL($folder, $user);
  3256. }
  3257. /**
  3258. * Returns the access control list for folder (GETACL)
  3259. *
  3260. * @param string $folder Folder name
  3261. *
  3262. * @return array User-rights array on success, NULL on error
  3263. * @since 0.5-beta
  3264. */
  3265. public function get_acl($folder)
  3266. {
  3267. if (!$this->get_capability('ACL')) {
  3268. return null;
  3269. }
  3270. if (!$this->check_connection()) {
  3271. return null;
  3272. }
  3273. return $this->conn->getACL($folder);
  3274. }
  3275. /**
  3276. * Returns information about what rights can be granted to the
  3277. * user (identifier) in the ACL for the folder (LISTRIGHTS)
  3278. *
  3279. * @param string $folder Folder name
  3280. * @param string $user User name
  3281. *
  3282. * @return array List of user rights
  3283. * @since 0.5-beta
  3284. */
  3285. public function list_rights($folder, $user)
  3286. {
  3287. if (!$this->get_capability('ACL')) {
  3288. return null;
  3289. }
  3290. if (!$this->check_connection()) {
  3291. return null;
  3292. }
  3293. return $this->conn->listRights($folder, $user);
  3294. }
  3295. /**
  3296. * Returns the set of rights that the current user has to
  3297. * folder (MYRIGHTS)
  3298. *
  3299. * @param string $folder Folder name
  3300. *
  3301. * @return array MYRIGHTS response on success, NULL on error
  3302. * @since 0.5-beta
  3303. */
  3304. public function my_rights($folder)
  3305. {
  3306. if (!$this->get_capability('ACL')) {
  3307. return null;
  3308. }
  3309. if (!$this->check_connection()) {
  3310. return null;
  3311. }
  3312. return $this->conn->myRights($folder);
  3313. }
  3314. /**
  3315. * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
  3316. *
  3317. * @param string $folder Folder name (empty for server metadata)
  3318. * @param array $entries Entry-value array (use NULL value as NIL)
  3319. *
  3320. * @return boolean True on success, False on failure
  3321. * @since 0.5-beta
  3322. */
  3323. public function set_metadata($folder, $entries)
  3324. {
  3325. if (!$this->check_connection()) {
  3326. return false;
  3327. }
  3328. $this->clear_cache('mailboxes.metadata.', true);
  3329. if ($this->get_capability('METADATA') ||
  3330. (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
  3331. ) {
  3332. return $this->conn->setMetadata($folder, $entries);
  3333. }
  3334. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3335. foreach ((array)$entries as $entry => $value) {
  3336. list($ent, $attr) = $this->md2annotate($entry);
  3337. $entries[$entry] = array($ent, $attr, $value);
  3338. }
  3339. return $this->conn->setAnnotation($folder, $entries);
  3340. }
  3341. return false;
  3342. }
  3343. /**
  3344. * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
  3345. *
  3346. * @param string $folder Folder name (empty for server metadata)
  3347. * @param array $entries Entry names array
  3348. *
  3349. * @return boolean True on success, False on failure
  3350. * @since 0.5-beta
  3351. */
  3352. public function delete_metadata($folder, $entries)
  3353. {
  3354. if (!$this->check_connection()) {
  3355. return false;
  3356. }
  3357. $this->clear_cache('mailboxes.metadata.', true);
  3358. if ($this->get_capability('METADATA') ||
  3359. (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
  3360. ) {
  3361. return $this->conn->deleteMetadata($folder, $entries);
  3362. }
  3363. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3364. foreach ((array)$entries as $idx => $entry) {
  3365. list($ent, $attr) = $this->md2annotate($entry);
  3366. $entries[$idx] = array($ent, $attr, NULL);
  3367. }
  3368. return $this->conn->setAnnotation($folder, $entries);
  3369. }
  3370. return false;
  3371. }
  3372. /**
  3373. * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
  3374. *
  3375. * @param string $folder Folder name (empty for server metadata)
  3376. * @param array $entries Entries
  3377. * @param array $options Command options (with MAXSIZE and DEPTH keys)
  3378. * @param bool $force Disables cache use
  3379. *
  3380. * @return array Metadata entry-value hash array on success, NULL on error
  3381. * @since 0.5-beta
  3382. */
  3383. public function get_metadata($folder, $entries, $options = array(), $force = false)
  3384. {
  3385. $entries = (array) $entries;
  3386. if (!$force) {
  3387. // create cache key
  3388. // @TODO: this is the simplest solution, but we do the same with folders list
  3389. // maybe we should store data per-entry and merge on request
  3390. sort($options);
  3391. sort($entries);
  3392. $cache_key = 'mailboxes.metadata.' . $folder;
  3393. $cache_key .= '.' . md5(serialize($options).serialize($entries));
  3394. // get cached data
  3395. $cached_data = $this->get_cache($cache_key);
  3396. if (is_array($cached_data)) {
  3397. return $cached_data;
  3398. }
  3399. }
  3400. if (!$this->check_connection()) {
  3401. return null;
  3402. }
  3403. if ($this->get_capability('METADATA') ||
  3404. (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
  3405. ) {
  3406. $res = $this->conn->getMetadata($folder, $entries, $options);
  3407. }
  3408. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3409. $queries = array();
  3410. $res = array();
  3411. // Convert entry names
  3412. foreach ($entries as $entry) {
  3413. list($ent, $attr) = $this->md2annotate($entry);
  3414. $queries[$attr][] = $ent;
  3415. }
  3416. // @TODO: Honor MAXSIZE and DEPTH options
  3417. foreach ($queries as $attrib => $entry) {
  3418. $result = $this->conn->getAnnotation($folder, $entry, $attrib);
  3419. // an error, invalidate any previous getAnnotation() results
  3420. if (!is_array($result)) {
  3421. return null;
  3422. }
  3423. else {
  3424. foreach ($result as $fldr => $data) {
  3425. $res[$fldr] = array_merge((array) $res[$fldr], $data);
  3426. }
  3427. }
  3428. }
  3429. }
  3430. if (isset($res)) {
  3431. if (!$force) {
  3432. $this->update_cache($cache_key, $res);
  3433. }
  3434. return $res;
  3435. }
  3436. }
  3437. /**
  3438. * Converts the METADATA extension entry name into the correct
  3439. * entry-attrib names for older ANNOTATEMORE version.
  3440. *
  3441. * @param string $entry Entry name
  3442. *
  3443. * @return array Entry-attribute list, NULL if not supported (?)
  3444. */
  3445. protected function md2annotate($entry)
  3446. {
  3447. if (substr($entry, 0, 7) == '/shared') {
  3448. return array(substr($entry, 7), 'value.shared');
  3449. }
  3450. else if (substr($entry, 0, 8) == '/private') {
  3451. return array(substr($entry, 8), 'value.priv');
  3452. }
  3453. // @TODO: log error
  3454. }
  3455. /* --------------------------------
  3456. * internal caching methods
  3457. * --------------------------------*/
  3458. /**
  3459. * Enable or disable indexes caching
  3460. *
  3461. * @param string $type Cache type (@see rcube::get_cache)
  3462. */
  3463. public function set_caching($type)
  3464. {
  3465. if ($type) {
  3466. $this->caching = $type;
  3467. }
  3468. else {
  3469. if ($this->cache) {
  3470. $this->cache->close();
  3471. }
  3472. $this->cache = null;
  3473. $this->caching = false;
  3474. }
  3475. }
  3476. /**
  3477. * Getter for IMAP cache object
  3478. */
  3479. protected function get_cache_engine()
  3480. {
  3481. if ($this->caching && !$this->cache) {
  3482. $rcube = rcube::get_instance();
  3483. $ttl = $rcube->config->get('imap_cache_ttl', '10d');
  3484. $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
  3485. }
  3486. return $this->cache;
  3487. }
  3488. /**
  3489. * Returns cached value
  3490. *
  3491. * @param string $key Cache key
  3492. *
  3493. * @return mixed
  3494. */
  3495. public function get_cache($key)
  3496. {
  3497. if ($cache = $this->get_cache_engine()) {
  3498. return $cache->get($key);
  3499. }
  3500. }
  3501. /**
  3502. * Update cache
  3503. *
  3504. * @param string $key Cache key
  3505. * @param mixed $data Data
  3506. */
  3507. public function update_cache($key, $data)
  3508. {
  3509. if ($cache = $this->get_cache_engine()) {
  3510. $cache->set($key, $data);
  3511. }
  3512. }
  3513. /**
  3514. * Clears the cache.
  3515. *
  3516. * @param string $key Cache key name or pattern
  3517. * @param boolean $prefix_mode Enable it to clear all keys starting
  3518. * with prefix specified in $key
  3519. */
  3520. public function clear_cache($key = null, $prefix_mode = false)
  3521. {
  3522. if ($cache = $this->get_cache_engine()) {
  3523. $cache->remove($key, $prefix_mode);
  3524. }
  3525. }
  3526. /* --------------------------------
  3527. * message caching methods
  3528. * --------------------------------*/
  3529. /**
  3530. * Enable or disable messages caching
  3531. *
  3532. * @param boolean $set Flag
  3533. * @param int $mode Cache mode
  3534. */
  3535. public function set_messages_caching($set, $mode = null)
  3536. {
  3537. if ($set) {
  3538. $this->messages_caching = true;
  3539. if ($mode && ($cache = $this->get_mcache_engine())) {
  3540. $cache->set_mode($mode);
  3541. }
  3542. }
  3543. else {
  3544. if ($this->mcache) {
  3545. $this->mcache->close();
  3546. }
  3547. $this->mcache = null;
  3548. $this->messages_caching = false;
  3549. }
  3550. }
  3551. /**
  3552. * Getter for messages cache object
  3553. */
  3554. protected function get_mcache_engine()
  3555. {
  3556. if ($this->messages_caching && !$this->mcache) {
  3557. $rcube = rcube::get_instance();
  3558. if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
  3559. $ttl = $rcube->config->get('messages_cache_ttl', '10d');
  3560. $threshold = $rcube->config->get('messages_cache_threshold', 50);
  3561. $this->mcache = new rcube_imap_cache(
  3562. $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
  3563. }
  3564. }
  3565. return $this->mcache;
  3566. }
  3567. /**
  3568. * Clears the messages cache.
  3569. *
  3570. * @param string $folder Folder name
  3571. * @param array $uids Optional message UIDs to remove from cache
  3572. */
  3573. protected function clear_message_cache($folder = null, $uids = null)
  3574. {
  3575. if ($mcache = $this->get_mcache_engine()) {
  3576. $mcache->clear($folder, $uids);
  3577. }
  3578. }
  3579. /**
  3580. * Delete outdated cache entries
  3581. */
  3582. function cache_gc()
  3583. {
  3584. rcube_imap_cache::gc();
  3585. }
  3586. /* --------------------------------
  3587. * protected methods
  3588. * --------------------------------*/
  3589. /**
  3590. * Validate the given input and save to local properties
  3591. *
  3592. * @param string $sort_field Sort column
  3593. * @param string $sort_order Sort order
  3594. */
  3595. protected function set_sort_order($sort_field, $sort_order)
  3596. {
  3597. if ($sort_field != null) {
  3598. $this->sort_field = asciiwords($sort_field);
  3599. }
  3600. if ($sort_order != null) {
  3601. $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
  3602. }
  3603. }
  3604. /**
  3605. * Sort folders first by default folders and then in alphabethical order
  3606. *
  3607. * @param array $a_folders Folders list
  3608. * @param bool $skip_default Skip default folders handling
  3609. *
  3610. * @return array Sorted list
  3611. */
  3612. public function sort_folder_list($a_folders, $skip_default = false)
  3613. {
  3614. $specials = array_merge(array('INBOX'), array_values($this->get_special_folders()));
  3615. $folders = array();
  3616. // convert names to UTF-8
  3617. foreach ($a_folders as $folder) {
  3618. // for better performance skip encoding conversion
  3619. // if the string does not look like UTF7-IMAP
  3620. $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
  3621. }
  3622. // sort folders
  3623. // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
  3624. uasort($folders, array($this, 'sort_folder_comparator'));
  3625. $folders = array_keys($folders);
  3626. if ($skip_default) {
  3627. return $folders;
  3628. }
  3629. // force the type of folder name variable (#1485527)
  3630. $folders = array_map('strval', $folders);
  3631. $out = array();
  3632. // finally we must put special folders on top and rebuild the list
  3633. // to move their subfolders where they belong...
  3634. $specials = array_unique(array_intersect($specials, $folders));
  3635. $folders = array_merge($specials, array_diff($folders, $specials));
  3636. $this->sort_folder_specials(null, $folders, $specials, $out);
  3637. return $out;
  3638. }
  3639. /**
  3640. * Recursive function to put subfolders of special folders in place
  3641. */
  3642. protected function sort_folder_specials($folder, &$list, &$specials, &$out)
  3643. {
  3644. while (list($key, $name) = each($list)) {
  3645. if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
  3646. $out[] = $name;
  3647. unset($list[$key]);
  3648. if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
  3649. unset($specials[$found]);
  3650. $this->sort_folder_specials($name, $list, $specials, $out);
  3651. }
  3652. }
  3653. }
  3654. reset($list);
  3655. }
  3656. /**
  3657. * Callback for uasort() that implements correct
  3658. * locale-aware case-sensitive sorting
  3659. */
  3660. protected function sort_folder_comparator($str1, $str2)
  3661. {
  3662. $path1 = explode($this->delimiter, $str1);
  3663. $path2 = explode($this->delimiter, $str2);
  3664. foreach ($path1 as $idx => $folder1) {
  3665. $folder2 = $path2[$idx];
  3666. if ($folder1 === $folder2) {
  3667. continue;
  3668. }
  3669. return strcoll($folder1, $folder2);
  3670. }
  3671. }
  3672. /**
  3673. * Find UID of the specified message sequence ID
  3674. *
  3675. * @param int $id Message (sequence) ID
  3676. * @param string $folder Folder name
  3677. *
  3678. * @return int Message UID
  3679. */
  3680. public function id2uid($id, $folder = null)
  3681. {
  3682. if (!strlen($folder)) {
  3683. $folder = $this->folder;
  3684. }
  3685. if (!$this->check_connection()) {
  3686. return null;
  3687. }
  3688. return $this->conn->ID2UID($folder, $id);
  3689. }
  3690. /**
  3691. * Subscribe/unsubscribe a list of folders and update local cache
  3692. */
  3693. protected function change_subscription($folders, $mode)
  3694. {
  3695. $updated = 0;
  3696. $folders = (array) $folders;
  3697. if (!empty($folders)) {
  3698. if (!$this->check_connection()) {
  3699. return false;
  3700. }
  3701. foreach ($folders as $folder) {
  3702. $updated += (int) $this->conn->{$mode}($folder);
  3703. }
  3704. }
  3705. // clear cached folders list(s)
  3706. if ($updated) {
  3707. $this->clear_cache('mailboxes', true);
  3708. }
  3709. return $updated == count($folders);
  3710. }
  3711. /**
  3712. * Increde/decrese messagecount for a specific folder
  3713. */
  3714. protected function set_messagecount($folder, $mode, $increment)
  3715. {
  3716. if (!is_numeric($increment)) {
  3717. return false;
  3718. }
  3719. $mode = strtoupper($mode);
  3720. $a_folder_cache = $this->get_cache('messagecount');
  3721. if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
  3722. return false;
  3723. }
  3724. // add incremental value to messagecount
  3725. $a_folder_cache[$folder][$mode] += $increment;
  3726. // there's something wrong, delete from cache
  3727. if ($a_folder_cache[$folder][$mode] < 0) {
  3728. unset($a_folder_cache[$folder][$mode]);
  3729. }
  3730. // write back to cache
  3731. $this->update_cache('messagecount', $a_folder_cache);
  3732. return true;
  3733. }
  3734. /**
  3735. * Remove messagecount of a specific folder from cache
  3736. */
  3737. protected function clear_messagecount($folder, $mode=null)
  3738. {
  3739. $a_folder_cache = $this->get_cache('messagecount');
  3740. if (is_array($a_folder_cache[$folder])) {
  3741. if ($mode) {
  3742. unset($a_folder_cache[$folder][$mode]);
  3743. }
  3744. else {
  3745. unset($a_folder_cache[$folder]);
  3746. }
  3747. $this->update_cache('messagecount', $a_folder_cache);
  3748. }
  3749. }
  3750. /**
  3751. * Converts date string/object into IMAP date/time format
  3752. */
  3753. protected function date_format($date)
  3754. {
  3755. if (empty($date)) {
  3756. return null;
  3757. }
  3758. if (!is_object($date) || !is_a($date, 'DateTime')) {
  3759. try {
  3760. $timestamp = rcube_utils::strtotime($date);
  3761. $date = new DateTime("@".$timestamp);
  3762. }
  3763. catch (Exception $e) {
  3764. return null;
  3765. }
  3766. }
  3767. return $date->format('d-M-Y H:i:s O');
  3768. }
  3769. /**
  3770. * This is our own debug handler for the IMAP connection
  3771. */
  3772. public function debug_handler(&$imap, $message)
  3773. {
  3774. rcube::write_log('imap', $message);
  3775. }
  3776. }