2
0

rcube_imap.php 139 KB

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