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

rcube_imap.php 140KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421
  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 performed 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->sort_field, $this->sort_order, $this->search_set);
  1194. }
  1195. }
  1196. if (isset($index)) {
  1197. if ($this->sort_order != $index->get_parameters('ORDER')) {
  1198. $index->revert();
  1199. }
  1200. return $index;
  1201. }
  1202. }
  1203. // check local cache
  1204. if ($mcache = $this->get_mcache_engine()) {
  1205. return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
  1206. }
  1207. // fetch from IMAP server
  1208. return $this->index_direct($folder, $this->sort_field, $this->sort_order);
  1209. }
  1210. /**
  1211. * Return sorted list of message UIDs ignoring current search settings.
  1212. * Doesn't uses cache by default.
  1213. *
  1214. * @param string $folder Folder to get index from
  1215. * @param string $sort_field Sort column
  1216. * @param string $sort_order Sort order [ASC, DESC]
  1217. * @param rcube_result_* $search Optional messages set to limit the result
  1218. *
  1219. * @return rcube_result_index Sorted list of message UIDs
  1220. */
  1221. public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
  1222. {
  1223. if (!empty($search)) {
  1224. $search = $search->get_compressed();
  1225. }
  1226. // use message index sort as default sorting
  1227. if (!$sort_field) {
  1228. // use search result from count() if possible
  1229. if (empty($search) && $this->options['skip_deleted']
  1230. && !empty($this->icache['undeleted_idx'])
  1231. && $this->icache['undeleted_idx']->get_parameters('ALL') !== null
  1232. && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
  1233. ) {
  1234. $index = $this->icache['undeleted_idx'];
  1235. }
  1236. else if (!$this->check_connection()) {
  1237. return new rcube_result_index();
  1238. }
  1239. else {
  1240. $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
  1241. if ($search) {
  1242. $query = trim($query . ' UID ' . $search);
  1243. }
  1244. $index = $this->conn->search($folder, $query, true);
  1245. }
  1246. }
  1247. else if (!$this->check_connection()) {
  1248. return new rcube_result_index();
  1249. }
  1250. // fetch complete message index
  1251. else {
  1252. if ($this->get_capability('SORT')) {
  1253. $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
  1254. if ($search) {
  1255. $query = trim($query . ' UID ' . $search);
  1256. }
  1257. $index = $this->conn->sort($folder, $sort_field, $query, true);
  1258. }
  1259. if (empty($index) || $index->is_error()) {
  1260. $index = $this->conn->index($folder, $search ? $search : "1:*",
  1261. $sort_field, $this->options['skip_deleted'],
  1262. $search ? true : false, true);
  1263. }
  1264. }
  1265. if ($sort_order != $index->get_parameters('ORDER')) {
  1266. $index->revert();
  1267. }
  1268. return $index;
  1269. }
  1270. /**
  1271. * Return index of threaded message UIDs
  1272. *
  1273. * @param string $folder Folder to get index from
  1274. * @param string $sort_field Sort column
  1275. * @param string $sort_order Sort order [ASC, DESC]
  1276. *
  1277. * @return rcube_result_thread Message UIDs
  1278. */
  1279. public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL)
  1280. {
  1281. if (!strlen($folder)) {
  1282. $folder = $this->folder;
  1283. }
  1284. // we have a saved search result, get index from there
  1285. if ($this->search_string && $this->search_threads && $folder == $this->folder) {
  1286. $threads = $this->search_set;
  1287. }
  1288. else {
  1289. // get all threads (default sort order)
  1290. $threads = $this->threads($folder);
  1291. }
  1292. $this->set_sort_order($sort_field, $sort_order);
  1293. $this->sort_threads($threads);
  1294. return $threads;
  1295. }
  1296. /**
  1297. * Sort threaded result, using THREAD=REFS method if available.
  1298. * If not, use any method and re-sort the result in THREAD=REFS way.
  1299. *
  1300. * @param rcube_result_thread $threads Threads result set
  1301. */
  1302. protected function sort_threads($threads)
  1303. {
  1304. if ($threads->is_empty()) {
  1305. return;
  1306. }
  1307. // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
  1308. // THREAD=REFERENCES: sorting by sent date of root message
  1309. // THREAD=REFS: sorting by the most recent date in each thread
  1310. if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
  1311. $sortby = $this->sort_field ? $this->sort_field : 'date';
  1312. $index = $this->index($this->folder, $sortby, $this->sort_order, true, true);
  1313. if (!$index->is_empty()) {
  1314. $threads->sort($index);
  1315. }
  1316. }
  1317. else if ($this->sort_order != $threads->get_parameters('ORDER')) {
  1318. $threads->revert();
  1319. }
  1320. }
  1321. /**
  1322. * Invoke search request to IMAP server
  1323. *
  1324. * @param string $folder Folder name to search in
  1325. * @param string $search Search criteria
  1326. * @param string $charset Search charset
  1327. * @param string $sort_field Header field to sort by
  1328. *
  1329. * @return rcube_result_index Search result object
  1330. * @todo: Search criteria should be provided in non-IMAP format, eg. array
  1331. */
  1332. public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
  1333. {
  1334. if (!$search) {
  1335. $search = 'ALL';
  1336. }
  1337. if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
  1338. $folder = $this->folder;
  1339. }
  1340. $plugin = $this->plugins->exec_hook('imap_search_before', array(
  1341. 'folder' => $folder,
  1342. 'search' => $search,
  1343. 'charset' => $charset,
  1344. 'sort_field' => $sort_field,
  1345. 'threading' => $this->threading,
  1346. ));
  1347. $folder = $plugin['folder'];
  1348. $search = $plugin['search'];
  1349. $charset = $plugin['charset'];
  1350. $sort_field = $plugin['sort_field'];
  1351. $results = $plugin['result'];
  1352. // multi-folder search
  1353. if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
  1354. // connect IMAP to have all the required classes and settings loaded
  1355. $this->check_connection();
  1356. // disable threading
  1357. $this->threading = false;
  1358. $searcher = new rcube_imap_search($this->options, $this->conn);
  1359. // set limit to not exceed the client's request timeout
  1360. $searcher->set_timelimit(60);
  1361. // continue existing incomplete search
  1362. if (!empty($this->search_set) && $this->search_set->incomplete && $search == $this->search_string) {
  1363. $searcher->set_results($this->search_set);
  1364. }
  1365. // execute the search
  1366. $results = $searcher->exec(
  1367. $folder,
  1368. $search,
  1369. $charset ? $charset : $this->default_charset,
  1370. $sort_field && $this->get_capability('SORT') ? $sort_field : null,
  1371. $this->threading
  1372. );
  1373. }
  1374. else if (!$results) {
  1375. $folder = is_array($folder) ? $folder[0] : $folder;
  1376. $search = is_array($search) ? $search[$folder] : $search;
  1377. $results = $this->search_index($folder, $search, $charset, $sort_field);
  1378. }
  1379. $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
  1380. $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
  1381. return $results;
  1382. }
  1383. /**
  1384. * Direct (real and simple) SEARCH request (without result sorting and caching).
  1385. *
  1386. * @param string $mailbox Mailbox name to search in
  1387. * @param string $str Search string
  1388. *
  1389. * @return rcube_result_index Search result (UIDs)
  1390. */
  1391. public function search_once($folder = null, $str = 'ALL')
  1392. {
  1393. if (!$this->check_connection()) {
  1394. return new rcube_result_index();
  1395. }
  1396. if (!$str) {
  1397. $str = 'ALL';
  1398. }
  1399. // multi-folder search
  1400. if (is_array($folder) && count($folder) > 1) {
  1401. $searcher = new rcube_imap_search($this->options, $this->conn);
  1402. $index = $searcher->exec($folder, $str, $this->default_charset);
  1403. }
  1404. else {
  1405. $folder = is_array($folder) ? $folder[0] : $folder;
  1406. if (!strlen($folder)) {
  1407. $folder = $this->folder;
  1408. }
  1409. $index = $this->conn->search($folder, $str, true);
  1410. }
  1411. return $index;
  1412. }
  1413. /**
  1414. * protected search method
  1415. *
  1416. * @param string $folder Folder name
  1417. * @param string $criteria Search criteria
  1418. * @param string $charset Charset
  1419. * @param string $sort_field Sorting field
  1420. *
  1421. * @return rcube_result_index|rcube_result_thread Search results (UIDs)
  1422. * @see rcube_imap::search()
  1423. */
  1424. protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
  1425. {
  1426. if (!$this->check_connection()) {
  1427. if ($this->threading) {
  1428. return new rcube_result_thread();
  1429. }
  1430. else {
  1431. return new rcube_result_index();
  1432. }
  1433. }
  1434. if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
  1435. $criteria = 'UNDELETED '.$criteria;
  1436. }
  1437. // unset CHARSET if criteria string is ASCII, this way
  1438. // SEARCH won't be re-sent after "unsupported charset" response
  1439. if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
  1440. $charset = 'US-ASCII';
  1441. }
  1442. if ($this->threading) {
  1443. $threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
  1444. // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
  1445. // but I've seen that Courier doesn't support UTF-8)
  1446. if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
  1447. $threads = $this->conn->thread($folder, $this->threading,
  1448. self::convert_criteria($criteria, $charset), true, 'US-ASCII');
  1449. }
  1450. return $threads;
  1451. }
  1452. if ($sort_field && $this->get_capability('SORT')) {
  1453. $charset = $charset ? $charset : $this->default_charset;
  1454. $messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
  1455. // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
  1456. // but I've seen Courier with disabled UTF-8 support)
  1457. if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
  1458. $messages = $this->conn->sort($folder, $sort_field,
  1459. self::convert_criteria($criteria, $charset), true, 'US-ASCII');
  1460. }
  1461. if (!$messages->is_error()) {
  1462. $this->search_sorted = true;
  1463. return $messages;
  1464. }
  1465. }
  1466. $messages = $this->conn->search($folder,
  1467. ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
  1468. // Error, try with US-ASCII (some servers may support only US-ASCII)
  1469. if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
  1470. $messages = $this->conn->search($folder,
  1471. self::convert_criteria($criteria, $charset), true);
  1472. }
  1473. $this->search_sorted = false;
  1474. return $messages;
  1475. }
  1476. /**
  1477. * Converts charset of search criteria string
  1478. *
  1479. * @param string $str Search string
  1480. * @param string $charset Original charset
  1481. * @param string $dest_charset Destination charset (default US-ASCII)
  1482. *
  1483. * @return string Search string
  1484. */
  1485. public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
  1486. {
  1487. // convert strings to US_ASCII
  1488. if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
  1489. $last = 0; $res = '';
  1490. foreach ($matches[1] as $m) {
  1491. $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
  1492. $string = substr($str, $string_offset - 1, $m[0]);
  1493. $string = rcube_charset::convert($string, $charset, $dest_charset);
  1494. if ($string === false || !strlen($string)) {
  1495. continue;
  1496. }
  1497. $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
  1498. $last = $m[0] + $string_offset - 1;
  1499. }
  1500. if ($last < strlen($str)) {
  1501. $res .= substr($str, $last, strlen($str)-$last);
  1502. }
  1503. }
  1504. // strings for conversion not found
  1505. else {
  1506. $res = $str;
  1507. }
  1508. return $res;
  1509. }
  1510. /**
  1511. * Refresh saved search set
  1512. *
  1513. * @return array Current search set
  1514. */
  1515. public function refresh_search()
  1516. {
  1517. if (!empty($this->search_string)) {
  1518. $this->search(
  1519. is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
  1520. $this->search_string,
  1521. $this->search_charset,
  1522. $this->search_sort_field
  1523. );
  1524. }
  1525. return $this->get_search_set();
  1526. }
  1527. /**
  1528. * Flag certain result subsets as 'incomplete'.
  1529. * For subsequent refresh_search() calls to only refresh the updated parts.
  1530. */
  1531. protected function set_search_dirty($folder)
  1532. {
  1533. if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
  1534. if ($subset = $this->search_set->get_set($folder)) {
  1535. $subset->incomplete = $this->search_set->incomplete = true;
  1536. }
  1537. }
  1538. }
  1539. /**
  1540. * Return message headers object of a specific message
  1541. *
  1542. * @param int $id Message UID
  1543. * @param string $folder Folder to read from
  1544. * @param bool $force True to skip cache
  1545. *
  1546. * @return rcube_message_header Message headers
  1547. */
  1548. public function get_message_headers($uid, $folder = null, $force = false)
  1549. {
  1550. // decode combined UID-folder identifier
  1551. if (preg_match('/^\d+-.+/', $uid)) {
  1552. list($uid, $folder) = explode('-', $uid, 2);
  1553. }
  1554. if (!strlen($folder)) {
  1555. $folder = $this->folder;
  1556. }
  1557. // get cached headers
  1558. if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
  1559. $headers = $mcache->get_message($folder, $uid);
  1560. }
  1561. else if (!$this->check_connection()) {
  1562. $headers = false;
  1563. }
  1564. else {
  1565. $headers = $this->conn->fetchHeader(
  1566. $folder, $uid, true, true, $this->get_fetch_headers());
  1567. if (is_object($headers))
  1568. $headers->folder = $folder;
  1569. }
  1570. return $headers;
  1571. }
  1572. /**
  1573. * Fetch message headers and body structure from the IMAP server and build
  1574. * an object structure.
  1575. *
  1576. * @param int $uid Message UID to fetch
  1577. * @param string $folder Folder to read from
  1578. *
  1579. * @return object rcube_message_header Message data
  1580. */
  1581. public function get_message($uid, $folder = null)
  1582. {
  1583. if (!strlen($folder)) {
  1584. $folder = $this->folder;
  1585. }
  1586. // decode combined UID-folder identifier
  1587. if (preg_match('/^\d+-.+/', $uid)) {
  1588. list($uid, $folder) = explode('-', $uid, 2);
  1589. }
  1590. // Check internal cache
  1591. if (!empty($this->icache['message'])) {
  1592. if (($headers = $this->icache['message']) && $headers->uid == $uid) {
  1593. return $headers;
  1594. }
  1595. }
  1596. $headers = $this->get_message_headers($uid, $folder);
  1597. // message doesn't exist?
  1598. if (empty($headers)) {
  1599. return null;
  1600. }
  1601. // structure might be cached
  1602. if (!empty($headers->structure)) {
  1603. return $headers;
  1604. }
  1605. $this->msg_uid = $uid;
  1606. if (!$this->check_connection()) {
  1607. return $headers;
  1608. }
  1609. if (empty($headers->bodystructure)) {
  1610. $headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
  1611. }
  1612. $structure = $headers->bodystructure;
  1613. if (empty($structure)) {
  1614. return $headers;
  1615. }
  1616. // set message charset from message headers
  1617. if ($headers->charset) {
  1618. $this->struct_charset = $headers->charset;
  1619. }
  1620. else {
  1621. $this->struct_charset = $this->structure_charset($structure);
  1622. }
  1623. $headers->ctype = @strtolower($headers->ctype);
  1624. // Here we can recognize malformed BODYSTRUCTURE and
  1625. // 1. [@TODO] parse the message in other way to create our own message structure
  1626. // 2. or just show the raw message body.
  1627. // Example of structure for malformed MIME message:
  1628. // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
  1629. if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
  1630. && strtolower($structure[0].'/'.$structure[1]) == 'text/plain'
  1631. ) {
  1632. // A special known case "Content-type: text" (#1488968)
  1633. if ($headers->ctype == 'text') {
  1634. $structure[1] = 'plain';
  1635. $headers->ctype = 'text/plain';
  1636. }
  1637. // we can handle single-part messages, by simple fix in structure (#1486898)
  1638. else if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
  1639. $structure[0] = $m[1];
  1640. $structure[1] = $m[2];
  1641. }
  1642. else {
  1643. // Try to parse the message using rcube_mime_decode.
  1644. // We need a better solution, it parses message
  1645. // in memory, which wouldn't work for very big messages,
  1646. // (it uses up to 10x more memory than the message size)
  1647. // it's also buggy and not actively developed
  1648. if ($headers->size && rcube_utils::mem_check($headers->size * 10)) {
  1649. $raw_msg = $this->get_raw_body($uid);
  1650. $struct = rcube_mime::parse_message($raw_msg);
  1651. }
  1652. else {
  1653. return $headers;
  1654. }
  1655. }
  1656. }
  1657. if (empty($struct)) {
  1658. $struct = $this->structure_part($structure, 0, '', $headers);
  1659. }
  1660. // some workarounds on simple messages...
  1661. if (empty($struct->parts)) {
  1662. // ...don't trust given content-type
  1663. if (!empty($headers->ctype)) {
  1664. $struct->mime_id = '1';
  1665. $struct->mimetype = strtolower($headers->ctype);
  1666. list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
  1667. }
  1668. // ...and charset (there's a case described in #1488968 where invalid content-type
  1669. // results in invalid charset in BODYSTRUCTURE)
  1670. if (!empty($headers->charset) && $headers->charset != $struct->ctype_parameters['charset']) {
  1671. $struct->charset = $headers->charset;
  1672. $struct->ctype_parameters['charset'] = $headers->charset;
  1673. }
  1674. }
  1675. $headers->structure = $struct;
  1676. return $this->icache['message'] = $headers;
  1677. }
  1678. /**
  1679. * Build message part object
  1680. *
  1681. * @param array $part
  1682. * @param int $count
  1683. * @param string $parent
  1684. */
  1685. protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null)
  1686. {
  1687. $struct = new rcube_message_part;
  1688. $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
  1689. // multipart
  1690. if (is_array($part[0])) {
  1691. $struct->ctype_primary = 'multipart';
  1692. /* RFC3501: BODYSTRUCTURE fields of multipart part
  1693. part1 array
  1694. part2 array
  1695. part3 array
  1696. ....
  1697. 1. subtype
  1698. 2. parameters (optional)
  1699. 3. description (optional)
  1700. 4. language (optional)
  1701. 5. location (optional)
  1702. */
  1703. // find first non-array entry
  1704. for ($i=1; $i<count($part); $i++) {
  1705. if (!is_array($part[$i])) {
  1706. $struct->ctype_secondary = strtolower($part[$i]);
  1707. // read content type parameters
  1708. if (is_array($part[$i+1])) {
  1709. $struct->ctype_parameters = array();
  1710. for ($j=0; $j<count($part[$i+1]); $j+=2) {
  1711. $param = strtolower($part[$i+1][$j]);
  1712. $struct->ctype_parameters[$param] = $part[$i+1][$j+1];
  1713. }
  1714. }
  1715. break;
  1716. }
  1717. }
  1718. $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
  1719. // build parts list for headers pre-fetching
  1720. for ($i=0; $i<count($part); $i++) {
  1721. if (!is_array($part[$i])) {
  1722. break;
  1723. }
  1724. // fetch message headers if message/rfc822
  1725. // or named part (could contain Content-Location header)
  1726. if (!is_array($part[$i][0])) {
  1727. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1728. if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
  1729. $mime_part_headers[] = $tmp_part_id;
  1730. }
  1731. else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
  1732. $mime_part_headers[] = $tmp_part_id;
  1733. }
  1734. }
  1735. }
  1736. // pre-fetch headers of all parts (in one command for better performance)
  1737. // @TODO: we could do this before _structure_part() call, to fetch
  1738. // headers for parts on all levels
  1739. if ($mime_part_headers) {
  1740. $mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder,
  1741. $this->msg_uid, $mime_part_headers);
  1742. }
  1743. $struct->parts = array();
  1744. for ($i=0, $count=0; $i<count($part); $i++) {
  1745. if (!is_array($part[$i])) {
  1746. break;
  1747. }
  1748. $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
  1749. $struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id,
  1750. $mime_part_headers[$tmp_part_id]);
  1751. }
  1752. return $struct;
  1753. }
  1754. /* RFC3501: BODYSTRUCTURE fields of non-multipart part
  1755. 0. type
  1756. 1. subtype
  1757. 2. parameters
  1758. 3. id
  1759. 4. description
  1760. 5. encoding
  1761. 6. size
  1762. -- text
  1763. 7. lines
  1764. -- message/rfc822
  1765. 7. envelope structure
  1766. 8. body structure
  1767. 9. lines
  1768. --
  1769. x. md5 (optional)
  1770. x. disposition (optional)
  1771. x. language (optional)
  1772. x. location (optional)
  1773. */
  1774. // regular part
  1775. $struct->ctype_primary = strtolower($part[0]);
  1776. $struct->ctype_secondary = strtolower($part[1]);
  1777. $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
  1778. // read content type parameters
  1779. if (is_array($part[2])) {
  1780. $struct->ctype_parameters = array();
  1781. for ($i=0; $i<count($part[2]); $i+=2) {
  1782. $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
  1783. }
  1784. if (isset($struct->ctype_parameters['charset'])) {
  1785. $struct->charset = $struct->ctype_parameters['charset'];
  1786. }
  1787. }
  1788. // #1487700: workaround for lack of charset in malformed structure
  1789. if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
  1790. $struct->charset = $mime_headers->charset;
  1791. }
  1792. // read content encoding
  1793. if (!empty($part[5])) {
  1794. $struct->encoding = strtolower($part[5]);
  1795. $struct->headers['content-transfer-encoding'] = $struct->encoding;
  1796. }
  1797. // get part size
  1798. if (!empty($part[6])) {
  1799. $struct->size = intval($part[6]);
  1800. }
  1801. // read part disposition
  1802. $di = 8;
  1803. if ($struct->ctype_primary == 'text') {
  1804. $di += 1;
  1805. }
  1806. else if ($struct->mimetype == 'message/rfc822') {
  1807. $di += 3;
  1808. }
  1809. if (is_array($part[$di]) && count($part[$di]) == 2) {
  1810. $struct->disposition = strtolower($part[$di][0]);
  1811. if ($struct->disposition && $struct->disposition !== 'inline' && $struct->disposition !== 'attachment') {
  1812. // RFC2183, Section 2.8 - unrecognized type should be treated as "attachment"
  1813. $struct->disposition = 'attachment';
  1814. }
  1815. if (is_array($part[$di][1])) {
  1816. for ($n=0; $n<count($part[$di][1]); $n+=2) {
  1817. $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
  1818. }
  1819. }
  1820. }
  1821. // get message/rfc822's child-parts
  1822. if (is_array($part[8]) && $di != 8) {
  1823. $struct->parts = array();
  1824. for ($i=0, $count=0; $i<count($part[8]); $i++) {
  1825. if (!is_array($part[8][$i])) {
  1826. break;
  1827. }
  1828. $struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id);
  1829. }
  1830. }
  1831. // get part ID
  1832. if (!empty($part[3])) {
  1833. $struct->content_id = $part[3];
  1834. $struct->headers['content-id'] = $part[3];
  1835. if (empty($struct->disposition)) {
  1836. $struct->disposition = 'inline';
  1837. }
  1838. }
  1839. // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
  1840. if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
  1841. if (empty($mime_headers)) {
  1842. $mime_headers = $this->conn->fetchPartHeader(
  1843. $this->folder, $this->msg_uid, true, $struct->mime_id);
  1844. }
  1845. if (is_string($mime_headers)) {
  1846. $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
  1847. }
  1848. else if (is_object($mime_headers)) {
  1849. $struct->headers = get_object_vars($mime_headers) + $struct->headers;
  1850. }
  1851. // get real content-type of message/rfc822
  1852. if ($struct->mimetype == 'message/rfc822') {
  1853. // single-part
  1854. if (!is_array($part[8][0])) {
  1855. $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
  1856. }
  1857. // multi-part
  1858. else {
  1859. for ($n=0; $n<count($part[8]); $n++) {
  1860. if (!is_array($part[8][$n])) {
  1861. break;
  1862. }
  1863. }
  1864. $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
  1865. }
  1866. }
  1867. if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
  1868. if (is_array($part[8]) && $di != 8) {
  1869. $struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id);
  1870. }
  1871. }
  1872. }
  1873. // normalize filename property
  1874. $this->set_part_filename($struct, $mime_headers);
  1875. return $struct;
  1876. }
  1877. /**
  1878. * Set attachment filename from message part structure
  1879. *
  1880. * @param rcube_message_part $part Part object
  1881. * @param string $headers Part's raw headers
  1882. */
  1883. protected function set_part_filename(&$part, $headers = null)
  1884. {
  1885. if (!empty($part->d_parameters['filename'])) {
  1886. $filename_mime = $part->d_parameters['filename'];
  1887. }
  1888. else if (!empty($part->d_parameters['filename*'])) {
  1889. $filename_encoded = $part->d_parameters['filename*'];
  1890. }
  1891. else if (!empty($part->ctype_parameters['name*'])) {
  1892. $filename_encoded = $part->ctype_parameters['name*'];
  1893. }
  1894. // RFC2231 value continuations
  1895. // TODO: this should be rewrited to support RFC2231 4.1 combinations
  1896. else if (!empty($part->d_parameters['filename*0'])) {
  1897. $i = 0;
  1898. while (isset($part->d_parameters['filename*'.$i])) {
  1899. $filename_mime .= $part->d_parameters['filename*'.$i];
  1900. $i++;
  1901. }
  1902. // some servers (eg. dovecot-1.x) have no support for parameter value continuations
  1903. // we must fetch and parse headers "manually"
  1904. if ($i<2) {
  1905. if (!$headers) {
  1906. $headers = $this->conn->fetchPartHeader(
  1907. $this->folder, $this->msg_uid, true, $part->mime_id);
  1908. }
  1909. $filename_mime = '';
  1910. $i = 0;
  1911. while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1912. $filename_mime .= $matches[1];
  1913. $i++;
  1914. }
  1915. }
  1916. }
  1917. else if (!empty($part->d_parameters['filename*0*'])) {
  1918. $i = 0;
  1919. while (isset($part->d_parameters['filename*'.$i.'*'])) {
  1920. $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
  1921. $i++;
  1922. }
  1923. if ($i<2) {
  1924. if (!$headers) {
  1925. $headers = $this->conn->fetchPartHeader(
  1926. $this->folder, $this->msg_uid, true, $part->mime_id);
  1927. }
  1928. $filename_encoded = '';
  1929. $i = 0; $matches = array();
  1930. while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1931. $filename_encoded .= $matches[1];
  1932. $i++;
  1933. }
  1934. }
  1935. }
  1936. else if (!empty($part->ctype_parameters['name*0'])) {
  1937. $i = 0;
  1938. while (isset($part->ctype_parameters['name*'.$i])) {
  1939. $filename_mime .= $part->ctype_parameters['name*'.$i];
  1940. $i++;
  1941. }
  1942. if ($i<2) {
  1943. if (!$headers) {
  1944. $headers = $this->conn->fetchPartHeader(
  1945. $this->folder, $this->msg_uid, true, $part->mime_id);
  1946. }
  1947. $filename_mime = '';
  1948. $i = 0; $matches = array();
  1949. while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1950. $filename_mime .= $matches[1];
  1951. $i++;
  1952. }
  1953. }
  1954. }
  1955. else if (!empty($part->ctype_parameters['name*0*'])) {
  1956. $i = 0;
  1957. while (isset($part->ctype_parameters['name*'.$i.'*'])) {
  1958. $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
  1959. $i++;
  1960. }
  1961. if ($i<2) {
  1962. if (!$headers) {
  1963. $headers = $this->conn->fetchPartHeader(
  1964. $this->folder, $this->msg_uid, true, $part->mime_id);
  1965. }
  1966. $filename_encoded = '';
  1967. $i = 0; $matches = array();
  1968. while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
  1969. $filename_encoded .= $matches[1];
  1970. $i++;
  1971. }
  1972. }
  1973. }
  1974. // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
  1975. else if (!empty($part->ctype_parameters['name'])) {
  1976. $filename_mime = $part->ctype_parameters['name'];
  1977. }
  1978. // Content-Disposition
  1979. else if (!empty($part->headers['content-description'])) {
  1980. $filename_mime = $part->headers['content-description'];
  1981. }
  1982. else {
  1983. return;
  1984. }
  1985. // decode filename
  1986. if (!empty($filename_mime)) {
  1987. if (!empty($part->charset)) {
  1988. $charset = $part->charset;
  1989. }
  1990. else if (!empty($this->struct_charset)) {
  1991. $charset = $this->struct_charset;
  1992. }
  1993. else {
  1994. $charset = rcube_charset::detect($filename_mime, $this->default_charset);
  1995. }
  1996. $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
  1997. }
  1998. else if (!empty($filename_encoded)) {
  1999. // decode filename according to RFC 2231, Section 4
  2000. if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
  2001. $filename_charset = $fmatches[1];
  2002. $filename_encoded = $fmatches[2];
  2003. }
  2004. $part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset);
  2005. }
  2006. }
  2007. /**
  2008. * Get charset name from message structure (first part)
  2009. *
  2010. * @param array $structure Message structure
  2011. *
  2012. * @return string Charset name
  2013. */
  2014. protected function structure_charset($structure)
  2015. {
  2016. while (is_array($structure)) {
  2017. if (is_array($structure[2]) && $structure[2][0] == 'charset') {
  2018. return $structure[2][1];
  2019. }
  2020. $structure = $structure[0];
  2021. }
  2022. }
  2023. /**
  2024. * Fetch message body of a specific message from the server
  2025. *
  2026. * @param int Message UID
  2027. * @param string Part number
  2028. * @param rcube_message_part Part object created by get_structure()
  2029. * @param mixed True to print part, resource to write part contents in
  2030. * @param resource File pointer to save the message part
  2031. * @param boolean Disables charset conversion
  2032. * @param int Only read this number of bytes
  2033. * @param boolean Enables formatting of text/* parts bodies
  2034. *
  2035. * @return string Message/part body if not printed
  2036. */
  2037. public function get_message_part($uid, $part = 1, $o_part = null, $print = null, $fp = null,
  2038. $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
  2039. {
  2040. if (!$this->check_connection()) {
  2041. return null;
  2042. }
  2043. // get part data if not provided
  2044. if (!is_object($o_part)) {
  2045. $structure = $this->conn->getStructure($this->folder, $uid, true);
  2046. $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
  2047. $o_part = new rcube_message_part;
  2048. $o_part->ctype_primary = $part_data['type'];
  2049. $o_part->encoding = $part_data['encoding'];
  2050. $o_part->charset = $part_data['charset'];
  2051. $o_part->size = $part_data['size'];
  2052. }
  2053. if ($o_part && $o_part->size) {
  2054. $formatted = $formatted && $o_part->ctype_primary == 'text';
  2055. $body = $this->conn->handlePartBody($this->folder, $uid, true,
  2056. $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
  2057. }
  2058. if ($fp || $print) {
  2059. return true;
  2060. }
  2061. // convert charset (if text or message part)
  2062. if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
  2063. // Remove NULL characters if any (#1486189)
  2064. if ($formatted && strpos($body, "\x00") !== false) {
  2065. $body = str_replace("\x00", '', $body);
  2066. }
  2067. if (!$skip_charset_conv) {
  2068. if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
  2069. // try to extract charset information from HTML meta tag (#1488125)
  2070. if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
  2071. $o_part->charset = strtoupper($m[1]);
  2072. }
  2073. else {
  2074. $o_part->charset = $this->default_charset;
  2075. }
  2076. }
  2077. $body = rcube_charset::convert($body, $o_part->charset);
  2078. }
  2079. }
  2080. return $body;
  2081. }
  2082. /**
  2083. * Returns the whole message source as string (or saves to a file)
  2084. *
  2085. * @param int $uid Message UID
  2086. * @param resource $fp File pointer to save the message
  2087. * @param string $part Optional message part ID
  2088. *
  2089. * @return string Message source string
  2090. */
  2091. public function get_raw_body($uid, $fp=null, $part = null)
  2092. {
  2093. if (!$this->check_connection()) {
  2094. return null;
  2095. }
  2096. return $this->conn->handlePartBody($this->folder, $uid,
  2097. true, $part, null, false, $fp);
  2098. }
  2099. /**
  2100. * Returns the message headers as string
  2101. *
  2102. * @param int $uid Message UID
  2103. * @param string $part Optional message part ID
  2104. *
  2105. * @return string Message headers string
  2106. */
  2107. public function get_raw_headers($uid, $part = null)
  2108. {
  2109. if (!$this->check_connection()) {
  2110. return null;
  2111. }
  2112. return $this->conn->fetchPartHeader($this->folder, $uid, true, $part);
  2113. }
  2114. /**
  2115. * Sends the whole message source to stdout
  2116. *
  2117. * @param int $uid Message UID
  2118. * @param bool $formatted Enables line-ending formatting
  2119. */
  2120. public function print_raw_body($uid, $formatted = true)
  2121. {
  2122. if (!$this->check_connection()) {
  2123. return;
  2124. }
  2125. $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
  2126. }
  2127. /**
  2128. * Set message flag to one or several messages
  2129. *
  2130. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2131. * @param string $flag Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
  2132. * @param string $folder Folder name
  2133. * @param boolean $skip_cache True to skip message cache clean up
  2134. *
  2135. * @return boolean Operation status
  2136. */
  2137. public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
  2138. {
  2139. if (!strlen($folder)) {
  2140. $folder = $this->folder;
  2141. }
  2142. if (!$this->check_connection()) {
  2143. return false;
  2144. }
  2145. $flag = strtoupper($flag);
  2146. list($uids, $all_mode) = $this->parse_uids($uids);
  2147. if (strpos($flag, 'UN') === 0) {
  2148. $result = $this->conn->unflag($folder, $uids, substr($flag, 2));
  2149. }
  2150. else {
  2151. $result = $this->conn->flag($folder, $uids, $flag);
  2152. }
  2153. if ($result && !$skip_cache) {
  2154. // reload message headers if cached
  2155. // update flags instead removing from cache
  2156. if ($mcache = $this->get_mcache_engine()) {
  2157. $status = strpos($flag, 'UN') !== 0;
  2158. $mflag = preg_replace('/^UN/', '', $flag);
  2159. $mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
  2160. $mflag, $status);
  2161. }
  2162. // clear cached counters
  2163. if ($flag == 'SEEN' || $flag == 'UNSEEN') {
  2164. $this->clear_messagecount($folder, array('SEEN', 'UNSEEN'));
  2165. }
  2166. else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
  2167. $this->clear_messagecount($folder, array('ALL', 'THREADS'));
  2168. if ($this->options['skip_deleted']) {
  2169. // remove cached messages
  2170. $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
  2171. }
  2172. }
  2173. $this->set_search_dirty($folder);
  2174. }
  2175. return $result;
  2176. }
  2177. /**
  2178. * Append a mail message (source) to a specific folder
  2179. *
  2180. * @param string $folder Target folder
  2181. * @param string|array $message The message source string or filename
  2182. * or array (of strings and file pointers)
  2183. * @param string $headers Headers string if $message contains only the body
  2184. * @param boolean $is_file True if $message is a filename
  2185. * @param array $flags Message flags
  2186. * @param mixed $date Message internal date
  2187. * @param bool $binary Enables BINARY append
  2188. *
  2189. * @return int|bool Appended message UID or True on success, False on error
  2190. */
  2191. public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null, $binary = false)
  2192. {
  2193. if (!strlen($folder)) {
  2194. $folder = $this->folder;
  2195. }
  2196. if (!$this->check_connection()) {
  2197. return false;
  2198. }
  2199. // make sure folder exists
  2200. if (!$this->folder_exists($folder)) {
  2201. return false;
  2202. }
  2203. $date = $this->date_format($date);
  2204. if ($is_file) {
  2205. $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date, $binary);
  2206. }
  2207. else {
  2208. $saved = $this->conn->append($folder, $message, $flags, $date, $binary);
  2209. }
  2210. if ($saved) {
  2211. // increase messagecount of the target folder
  2212. $this->set_messagecount($folder, 'ALL', 1);
  2213. $this->plugins->exec_hook('message_saved', array(
  2214. 'folder' => $folder,
  2215. 'message' => $message,
  2216. 'headers' => $headers,
  2217. 'is_file' => $is_file,
  2218. 'flags' => $flags,
  2219. 'date' => $date,
  2220. 'binary' => $binary,
  2221. 'result' => $saved,
  2222. ));
  2223. }
  2224. return $saved;
  2225. }
  2226. /**
  2227. * Move a message from one folder to another
  2228. *
  2229. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2230. * @param string $to_mbox Target folder
  2231. * @param string $from_mbox Source folder
  2232. *
  2233. * @return boolean True on success, False on error
  2234. */
  2235. public function move_message($uids, $to_mbox, $from_mbox='')
  2236. {
  2237. if (!strlen($from_mbox)) {
  2238. $from_mbox = $this->folder;
  2239. }
  2240. if ($to_mbox === $from_mbox) {
  2241. return false;
  2242. }
  2243. list($uids, $all_mode) = $this->parse_uids($uids);
  2244. // exit if no message uids are specified
  2245. if (empty($uids)) {
  2246. return false;
  2247. }
  2248. if (!$this->check_connection()) {
  2249. return false;
  2250. }
  2251. $config = rcube::get_instance()->config;
  2252. $to_trash = $to_mbox == $config->get('trash_mbox');
  2253. // flag messages as read before moving them
  2254. if ($to_trash && $config->get('read_when_deleted')) {
  2255. // don't flush cache (4th argument)
  2256. $this->set_flag($uids, 'SEEN', $from_mbox, true);
  2257. }
  2258. // move messages
  2259. $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
  2260. // when moving to Trash we make sure the folder exists
  2261. // as it's uncommon scenario we do this when MOVE fails, not before
  2262. if (!$moved && $to_trash && $this->get_response_code() == rcube_storage::TRYCREATE) {
  2263. if ($this->create_folder($to_mbox, true, 'trash')) {
  2264. $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
  2265. }
  2266. }
  2267. if ($moved) {
  2268. $this->clear_messagecount($from_mbox);
  2269. $this->clear_messagecount($to_mbox);
  2270. $this->set_search_dirty($from_mbox);
  2271. $this->set_search_dirty($to_mbox);
  2272. }
  2273. // moving failed
  2274. else if ($to_trash && $config->get('delete_always', false)) {
  2275. $moved = $this->delete_message($uids, $from_mbox);
  2276. }
  2277. if ($moved) {
  2278. // unset threads internal cache
  2279. unset($this->icache['threads']);
  2280. // remove message ids from search set
  2281. if ($this->search_set && $from_mbox == $this->folder) {
  2282. // threads are too complicated to just remove messages from set
  2283. if ($this->search_threads || $all_mode) {
  2284. $this->refresh_search();
  2285. }
  2286. else if (!$this->search_set->incomplete) {
  2287. $this->search_set->filter(explode(',', $uids), $this->folder);
  2288. }
  2289. }
  2290. // remove cached messages
  2291. // @TODO: do cache update instead of clearing it
  2292. $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
  2293. }
  2294. return $moved;
  2295. }
  2296. /**
  2297. * Copy a message from one folder to another
  2298. *
  2299. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2300. * @param string $to_mbox Target folder
  2301. * @param string $from_mbox Source folder
  2302. *
  2303. * @return boolean True on success, False on error
  2304. */
  2305. public function copy_message($uids, $to_mbox, $from_mbox='')
  2306. {
  2307. if (!strlen($from_mbox)) {
  2308. $from_mbox = $this->folder;
  2309. }
  2310. list($uids, $all_mode) = $this->parse_uids($uids);
  2311. // exit if no message uids are specified
  2312. if (empty($uids)) {
  2313. return false;
  2314. }
  2315. if (!$this->check_connection()) {
  2316. return false;
  2317. }
  2318. // copy messages
  2319. $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
  2320. if ($copied) {
  2321. $this->clear_messagecount($to_mbox);
  2322. }
  2323. return $copied;
  2324. }
  2325. /**
  2326. * Mark messages as deleted and expunge them
  2327. *
  2328. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2329. * @param string $folder Source folder
  2330. *
  2331. * @return boolean True on success, False on error
  2332. */
  2333. public function delete_message($uids, $folder='')
  2334. {
  2335. if (!strlen($folder)) {
  2336. $folder = $this->folder;
  2337. }
  2338. list($uids, $all_mode) = $this->parse_uids($uids);
  2339. // exit if no message uids are specified
  2340. if (empty($uids)) {
  2341. return false;
  2342. }
  2343. if (!$this->check_connection()) {
  2344. return false;
  2345. }
  2346. $deleted = $this->conn->flag($folder, $uids, 'DELETED');
  2347. if ($deleted) {
  2348. // send expunge command in order to have the deleted message
  2349. // really deleted from the folder
  2350. $this->expunge_message($uids, $folder, false);
  2351. $this->clear_messagecount($folder);
  2352. // unset threads internal cache
  2353. unset($this->icache['threads']);
  2354. $this->set_search_dirty($folder);
  2355. // remove message ids from search set
  2356. if ($this->search_set && $folder == $this->folder) {
  2357. // threads are too complicated to just remove messages from set
  2358. if ($this->search_threads || $all_mode) {
  2359. $this->refresh_search();
  2360. }
  2361. else if (!$this->search_set->incomplete) {
  2362. $this->search_set->filter(explode(',', $uids));
  2363. }
  2364. }
  2365. // remove cached messages
  2366. $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
  2367. }
  2368. return $deleted;
  2369. }
  2370. /**
  2371. * Send IMAP expunge command and clear cache
  2372. *
  2373. * @param mixed $uids Message UIDs as array or comma-separated string, or '*'
  2374. * @param string $folder Folder name
  2375. * @param boolean $clear_cache False if cache should not be cleared
  2376. *
  2377. * @return boolean True on success, False on failure
  2378. */
  2379. public function expunge_message($uids, $folder = null, $clear_cache = true)
  2380. {
  2381. if ($uids && $this->get_capability('UIDPLUS')) {
  2382. list($uids, $all_mode) = $this->parse_uids($uids);
  2383. }
  2384. else {
  2385. $uids = null;
  2386. }
  2387. if (!strlen($folder)) {
  2388. $folder = $this->folder;
  2389. }
  2390. if (!$this->check_connection()) {
  2391. return false;
  2392. }
  2393. // force folder selection and check if folder is writeable
  2394. // to prevent a situation when CLOSE is executed on closed
  2395. // or EXPUNGE on read-only folder
  2396. $result = $this->conn->select($folder);
  2397. if (!$result) {
  2398. return false;
  2399. }
  2400. if (!$this->conn->data['READ-WRITE']) {
  2401. $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
  2402. return false;
  2403. }
  2404. // CLOSE(+SELECT) should be faster than EXPUNGE
  2405. if (empty($uids) || $all_mode) {
  2406. $result = $this->conn->close();
  2407. }
  2408. else {
  2409. $result = $this->conn->expunge($folder, $uids);
  2410. }
  2411. if ($result && $clear_cache) {
  2412. $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
  2413. $this->clear_messagecount($folder);
  2414. }
  2415. return $result;
  2416. }
  2417. /* --------------------------------
  2418. * folder management
  2419. * --------------------------------*/
  2420. /**
  2421. * Public method for listing subscribed folders.
  2422. *
  2423. * @param string $root Optional root folder
  2424. * @param string $name Optional name pattern
  2425. * @param string $filter Optional filter
  2426. * @param string $rights Optional ACL requirements
  2427. * @param bool $skip_sort Enable to return unsorted list (for better performance)
  2428. *
  2429. * @return array List of folders
  2430. */
  2431. public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
  2432. {
  2433. $cache_key = $root.':'.$name;
  2434. if (!empty($filter)) {
  2435. $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
  2436. }
  2437. $cache_key .= ':'.$rights;
  2438. $cache_key = 'mailboxes.'.md5($cache_key);
  2439. // get cached folder list
  2440. $a_mboxes = $this->get_cache($cache_key);
  2441. if (is_array($a_mboxes)) {
  2442. return $a_mboxes;
  2443. }
  2444. // Give plugins a chance to provide a list of folders
  2445. $data = $this->plugins->exec_hook('storage_folders',
  2446. array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
  2447. if (isset($data['folders'])) {
  2448. $a_mboxes = $data['folders'];
  2449. }
  2450. else {
  2451. $a_mboxes = $this->list_folders_subscribed_direct($root, $name);
  2452. }
  2453. if (!is_array($a_mboxes)) {
  2454. return array();
  2455. }
  2456. // filter folders list according to rights requirements
  2457. if ($rights && $this->get_capability('ACL')) {
  2458. $a_mboxes = $this->filter_rights($a_mboxes, $rights);
  2459. }
  2460. // INBOX should always be available
  2461. if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
  2462. && (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
  2463. ) {
  2464. array_unshift($a_mboxes, 'INBOX');
  2465. }
  2466. // sort folders (always sort for cache)
  2467. if (!$skip_sort || $this->cache) {
  2468. $a_mboxes = $this->sort_folder_list($a_mboxes);
  2469. }
  2470. // write folders list to cache
  2471. $this->update_cache($cache_key, $a_mboxes);
  2472. return $a_mboxes;
  2473. }
  2474. /**
  2475. * Method for direct folders listing (LSUB)
  2476. *
  2477. * @param string $root Optional root folder
  2478. * @param string $name Optional name pattern
  2479. *
  2480. * @return array List of subscribed folders
  2481. * @see rcube_imap::list_folders_subscribed()
  2482. */
  2483. public function list_folders_subscribed_direct($root='', $name='*')
  2484. {
  2485. if (!$this->check_connection()) {
  2486. return null;
  2487. }
  2488. $config = rcube::get_instance()->config;
  2489. // Server supports LIST-EXTENDED, we can use selection options
  2490. // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
  2491. $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
  2492. if ($list_extended) {
  2493. // This will also set folder options, LSUB doesn't do that
  2494. $result = $this->conn->listMailboxes($root, $name,
  2495. NULL, array('SUBSCRIBED'));
  2496. }
  2497. else {
  2498. // retrieve list of folders from IMAP server using LSUB
  2499. $result = $this->conn->listSubscribed($root, $name);
  2500. }
  2501. if (!is_array($result)) {
  2502. return array();
  2503. }
  2504. // #1486796: some server configurations doesn't return folders in all namespaces
  2505. if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
  2506. $this->list_folders_update($result, ($list_extended ? 'ext-' : '') . 'subscribed');
  2507. }
  2508. // Remove hidden folders
  2509. if ($config->get('imap_skip_hidden_folders')) {
  2510. $result = array_filter($result, function($v) { return $v[0] != '.'; });
  2511. }
  2512. if ($list_extended) {
  2513. // unsubscribe non-existent folders, remove from the list
  2514. if ($name == '*' && !empty($this->conn->data['LIST'])) {
  2515. foreach ($result as $idx => $folder) {
  2516. if (($opts = $this->conn->data['LIST'][$folder])
  2517. && in_array_nocase('\\NonExistent', $opts)
  2518. ) {
  2519. $this->conn->unsubscribe($folder);
  2520. unset($result[$idx]);
  2521. }
  2522. }
  2523. }
  2524. }
  2525. else {
  2526. // unsubscribe non-existent folders, remove them from the list
  2527. if (!empty($result) && $name == '*') {
  2528. $existing = $this->list_folders($root, $name);
  2529. $nonexisting = array_diff($result, $existing);
  2530. $result = array_diff($result, $nonexisting);
  2531. foreach ($nonexisting as $folder) {
  2532. $this->conn->unsubscribe($folder);
  2533. }
  2534. }
  2535. }
  2536. return $result;
  2537. }
  2538. /**
  2539. * Get a list of all folders available on the server
  2540. *
  2541. * @param string $root IMAP root dir
  2542. * @param string $name Optional name pattern
  2543. * @param mixed $filter Optional filter
  2544. * @param string $rights Optional ACL requirements
  2545. * @param bool $skip_sort Enable to return unsorted list (for better performance)
  2546. *
  2547. * @return array Indexed array with folder names
  2548. */
  2549. public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
  2550. {
  2551. $cache_key = $root.':'.$name;
  2552. if (!empty($filter)) {
  2553. $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
  2554. }
  2555. $cache_key .= ':'.$rights;
  2556. $cache_key = 'mailboxes.list.'.md5($cache_key);
  2557. // get cached folder list
  2558. $a_mboxes = $this->get_cache($cache_key);
  2559. if (is_array($a_mboxes)) {
  2560. return $a_mboxes;
  2561. }
  2562. // Give plugins a chance to provide a list of folders
  2563. $data = $this->plugins->exec_hook('storage_folders',
  2564. array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
  2565. if (isset($data['folders'])) {
  2566. $a_mboxes = $data['folders'];
  2567. }
  2568. else {
  2569. // retrieve list of folders from IMAP server
  2570. $a_mboxes = $this->list_folders_direct($root, $name);
  2571. }
  2572. if (!is_array($a_mboxes)) {
  2573. $a_mboxes = array();
  2574. }
  2575. // INBOX should always be available
  2576. if (in_array_nocase($root . $name, array('*', '%', 'INBOX', 'INBOX*'))
  2577. && (!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)
  2578. ) {
  2579. array_unshift($a_mboxes, 'INBOX');
  2580. }
  2581. // cache folder attributes
  2582. if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
  2583. $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
  2584. }
  2585. // filter folders list according to rights requirements
  2586. if ($rights && $this->get_capability('ACL')) {
  2587. $a_mboxes = $this->filter_rights($a_mboxes, $rights);
  2588. }
  2589. // filter folders and sort them
  2590. if (!$skip_sort) {
  2591. $a_mboxes = $this->sort_folder_list($a_mboxes);
  2592. }
  2593. // write folders list to cache
  2594. $this->update_cache($cache_key, $a_mboxes);
  2595. return $a_mboxes;
  2596. }
  2597. /**
  2598. * Method for direct folders listing (LIST)
  2599. *
  2600. * @param string $root Optional root folder
  2601. * @param string $name Optional name pattern
  2602. *
  2603. * @return array List of folders
  2604. * @see rcube_imap::list_folders()
  2605. */
  2606. public function list_folders_direct($root='', $name='*')
  2607. {
  2608. if (!$this->check_connection()) {
  2609. return null;
  2610. }
  2611. $result = $this->conn->listMailboxes($root, $name);
  2612. if (!is_array($result)) {
  2613. return array();
  2614. }
  2615. $config = rcube::get_instance()->config;
  2616. // #1486796: some server configurations doesn't return folders in all namespaces
  2617. if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
  2618. $this->list_folders_update($result);
  2619. }
  2620. // Remove hidden folders
  2621. if ($config->get('imap_skip_hidden_folders')) {
  2622. $result = array_filter($result, function($v) { return $v[0] != '.'; });
  2623. }
  2624. return $result;
  2625. }
  2626. /**
  2627. * Fix folders list by adding folders from other namespaces.
  2628. * Needed on some servers eg. Courier IMAP
  2629. *
  2630. * @param array $result Reference to folders list
  2631. * @param string $type Listing type (ext-subscribed, subscribed or all)
  2632. */
  2633. protected function list_folders_update(&$result, $type = null)
  2634. {
  2635. $namespace = $this->get_namespace();
  2636. $search = array();
  2637. // build list of namespace prefixes
  2638. foreach ((array)$namespace as $ns) {
  2639. if (is_array($ns)) {
  2640. foreach ($ns as $ns_data) {
  2641. if (strlen($ns_data[0])) {
  2642. $search[] = $ns_data[0];
  2643. }
  2644. }
  2645. }
  2646. }
  2647. if (!empty($search)) {
  2648. // go through all folders detecting namespace usage
  2649. foreach ($result as $folder) {
  2650. foreach ($search as $idx => $prefix) {
  2651. if (strpos($folder, $prefix) === 0) {
  2652. unset($search[$idx]);
  2653. }
  2654. }
  2655. if (empty($search)) {
  2656. break;
  2657. }
  2658. }
  2659. // get folders in hidden namespaces and add to the result
  2660. foreach ($search as $prefix) {
  2661. if ($type == 'ext-subscribed') {
  2662. $list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED'));
  2663. }
  2664. else if ($type == 'subscribed') {
  2665. $list = $this->conn->listSubscribed('', $prefix . '*');
  2666. }
  2667. else {
  2668. $list = $this->conn->listMailboxes('', $prefix . '*');
  2669. }
  2670. if (!empty($list)) {
  2671. $result = array_merge($result, $list);
  2672. }
  2673. }
  2674. }
  2675. }
  2676. /**
  2677. * Filter the given list of folders according to access rights
  2678. *
  2679. * For performance reasons we assume user has full rights
  2680. * on all personal folders.
  2681. */
  2682. protected function filter_rights($a_folders, $rights)
  2683. {
  2684. $regex = '/('.$rights.')/';
  2685. foreach ($a_folders as $idx => $folder) {
  2686. if ($this->folder_namespace($folder) == 'personal') {
  2687. continue;
  2688. }
  2689. $myrights = join('', (array)$this->my_rights($folder));
  2690. if ($myrights !== null && !preg_match($regex, $myrights)) {
  2691. unset($a_folders[$idx]);
  2692. }
  2693. }
  2694. return $a_folders;
  2695. }
  2696. /**
  2697. * Get mailbox quota information
  2698. *
  2699. * @param string $folder Folder name
  2700. *
  2701. * @return mixed Quota info or False if not supported
  2702. */
  2703. public function get_quota($folder = null)
  2704. {
  2705. if ($this->get_capability('QUOTA') && $this->check_connection()) {
  2706. return $this->conn->getQuota($folder);
  2707. }
  2708. return false;
  2709. }
  2710. /**
  2711. * Get folder size (size of all messages in a folder)
  2712. *
  2713. * @param string $folder Folder name
  2714. *
  2715. * @return int Folder size in bytes, False on error
  2716. */
  2717. public function folder_size($folder)
  2718. {
  2719. if (!strlen($folder)) {
  2720. return false;
  2721. }
  2722. if (!$this->check_connection()) {
  2723. return 0;
  2724. }
  2725. // On Cyrus we can use special folder annotation, which should be much faster
  2726. if ($this->get_vendor() == 'cyrus') {
  2727. $idx = '/shared/vendor/cmu/cyrus-imapd/size';
  2728. $result = $this->get_metadata($folder, $idx, array(), true);
  2729. if (!empty($result) && is_numeric($result[$folder][$idx])) {
  2730. return $result[$folder][$idx];
  2731. }
  2732. }
  2733. // @TODO: could we try to use QUOTA here?
  2734. $result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
  2735. if (is_array($result)) {
  2736. $result = array_sum($result);
  2737. }
  2738. return $result;
  2739. }
  2740. /**
  2741. * Subscribe to a specific folder(s)
  2742. *
  2743. * @param array $folders Folder name(s)
  2744. *
  2745. * @return boolean True on success
  2746. */
  2747. public function subscribe($folders)
  2748. {
  2749. // let this common function do the main work
  2750. return $this->change_subscription($folders, 'subscribe');
  2751. }
  2752. /**
  2753. * Unsubscribe folder(s)
  2754. *
  2755. * @param array $a_mboxes Folder name(s)
  2756. *
  2757. * @return boolean True on success
  2758. */
  2759. public function unsubscribe($folders)
  2760. {
  2761. // let this common function do the main work
  2762. return $this->change_subscription($folders, 'unsubscribe');
  2763. }
  2764. /**
  2765. * Create a new folder on the server and register it in local cache
  2766. *
  2767. * @param string $folder New folder name
  2768. * @param boolean $subscribe True if the new folder should be subscribed
  2769. * @param string $type Optional folder type (junk, trash, drafts, sent, archive)
  2770. *
  2771. * @return boolean True on success
  2772. */
  2773. public function create_folder($folder, $subscribe = false, $type = null)
  2774. {
  2775. if (!$this->check_connection()) {
  2776. return false;
  2777. }
  2778. $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
  2779. // try to subscribe it
  2780. if ($result) {
  2781. // clear cache
  2782. $this->clear_cache('mailboxes', true);
  2783. if ($subscribe) {
  2784. $this->subscribe($folder);
  2785. }
  2786. }
  2787. return $result;
  2788. }
  2789. /**
  2790. * Set a new name to an existing folder
  2791. *
  2792. * @param string $folder Folder to rename
  2793. * @param string $new_name New folder name
  2794. *
  2795. * @return boolean True on success
  2796. */
  2797. public function rename_folder($folder, $new_name)
  2798. {
  2799. if (!strlen($new_name)) {
  2800. return false;
  2801. }
  2802. if (!$this->check_connection()) {
  2803. return false;
  2804. }
  2805. $delm = $this->get_hierarchy_delimiter();
  2806. // get list of subscribed folders
  2807. if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
  2808. $a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
  2809. $subscribed = $this->folder_exists($folder, true);
  2810. }
  2811. else {
  2812. $a_subscribed = $this->list_folders_subscribed();
  2813. $subscribed = in_array($folder, $a_subscribed);
  2814. }
  2815. $result = $this->conn->renameFolder($folder, $new_name);
  2816. if ($result) {
  2817. // unsubscribe the old folder, subscribe the new one
  2818. if ($subscribed) {
  2819. $this->conn->unsubscribe($folder);
  2820. $this->conn->subscribe($new_name);
  2821. }
  2822. // check if folder children are subscribed
  2823. foreach ($a_subscribed as $c_subscribed) {
  2824. if (strpos($c_subscribed, $folder.$delm) === 0) {
  2825. $this->conn->unsubscribe($c_subscribed);
  2826. $this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
  2827. $new_name, $c_subscribed));
  2828. // clear cache
  2829. $this->clear_message_cache($c_subscribed);
  2830. }
  2831. }
  2832. // clear cache
  2833. $this->clear_message_cache($folder);
  2834. $this->clear_cache('mailboxes', true);
  2835. }
  2836. return $result;
  2837. }
  2838. /**
  2839. * Remove folder (with subfolders) from the server
  2840. *
  2841. * @param string $folder Folder name
  2842. *
  2843. * @return boolean True on success, False on failure
  2844. */
  2845. function delete_folder($folder)
  2846. {
  2847. if (!$this->check_connection()) {
  2848. return false;
  2849. }
  2850. $delm = $this->get_hierarchy_delimiter();
  2851. // get list of sub-folders or all folders
  2852. // if folder name contains special characters
  2853. $path = strspn($folder, '%*') > 0 ? ($folder . $delm) : '';
  2854. $sub_mboxes = $this->list_folders('', $path . '*');
  2855. // According to RFC3501 deleting a \Noselect folder
  2856. // with subfolders may fail. To workaround this we delete
  2857. // subfolders first (in reverse order) (#5466)
  2858. if (!empty($sub_mboxes)) {
  2859. foreach (array_reverse($sub_mboxes) as $mbox) {
  2860. if (strpos($mbox, $folder . $delm) === 0) {
  2861. if ($this->conn->deleteFolder($mbox)) {
  2862. $this->conn->unsubscribe($mbox);
  2863. $this->clear_message_cache($mbox);
  2864. }
  2865. }
  2866. }
  2867. }
  2868. // delete the folder
  2869. if ($result = $this->conn->deleteFolder($folder)) {
  2870. // and unsubscribe it
  2871. $this->conn->unsubscribe($folder);
  2872. $this->clear_message_cache($folder);
  2873. }
  2874. $this->clear_cache('mailboxes', true);
  2875. return $result;
  2876. }
  2877. /**
  2878. * Detect special folder associations stored in storage backend
  2879. */
  2880. public function get_special_folders($forced = false)
  2881. {
  2882. $result = parent::get_special_folders();
  2883. $rcube = rcube::get_instance();
  2884. // Lock SPECIAL-USE after user preferences change (#4782)
  2885. if ($rcube->config->get('lock_special_folders')) {
  2886. return $result;
  2887. }
  2888. if (isset($this->icache['special-use'])) {
  2889. return array_merge($result, $this->icache['special-use']);
  2890. }
  2891. if (!$forced || !$this->get_capability('SPECIAL-USE')) {
  2892. return $result;
  2893. }
  2894. if (!$this->check_connection()) {
  2895. return $result;
  2896. }
  2897. $types = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
  2898. $special = array();
  2899. // request \Subscribed flag in LIST response as performance improvement for folder_exists()
  2900. $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
  2901. if (!empty($folders)) {
  2902. foreach ($folders as $folder) {
  2903. if ($flags = $this->conn->data['LIST'][$folder]) {
  2904. foreach ($types as $type) {
  2905. if (in_array($type, $flags)) {
  2906. $type = strtolower(substr($type, 1));
  2907. $special[$type] = $folder;
  2908. }
  2909. }
  2910. }
  2911. }
  2912. }
  2913. $this->icache['special-use'] = $special;
  2914. unset($this->icache['special-folders']);
  2915. return array_merge($result, $special);
  2916. }
  2917. /**
  2918. * Set special folder associations stored in storage backend
  2919. */
  2920. public function set_special_folders($specials)
  2921. {
  2922. if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
  2923. return false;
  2924. }
  2925. if (!$this->check_connection()) {
  2926. return false;
  2927. }
  2928. $folders = $this->get_special_folders(true);
  2929. $old = (array) $this->icache['special-use'];
  2930. foreach ($specials as $type => $folder) {
  2931. if (in_array($type, rcube_storage::$folder_types)) {
  2932. $old_folder = $old[$type];
  2933. if ($old_folder !== $folder) {
  2934. // unset old-folder metadata
  2935. if ($old_folder !== null) {
  2936. $this->delete_metadata($old_folder, array('/private/specialuse'));
  2937. }
  2938. // set new folder metadata
  2939. if ($folder) {
  2940. $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
  2941. }
  2942. }
  2943. }
  2944. }
  2945. $this->icache['special-use'] = $specials;
  2946. unset($this->icache['special-folders']);
  2947. return true;
  2948. }
  2949. /**
  2950. * Checks if folder exists and is subscribed
  2951. *
  2952. * @param string $folder Folder name
  2953. * @param boolean $subscription Enable subscription checking
  2954. *
  2955. * @return boolean TRUE or FALSE
  2956. */
  2957. public function folder_exists($folder, $subscription = false)
  2958. {
  2959. if ($folder == 'INBOX') {
  2960. return true;
  2961. }
  2962. $key = $subscription ? 'subscribed' : 'existing';
  2963. if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
  2964. return true;
  2965. }
  2966. if (!$this->check_connection()) {
  2967. return false;
  2968. }
  2969. if ($subscription) {
  2970. // It's possible we already called LIST command, check LIST data
  2971. if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
  2972. && in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
  2973. ) {
  2974. $a_folders = array($folder);
  2975. }
  2976. else {
  2977. $a_folders = $this->conn->listSubscribed('', $folder);
  2978. }
  2979. }
  2980. else {
  2981. // It's possible we already called LIST command, check LIST data
  2982. if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
  2983. $a_folders = array($folder);
  2984. }
  2985. else {
  2986. $a_folders = $this->conn->listMailboxes('', $folder);
  2987. }
  2988. }
  2989. if (is_array($a_folders) && in_array($folder, $a_folders)) {
  2990. $this->icache[$key][] = $folder;
  2991. return true;
  2992. }
  2993. return false;
  2994. }
  2995. /**
  2996. * Returns the namespace where the folder is in
  2997. *
  2998. * @param string $folder Folder name
  2999. *
  3000. * @return string One of 'personal', 'other' or 'shared'
  3001. */
  3002. public function folder_namespace($folder)
  3003. {
  3004. if ($folder == 'INBOX') {
  3005. return 'personal';
  3006. }
  3007. foreach ($this->namespace as $type => $namespace) {
  3008. if (is_array($namespace)) {
  3009. foreach ($namespace as $ns) {
  3010. if ($len = strlen($ns[0])) {
  3011. if (($len > 1 && $folder == substr($ns[0], 0, -1))
  3012. || strpos($folder, $ns[0]) === 0
  3013. ) {
  3014. return $type;
  3015. }
  3016. }
  3017. }
  3018. }
  3019. }
  3020. return 'personal';
  3021. }
  3022. /**
  3023. * Modify folder name according to personal namespace prefix.
  3024. * For output it removes prefix of the personal namespace if it's possible.
  3025. * For input it adds the prefix. Use it before creating a folder in root
  3026. * of the folders tree.
  3027. *
  3028. * @param string $folder Folder name
  3029. * @param string $mode Mode name (out/in)
  3030. *
  3031. * @return string Folder name
  3032. */
  3033. public function mod_folder($folder, $mode = 'out')
  3034. {
  3035. $prefix = $this->namespace['prefix_' . $mode]; // see set_env()
  3036. if ($prefix === null || $prefix === ''
  3037. || !($prefix_len = strlen($prefix)) || !strlen($folder)
  3038. ) {
  3039. return $folder;
  3040. }
  3041. // remove prefix for output
  3042. if ($mode == 'out') {
  3043. if (substr($folder, 0, $prefix_len) === $prefix) {
  3044. return substr($folder, $prefix_len);
  3045. }
  3046. return $folder;
  3047. }
  3048. // add prefix for input (e.g. folder creation)
  3049. return $prefix . $folder;
  3050. }
  3051. /**
  3052. * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
  3053. *
  3054. * @param string $folder Folder name
  3055. * @param bool $force Set to True if attributes should be refreshed
  3056. *
  3057. * @return array Options list
  3058. */
  3059. public function folder_attributes($folder, $force=false)
  3060. {
  3061. // get attributes directly from LIST command
  3062. if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
  3063. $opts = $this->conn->data['LIST'][$folder];
  3064. }
  3065. // get cached folder attributes
  3066. else if (!$force) {
  3067. $opts = $this->get_cache('mailboxes.attributes');
  3068. $opts = $opts[$folder];
  3069. }
  3070. if (!is_array($opts)) {
  3071. if (!$this->check_connection()) {
  3072. return array();
  3073. }
  3074. $this->conn->listMailboxes('', $folder);
  3075. $opts = $this->conn->data['LIST'][$folder];
  3076. }
  3077. return is_array($opts) ? $opts : array();
  3078. }
  3079. /**
  3080. * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
  3081. * PERMANENTFLAGS, UIDNEXT, UNSEEN
  3082. *
  3083. * @param string $folder Folder name
  3084. *
  3085. * @return array Data
  3086. */
  3087. public function folder_data($folder)
  3088. {
  3089. if (!strlen($folder)) {
  3090. $folder = $this->folder !== null ? $this->folder : 'INBOX';
  3091. }
  3092. if ($this->conn->selected != $folder) {
  3093. if (!$this->check_connection()) {
  3094. return array();
  3095. }
  3096. if ($this->conn->select($folder)) {
  3097. $this->folder = $folder;
  3098. }
  3099. else {
  3100. return null;
  3101. }
  3102. }
  3103. $data = $this->conn->data;
  3104. // add (E)SEARCH result for ALL UNDELETED query
  3105. if (!empty($this->icache['undeleted_idx'])
  3106. && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
  3107. ) {
  3108. $data['UNDELETED'] = $this->icache['undeleted_idx'];
  3109. }
  3110. return $data;
  3111. }
  3112. /**
  3113. * Returns extended information about the folder
  3114. *
  3115. * @param string $folder Folder name
  3116. *
  3117. * @return array Data
  3118. */
  3119. public function folder_info($folder)
  3120. {
  3121. if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
  3122. return $this->icache['options'];
  3123. }
  3124. // get cached metadata
  3125. $cache_key = 'mailboxes.folder-info.' . $folder;
  3126. $cached = $this->get_cache($cache_key);
  3127. if (is_array($cached)) {
  3128. return $cached;
  3129. }
  3130. $acl = $this->get_capability('ACL');
  3131. $namespace = $this->get_namespace();
  3132. $options = array();
  3133. // check if the folder is a namespace prefix
  3134. if (!empty($namespace)) {
  3135. $mbox = $folder . $this->delimiter;
  3136. foreach ($namespace as $ns) {
  3137. if (!empty($ns)) {
  3138. foreach ($ns as $item) {
  3139. if ($item[0] === $mbox) {
  3140. $options['is_root'] = true;
  3141. break 2;
  3142. }
  3143. }
  3144. }
  3145. }
  3146. }
  3147. // check if the folder is other user virtual-root
  3148. if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
  3149. $parts = explode($this->delimiter, $folder);
  3150. if (count($parts) == 2) {
  3151. $mbox = $parts[0] . $this->delimiter;
  3152. foreach ($namespace['other'] as $item) {
  3153. if ($item[0] === $mbox) {
  3154. $options['is_root'] = true;
  3155. break;
  3156. }
  3157. }
  3158. }
  3159. }
  3160. $options['name'] = $folder;
  3161. $options['attributes'] = $this->folder_attributes($folder, true);
  3162. $options['namespace'] = $this->folder_namespace($folder);
  3163. $options['special'] = $this->is_special_folder($folder);
  3164. // Set 'noselect' flag
  3165. if (is_array($options['attributes'])) {
  3166. foreach ($options['attributes'] as $attrib) {
  3167. $attrib = strtolower($attrib);
  3168. if ($attrib == '\noselect' || $attrib == '\nonexistent') {
  3169. $options['noselect'] = true;
  3170. }
  3171. }
  3172. }
  3173. else {
  3174. $options['noselect'] = true;
  3175. }
  3176. // Get folder rights (MYRIGHTS)
  3177. if ($acl && ($rights = $this->my_rights($folder))) {
  3178. $options['rights'] = $rights;
  3179. }
  3180. // Set 'norename' flag
  3181. if (!empty($options['rights'])) {
  3182. $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
  3183. if (!$options['noselect']) {
  3184. $options['noselect'] = !in_array('r', $options['rights']);
  3185. }
  3186. }
  3187. else {
  3188. $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
  3189. }
  3190. // update caches
  3191. $this->icache['options'] = $options;
  3192. $this->update_cache($cache_key, $options);
  3193. return $options;
  3194. }
  3195. /**
  3196. * Synchronizes messages cache.
  3197. *
  3198. * @param string $folder Folder name
  3199. */
  3200. public function folder_sync($folder)
  3201. {
  3202. if ($mcache = $this->get_mcache_engine()) {
  3203. $mcache->synchronize($folder);
  3204. }
  3205. }
  3206. /**
  3207. * Get message header names for rcube_imap_generic::fetchHeader(s)
  3208. *
  3209. * @return string Space-separated list of header names
  3210. */
  3211. protected function get_fetch_headers()
  3212. {
  3213. if (!empty($this->options['fetch_headers'])) {
  3214. $headers = explode(' ', $this->options['fetch_headers']);
  3215. }
  3216. else {
  3217. $headers = array();
  3218. }
  3219. if ($this->messages_caching || $this->options['all_headers']) {
  3220. $headers = array_merge($headers, $this->all_headers);
  3221. }
  3222. return $headers;
  3223. }
  3224. /* -----------------------------------------
  3225. * ACL and METADATA/ANNOTATEMORE methods
  3226. * ----------------------------------------*/
  3227. /**
  3228. * Changes the ACL on the specified folder (SETACL)
  3229. *
  3230. * @param string $folder Folder name
  3231. * @param string $user User name
  3232. * @param string $acl ACL string
  3233. *
  3234. * @return boolean True on success, False on failure
  3235. * @since 0.5-beta
  3236. */
  3237. public function set_acl($folder, $user, $acl)
  3238. {
  3239. if (!$this->get_capability('ACL')) {
  3240. return false;
  3241. }
  3242. if (!$this->check_connection()) {
  3243. return false;
  3244. }
  3245. $this->clear_cache('mailboxes.folder-info.' . $folder);
  3246. return $this->conn->setACL($folder, $user, $acl);
  3247. }
  3248. /**
  3249. * Removes any <identifier,rights> pair for the
  3250. * specified user from the ACL for the specified
  3251. * folder (DELETEACL)
  3252. *
  3253. * @param string $folder Folder name
  3254. * @param string $user User name
  3255. *
  3256. * @return boolean True on success, False on failure
  3257. * @since 0.5-beta
  3258. */
  3259. public function delete_acl($folder, $user)
  3260. {
  3261. if (!$this->get_capability('ACL')) {
  3262. return false;
  3263. }
  3264. if (!$this->check_connection()) {
  3265. return false;
  3266. }
  3267. return $this->conn->deleteACL($folder, $user);
  3268. }
  3269. /**
  3270. * Returns the access control list for folder (GETACL)
  3271. *
  3272. * @param string $folder Folder name
  3273. *
  3274. * @return array User-rights array on success, NULL on error
  3275. * @since 0.5-beta
  3276. */
  3277. public function get_acl($folder)
  3278. {
  3279. if (!$this->get_capability('ACL')) {
  3280. return null;
  3281. }
  3282. if (!$this->check_connection()) {
  3283. return null;
  3284. }
  3285. return $this->conn->getACL($folder);
  3286. }
  3287. /**
  3288. * Returns information about what rights can be granted to the
  3289. * user (identifier) in the ACL for the folder (LISTRIGHTS)
  3290. *
  3291. * @param string $folder Folder name
  3292. * @param string $user User name
  3293. *
  3294. * @return array List of user rights
  3295. * @since 0.5-beta
  3296. */
  3297. public function list_rights($folder, $user)
  3298. {
  3299. if (!$this->get_capability('ACL')) {
  3300. return null;
  3301. }
  3302. if (!$this->check_connection()) {
  3303. return null;
  3304. }
  3305. return $this->conn->listRights($folder, $user);
  3306. }
  3307. /**
  3308. * Returns the set of rights that the current user has to
  3309. * folder (MYRIGHTS)
  3310. *
  3311. * @param string $folder Folder name
  3312. *
  3313. * @return array MYRIGHTS response on success, NULL on error
  3314. * @since 0.5-beta
  3315. */
  3316. public function my_rights($folder)
  3317. {
  3318. if (!$this->get_capability('ACL')) {
  3319. return null;
  3320. }
  3321. if (!$this->check_connection()) {
  3322. return null;
  3323. }
  3324. return $this->conn->myRights($folder);
  3325. }
  3326. /**
  3327. * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
  3328. *
  3329. * @param string $folder Folder name (empty for server metadata)
  3330. * @param array $entries Entry-value array (use NULL value as NIL)
  3331. *
  3332. * @return boolean True on success, False on failure
  3333. * @since 0.5-beta
  3334. */
  3335. public function set_metadata($folder, $entries)
  3336. {
  3337. if (!$this->check_connection()) {
  3338. return false;
  3339. }
  3340. $this->clear_cache('mailboxes.metadata.', true);
  3341. if ($this->get_capability('METADATA') ||
  3342. (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
  3343. ) {
  3344. return $this->conn->setMetadata($folder, $entries);
  3345. }
  3346. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3347. foreach ((array)$entries as $entry => $value) {
  3348. list($ent, $attr) = $this->md2annotate($entry);
  3349. $entries[$entry] = array($ent, $attr, $value);
  3350. }
  3351. return $this->conn->setAnnotation($folder, $entries);
  3352. }
  3353. return false;
  3354. }
  3355. /**
  3356. * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
  3357. *
  3358. * @param string $folder Folder name (empty for server metadata)
  3359. * @param array $entries Entry names array
  3360. *
  3361. * @return boolean True on success, False on failure
  3362. * @since 0.5-beta
  3363. */
  3364. public function delete_metadata($folder, $entries)
  3365. {
  3366. if (!$this->check_connection()) {
  3367. return false;
  3368. }
  3369. $this->clear_cache('mailboxes.metadata.', true);
  3370. if ($this->get_capability('METADATA') ||
  3371. (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
  3372. ) {
  3373. return $this->conn->deleteMetadata($folder, $entries);
  3374. }
  3375. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3376. foreach ((array)$entries as $idx => $entry) {
  3377. list($ent, $attr) = $this->md2annotate($entry);
  3378. $entries[$idx] = array($ent, $attr, NULL);
  3379. }
  3380. return $this->conn->setAnnotation($folder, $entries);
  3381. }
  3382. return false;
  3383. }
  3384. /**
  3385. * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
  3386. *
  3387. * @param string $folder Folder name (empty for server metadata)
  3388. * @param array $entries Entries
  3389. * @param array $options Command options (with MAXSIZE and DEPTH keys)
  3390. * @param bool $force Disables cache use
  3391. *
  3392. * @return array Metadata entry-value hash array on success, NULL on error
  3393. * @since 0.5-beta
  3394. */
  3395. public function get_metadata($folder, $entries, $options = array(), $force = false)
  3396. {
  3397. $entries = (array) $entries;
  3398. if (!$force) {
  3399. // create cache key
  3400. // @TODO: this is the simplest solution, but we do the same with folders list
  3401. // maybe we should store data per-entry and merge on request
  3402. sort($options);
  3403. sort($entries);
  3404. $cache_key = 'mailboxes.metadata.' . $folder;
  3405. $cache_key .= '.' . md5(serialize($options).serialize($entries));
  3406. // get cached data
  3407. $cached_data = $this->get_cache($cache_key);
  3408. if (is_array($cached_data)) {
  3409. return $cached_data;
  3410. }
  3411. }
  3412. if (!$this->check_connection()) {
  3413. return null;
  3414. }
  3415. if ($this->get_capability('METADATA') ||
  3416. (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
  3417. ) {
  3418. $res = $this->conn->getMetadata($folder, $entries, $options);
  3419. }
  3420. else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
  3421. $queries = array();
  3422. $res = array();
  3423. // Convert entry names
  3424. foreach ($entries as $entry) {
  3425. list($ent, $attr) = $this->md2annotate($entry);
  3426. $queries[$attr][] = $ent;
  3427. }
  3428. // @TODO: Honor MAXSIZE and DEPTH options
  3429. foreach ($queries as $attrib => $entry) {
  3430. $result = $this->conn->getAnnotation($folder, $entry, $attrib);
  3431. // an error, invalidate any previous getAnnotation() results
  3432. if (!is_array($result)) {
  3433. return null;
  3434. }
  3435. else {
  3436. foreach ($result as $fldr => $data) {
  3437. $res[$fldr] = array_merge((array) $res[$fldr], $data);
  3438. }
  3439. }
  3440. }
  3441. }
  3442. if (isset($res)) {
  3443. if (!$force) {
  3444. $this->update_cache($cache_key, $res);
  3445. }
  3446. return $res;
  3447. }
  3448. }
  3449. /**
  3450. * Converts the METADATA extension entry name into the correct
  3451. * entry-attrib names for older ANNOTATEMORE version.
  3452. *
  3453. * @param string $entry Entry name
  3454. *
  3455. * @return array Entry-attribute list, NULL if not supported (?)
  3456. */
  3457. protected function md2annotate($entry)
  3458. {
  3459. if (substr($entry, 0, 7) == '/shared') {
  3460. return array(substr($entry, 7), 'value.shared');
  3461. }
  3462. else if (substr($entry, 0, 8) == '/private') {
  3463. return array(substr($entry, 8), 'value.priv');
  3464. }
  3465. // @TODO: log error
  3466. }
  3467. /* --------------------------------
  3468. * internal caching methods
  3469. * --------------------------------*/
  3470. /**
  3471. * Enable or disable indexes caching
  3472. *
  3473. * @param string $type Cache type (@see rcube::get_cache)
  3474. */
  3475. public function set_caching($type)
  3476. {
  3477. if ($type) {
  3478. $this->caching = $type;
  3479. }
  3480. else {
  3481. if ($this->cache) {
  3482. $this->cache->close();
  3483. }
  3484. $this->cache = null;
  3485. $this->caching = false;
  3486. }
  3487. }
  3488. /**
  3489. * Getter for IMAP cache object
  3490. */
  3491. protected function get_cache_engine()
  3492. {
  3493. if ($this->caching && !$this->cache) {
  3494. $rcube = rcube::get_instance();
  3495. $ttl = $rcube->config->get('imap_cache_ttl', '10d');
  3496. $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
  3497. }
  3498. return $this->cache;
  3499. }
  3500. /**
  3501. * Returns cached value
  3502. *
  3503. * @param string $key Cache key
  3504. *
  3505. * @return mixed
  3506. */
  3507. public function get_cache($key)
  3508. {
  3509. if ($cache = $this->get_cache_engine()) {
  3510. return $cache->get($key);
  3511. }
  3512. }
  3513. /**
  3514. * Update cache
  3515. *
  3516. * @param string $key Cache key
  3517. * @param mixed $data Data
  3518. */
  3519. public function update_cache($key, $data)
  3520. {
  3521. if ($cache = $this->get_cache_engine()) {
  3522. $cache->set($key, $data);
  3523. }
  3524. }
  3525. /**
  3526. * Clears the cache.
  3527. *
  3528. * @param string $key Cache key name or pattern
  3529. * @param boolean $prefix_mode Enable it to clear all keys starting
  3530. * with prefix specified in $key
  3531. */
  3532. public function clear_cache($key = null, $prefix_mode = false)
  3533. {
  3534. if ($cache = $this->get_cache_engine()) {
  3535. $cache->remove($key, $prefix_mode);
  3536. }
  3537. }
  3538. /* --------------------------------
  3539. * message caching methods
  3540. * --------------------------------*/
  3541. /**
  3542. * Enable or disable messages caching
  3543. *
  3544. * @param boolean $set Flag
  3545. * @param int $mode Cache mode
  3546. */
  3547. public function set_messages_caching($set, $mode = null)
  3548. {
  3549. if ($set) {
  3550. $this->messages_caching = true;
  3551. if ($mode && ($cache = $this->get_mcache_engine())) {
  3552. $cache->set_mode($mode);
  3553. }
  3554. }
  3555. else {
  3556. if ($this->mcache) {
  3557. $this->mcache->close();
  3558. }
  3559. $this->mcache = null;
  3560. $this->messages_caching = false;
  3561. }
  3562. }
  3563. /**
  3564. * Getter for messages cache object
  3565. */
  3566. protected function get_mcache_engine()
  3567. {
  3568. if ($this->messages_caching && !$this->mcache) {
  3569. $rcube = rcube::get_instance();
  3570. if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
  3571. $ttl = $rcube->config->get('messages_cache_ttl', '10d');
  3572. $threshold = $rcube->config->get('messages_cache_threshold', 50);
  3573. $this->mcache = new rcube_imap_cache(
  3574. $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
  3575. }
  3576. }
  3577. return $this->mcache;
  3578. }
  3579. /**
  3580. * Clears the messages cache.
  3581. *
  3582. * @param string $folder Folder name
  3583. * @param array $uids Optional message UIDs to remove from cache
  3584. */
  3585. protected function clear_message_cache($folder = null, $uids = null)
  3586. {
  3587. if ($mcache = $this->get_mcache_engine()) {
  3588. $mcache->clear($folder, $uids);
  3589. }
  3590. }
  3591. /**
  3592. * Delete outdated cache entries
  3593. */
  3594. function cache_gc()
  3595. {
  3596. rcube_imap_cache::gc();
  3597. }
  3598. /* --------------------------------
  3599. * protected methods
  3600. * --------------------------------*/
  3601. /**
  3602. * Validate the given input and save to local properties
  3603. *
  3604. * @param string $sort_field Sort column
  3605. * @param string $sort_order Sort order
  3606. */
  3607. protected function set_sort_order($sort_field, $sort_order)
  3608. {
  3609. if ($sort_field != null) {
  3610. $this->sort_field = asciiwords($sort_field);
  3611. }
  3612. if ($sort_order != null) {
  3613. $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
  3614. }
  3615. }
  3616. /**
  3617. * Sort folders first by default folders and then in alphabethical order
  3618. *
  3619. * @param array $a_folders Folders list
  3620. * @param bool $skip_default Skip default folders handling
  3621. *
  3622. * @return array Sorted list
  3623. */
  3624. public function sort_folder_list($a_folders, $skip_default = false)
  3625. {
  3626. $specials = array_merge(array('INBOX'), array_values($this->get_special_folders()));
  3627. $folders = array();
  3628. // convert names to UTF-8
  3629. foreach ($a_folders as $folder) {
  3630. // for better performance skip encoding conversion
  3631. // if the string does not look like UTF7-IMAP
  3632. $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
  3633. }
  3634. // sort folders
  3635. // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
  3636. uasort($folders, array($this, 'sort_folder_comparator'));
  3637. $folders = array_keys($folders);
  3638. if ($skip_default) {
  3639. return $folders;
  3640. }
  3641. // force the type of folder name variable (#1485527)
  3642. $folders = array_map('strval', $folders);
  3643. $out = array();
  3644. // finally we must put special folders on top and rebuild the list
  3645. // to move their subfolders where they belong...
  3646. $specials = array_unique(array_intersect($specials, $folders));
  3647. $folders = array_merge($specials, array_diff($folders, $specials));
  3648. $this->sort_folder_specials(null, $folders, $specials, $out);
  3649. return $out;
  3650. }
  3651. /**
  3652. * Recursive function to put subfolders of special folders in place
  3653. */
  3654. protected function sort_folder_specials($folder, &$list, &$specials, &$out)
  3655. {
  3656. foreach ($list as $key => $name) {
  3657. if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
  3658. $out[] = $name;
  3659. unset($list[$key]);
  3660. if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
  3661. unset($specials[$found]);
  3662. $this->sort_folder_specials($name, $list, $specials, $out);
  3663. }
  3664. }
  3665. }
  3666. reset($list);
  3667. }
  3668. /**
  3669. * Callback for uasort() that implements correct
  3670. * locale-aware case-sensitive sorting
  3671. */
  3672. protected function sort_folder_comparator($str1, $str2)
  3673. {
  3674. if ($this->sort_folder_collator === null) {
  3675. $this->sort_folder_collator = false;
  3676. // strcoll() does not work with UTF8 locale on Windows,
  3677. // use Collator from the intl extension
  3678. if (stripos(PHP_OS, 'win') === 0 && function_exists('collator_compare')) {
  3679. $locale = $this->options['language'] ?: 'en_US';
  3680. $this->sort_folder_collator = collator_create($locale) ?: false;
  3681. }
  3682. }
  3683. $path1 = explode($this->delimiter, $str1);
  3684. $path2 = explode($this->delimiter, $str2);
  3685. foreach ($path1 as $idx => $folder1) {
  3686. $folder2 = $path2[$idx];
  3687. if ($folder1 === $folder2) {
  3688. continue;
  3689. }
  3690. if ($this->sort_folder_collator) {
  3691. return collator_compare($this->sort_folder_collator, $folder1, $folder2);
  3692. }
  3693. return strcoll($folder1, $folder2);
  3694. }
  3695. }
  3696. /**
  3697. * Find UID of the specified message sequence ID
  3698. *
  3699. * @param int $id Message (sequence) ID
  3700. * @param string $folder Folder name
  3701. *
  3702. * @return int Message UID
  3703. */
  3704. public function id2uid($id, $folder = null)
  3705. {
  3706. if (!strlen($folder)) {
  3707. $folder = $this->folder;
  3708. }
  3709. if (!$this->check_connection()) {
  3710. return null;
  3711. }
  3712. return $this->conn->ID2UID($folder, $id);
  3713. }
  3714. /**
  3715. * Subscribe/unsubscribe a list of folders and update local cache
  3716. */
  3717. protected function change_subscription($folders, $mode)
  3718. {
  3719. $updated = 0;
  3720. $folders = (array) $folders;
  3721. if (!empty($folders)) {
  3722. if (!$this->check_connection()) {
  3723. return false;
  3724. }
  3725. foreach ($folders as $folder) {
  3726. $updated += (int) $this->conn->{$mode}($folder);
  3727. }
  3728. }
  3729. // clear cached folders list(s)
  3730. if ($updated) {
  3731. $this->clear_cache('mailboxes', true);
  3732. }
  3733. return $updated == count($folders);
  3734. }
  3735. /**
  3736. * Increde/decrese messagecount for a specific folder
  3737. */
  3738. protected function set_messagecount($folder, $mode, $increment)
  3739. {
  3740. if (!is_numeric($increment)) {
  3741. return false;
  3742. }
  3743. $mode = strtoupper($mode);
  3744. $a_folder_cache = $this->get_cache('messagecount');
  3745. if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
  3746. return false;
  3747. }
  3748. // add incremental value to messagecount
  3749. $a_folder_cache[$folder][$mode] += $increment;
  3750. // there's something wrong, delete from cache
  3751. if ($a_folder_cache[$folder][$mode] < 0) {
  3752. unset($a_folder_cache[$folder][$mode]);
  3753. }
  3754. // write back to cache
  3755. $this->update_cache('messagecount', $a_folder_cache);
  3756. return true;
  3757. }
  3758. /**
  3759. * Remove messagecount of a specific folder from cache
  3760. */
  3761. protected function clear_messagecount($folder, $mode = array())
  3762. {
  3763. $a_folder_cache = $this->get_cache('messagecount');
  3764. if (is_array($a_folder_cache[$folder])) {
  3765. if (!empty($mode)) {
  3766. foreach ((array) $mode as $key) {
  3767. unset($a_folder_cache[$folder][$key]);
  3768. }
  3769. }
  3770. else {
  3771. unset($a_folder_cache[$folder]);
  3772. }
  3773. $this->update_cache('messagecount', $a_folder_cache);
  3774. }
  3775. }
  3776. /**
  3777. * Converts date string/object into IMAP date/time format
  3778. */
  3779. protected function date_format($date)
  3780. {
  3781. if (empty($date)) {
  3782. return null;
  3783. }
  3784. if (!is_object($date) || !is_a($date, 'DateTime')) {
  3785. try {
  3786. $timestamp = rcube_utils::strtotime($date);
  3787. $date = new DateTime("@".$timestamp);
  3788. }
  3789. catch (Exception $e) {
  3790. return null;
  3791. }
  3792. }
  3793. return $date->format('d-M-Y H:i:s O');
  3794. }
  3795. /**
  3796. * This is our own debug handler for the IMAP connection
  3797. */
  3798. public function debug_handler(&$imap, $message)
  3799. {
  3800. rcube::write_log('imap', $message);
  3801. }
  3802. }