rcube_imap_generic.php 130 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2005-2015, 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. | Provide alternative IMAP library that doesn't rely on the standard |
  14. | C-Client based version. This allows to function regardless |
  15. | of whether or not the PHP build it's running on has IMAP |
  16. | functionality built-in. |
  17. | |
  18. | Based on Iloha IMAP Library. See http://ilohamail.org/ for details |
  19. +-----------------------------------------------------------------------+
  20. | Author: Aleksander Machniak <alec@alec.pl> |
  21. | Author: Ryo Chijiiwa <Ryo@IlohaMail.org> |
  22. +-----------------------------------------------------------------------+
  23. */
  24. /**
  25. * PHP based wrapper class to connect to an IMAP server
  26. *
  27. * @package Framework
  28. * @subpackage Storage
  29. */
  30. class rcube_imap_generic
  31. {
  32. public $error;
  33. public $errornum;
  34. public $result;
  35. public $resultcode;
  36. public $selected;
  37. public $data = array();
  38. public $flags = array(
  39. 'SEEN' => '\\Seen',
  40. 'DELETED' => '\\Deleted',
  41. 'ANSWERED' => '\\Answered',
  42. 'DRAFT' => '\\Draft',
  43. 'FLAGGED' => '\\Flagged',
  44. 'FORWARDED' => '$Forwarded',
  45. 'MDNSENT' => '$MDNSent',
  46. '*' => '\\*',
  47. );
  48. protected $fp;
  49. protected $host;
  50. protected $cmd_tag;
  51. protected $cmd_num = 0;
  52. protected $resourceid;
  53. protected $prefs = array();
  54. protected $logged = false;
  55. protected $capability = array();
  56. protected $capability_readed = false;
  57. protected $debug = false;
  58. protected $debug_handler = false;
  59. const ERROR_OK = 0;
  60. const ERROR_NO = -1;
  61. const ERROR_BAD = -2;
  62. const ERROR_BYE = -3;
  63. const ERROR_UNKNOWN = -4;
  64. const ERROR_COMMAND = -5;
  65. const ERROR_READONLY = -6;
  66. const COMMAND_NORESPONSE = 1;
  67. const COMMAND_CAPABILITY = 2;
  68. const COMMAND_LASTLINE = 4;
  69. const COMMAND_ANONYMIZED = 8;
  70. const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
  71. /**
  72. * Send simple (one line) command to the connection stream
  73. *
  74. * @param string $string Command string
  75. * @param bool $endln True if CRLF need to be added at the end of command
  76. * @param bool $anonymized Don't write the given data to log but a placeholder
  77. *
  78. * @param int Number of bytes sent, False on error
  79. */
  80. protected function putLine($string, $endln = true, $anonymized = false)
  81. {
  82. if (!$this->fp) {
  83. return false;
  84. }
  85. if ($this->debug) {
  86. // anonymize the sent command for logging
  87. $cut = $endln ? 2 : 0;
  88. if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
  89. $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
  90. }
  91. else if ($anonymized) {
  92. $log = sprintf('****** [%d]', strlen($string) - $cut);
  93. }
  94. else {
  95. $log = rtrim($string);
  96. }
  97. $this->debug('C: ' . $log);
  98. }
  99. if ($endln) {
  100. $string .= "\r\n";
  101. }
  102. $res = fwrite($this->fp, $string);
  103. if ($res === false) {
  104. @fclose($this->fp);
  105. $this->fp = null;
  106. }
  107. return $res;
  108. }
  109. /**
  110. * Send command to the connection stream with Command Continuation
  111. * Requests (RFC3501 7.5) and LITERAL+ (RFC2088) support
  112. *
  113. * @param string $string Command string
  114. * @param bool $endln True if CRLF need to be added at the end of command
  115. * @param bool $anonymized Don't write the given data to log but a placeholder
  116. *
  117. * @return int|bool Number of bytes sent, False on error
  118. */
  119. protected function putLineC($string, $endln=true, $anonymized=false)
  120. {
  121. if (!$this->fp) {
  122. return false;
  123. }
  124. if ($endln) {
  125. $string .= "\r\n";
  126. }
  127. $res = 0;
  128. if ($parts = preg_split('/(\{[0-9]+\}\r\n)/m', $string, -1, PREG_SPLIT_DELIM_CAPTURE)) {
  129. for ($i=0, $cnt=count($parts); $i<$cnt; $i++) {
  130. if (preg_match('/^\{([0-9]+)\}\r\n$/', $parts[$i+1], $matches)) {
  131. // LITERAL+ support
  132. if ($this->prefs['literal+']) {
  133. $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
  134. }
  135. $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
  136. if ($bytes === false) {
  137. return false;
  138. }
  139. $res += $bytes;
  140. // don't wait if server supports LITERAL+ capability
  141. if (!$this->prefs['literal+']) {
  142. $line = $this->readLine(1000);
  143. // handle error in command
  144. if ($line[0] != '+') {
  145. return false;
  146. }
  147. }
  148. $i++;
  149. }
  150. else {
  151. $bytes = $this->putLine($parts[$i], false, $anonymized);
  152. if ($bytes === false) {
  153. return false;
  154. }
  155. $res += $bytes;
  156. }
  157. }
  158. }
  159. return $res;
  160. }
  161. /**
  162. * Reads line from the connection stream
  163. *
  164. * @param int $size Buffer size
  165. *
  166. * @return string Line of text response
  167. */
  168. protected function readLine($size = 1024)
  169. {
  170. $line = '';
  171. if (!$size) {
  172. $size = 1024;
  173. }
  174. do {
  175. if ($this->eof()) {
  176. return $line ?: null;
  177. }
  178. $buffer = fgets($this->fp, $size);
  179. if ($buffer === false) {
  180. $this->closeSocket();
  181. break;
  182. }
  183. if ($this->debug) {
  184. $this->debug('S: '. rtrim($buffer));
  185. }
  186. $line .= $buffer;
  187. }
  188. while (substr($buffer, -1) != "\n");
  189. return $line;
  190. }
  191. /**
  192. * Reads more data from the connection stream when provided
  193. * data contain string literal
  194. *
  195. * @param string $line Response text
  196. * @param bool $escape Enables escaping
  197. *
  198. * @return string Line of text response
  199. */
  200. protected function multLine($line, $escape = false)
  201. {
  202. $line = rtrim($line);
  203. if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
  204. $out = '';
  205. $str = substr($line, 0, -strlen($m[0]));
  206. $bytes = $m[1];
  207. while (strlen($out) < $bytes) {
  208. $line = $this->readBytes($bytes);
  209. if ($line === null) {
  210. break;
  211. }
  212. $out .= $line;
  213. }
  214. $line = $str . ($escape ? $this->escape($out) : $out);
  215. }
  216. return $line;
  217. }
  218. /**
  219. * Reads specified number of bytes from the connection stream
  220. *
  221. * @param int $bytes Number of bytes to get
  222. *
  223. * @return string Response text
  224. */
  225. protected function readBytes($bytes)
  226. {
  227. $data = '';
  228. $len = 0;
  229. while ($len < $bytes && !$this->eof()) {
  230. $d = fread($this->fp, $bytes-$len);
  231. if ($this->debug) {
  232. $this->debug('S: '. $d);
  233. }
  234. $data .= $d;
  235. $data_len = strlen($data);
  236. if ($len == $data_len) {
  237. break; // nothing was read -> exit to avoid apache lockups
  238. }
  239. $len = $data_len;
  240. }
  241. return $data;
  242. }
  243. /**
  244. * Reads complete response to the IMAP command
  245. *
  246. * @param array $untagged Will be filled with untagged response lines
  247. *
  248. * @return string Response text
  249. */
  250. protected function readReply(&$untagged = null)
  251. {
  252. do {
  253. $line = trim($this->readLine(1024));
  254. // store untagged response lines
  255. if ($line[0] == '*') {
  256. $untagged[] = $line;
  257. }
  258. }
  259. while ($line[0] == '*');
  260. if ($untagged) {
  261. $untagged = join("\n", $untagged);
  262. }
  263. return $line;
  264. }
  265. /**
  266. * Response parser.
  267. *
  268. * @param string $string Response text
  269. * @param string $err_prefix Error message prefix
  270. *
  271. * @return int Response status
  272. */
  273. protected function parseResult($string, $err_prefix = '')
  274. {
  275. if (preg_match('/^[a-z0-9*]+ (OK|NO|BAD|BYE)(.*)$/i', trim($string), $matches)) {
  276. $res = strtoupper($matches[1]);
  277. $str = trim($matches[2]);
  278. if ($res == 'OK') {
  279. $this->errornum = self::ERROR_OK;
  280. }
  281. else if ($res == 'NO') {
  282. $this->errornum = self::ERROR_NO;
  283. }
  284. else if ($res == 'BAD') {
  285. $this->errornum = self::ERROR_BAD;
  286. }
  287. else if ($res == 'BYE') {
  288. $this->closeSocket();
  289. $this->errornum = self::ERROR_BYE;
  290. }
  291. if ($str) {
  292. $str = trim($str);
  293. // get response string and code (RFC5530)
  294. if (preg_match("/^\[([a-z-]+)\]/i", $str, $m)) {
  295. $this->resultcode = strtoupper($m[1]);
  296. $str = trim(substr($str, strlen($m[1]) + 2));
  297. }
  298. else {
  299. $this->resultcode = null;
  300. // parse response for [APPENDUID 1204196876 3456]
  301. if (preg_match("/^\[APPENDUID [0-9]+ ([0-9]+)\]/i", $str, $m)) {
  302. $this->data['APPENDUID'] = $m[1];
  303. }
  304. // parse response for [COPYUID 1204196876 3456:3457 123:124]
  305. else if (preg_match("/^\[COPYUID [0-9]+ ([0-9,:]+) ([0-9,:]+)\]/i", $str, $m)) {
  306. $this->data['COPYUID'] = array($m[1], $m[2]);
  307. }
  308. }
  309. $this->result = $str;
  310. if ($this->errornum != self::ERROR_OK) {
  311. $this->error = $err_prefix ? $err_prefix.$str : $str;
  312. }
  313. }
  314. return $this->errornum;
  315. }
  316. return self::ERROR_UNKNOWN;
  317. }
  318. /**
  319. * Checks connection stream state.
  320. *
  321. * @return bool True if connection is closed
  322. */
  323. protected function eof()
  324. {
  325. if (!is_resource($this->fp)) {
  326. return true;
  327. }
  328. // If a connection opened by fsockopen() wasn't closed
  329. // by the server, feof() will hang.
  330. $start = microtime(true);
  331. if (feof($this->fp) ||
  332. ($this->prefs['timeout'] && (microtime(true) - $start > $this->prefs['timeout']))
  333. ) {
  334. $this->closeSocket();
  335. return true;
  336. }
  337. return false;
  338. }
  339. /**
  340. * Closes connection stream.
  341. */
  342. protected function closeSocket()
  343. {
  344. @fclose($this->fp);
  345. $this->fp = null;
  346. }
  347. /**
  348. * Error code/message setter.
  349. */
  350. protected function setError($code, $msg = '')
  351. {
  352. $this->errornum = $code;
  353. $this->error = $msg;
  354. }
  355. /**
  356. * Checks response status.
  357. * Checks if command response line starts with specified prefix (or * BYE/BAD)
  358. *
  359. * @param string $string Response text
  360. * @param string $match Prefix to match with (case-sensitive)
  361. * @param bool $error Enables BYE/BAD checking
  362. * @param bool $nonempty Enables empty response checking
  363. *
  364. * @return bool True any check is true or connection is closed.
  365. */
  366. protected function startsWith($string, $match, $error = false, $nonempty = false)
  367. {
  368. if (!$this->fp) {
  369. return true;
  370. }
  371. if (strncmp($string, $match, strlen($match)) == 0) {
  372. return true;
  373. }
  374. if ($error && preg_match('/^\* (BYE|BAD) /i', $string, $m)) {
  375. if (strtoupper($m[1]) == 'BYE') {
  376. $this->closeSocket();
  377. }
  378. return true;
  379. }
  380. if ($nonempty && !strlen($string)) {
  381. return true;
  382. }
  383. return false;
  384. }
  385. /**
  386. * Capabilities checker
  387. */
  388. protected function hasCapability($name)
  389. {
  390. if (empty($this->capability) || $name == '') {
  391. return false;
  392. }
  393. if (in_array($name, $this->capability)) {
  394. return true;
  395. }
  396. else if (strpos($name, '=')) {
  397. return false;
  398. }
  399. $result = array();
  400. foreach ($this->capability as $cap) {
  401. $entry = explode('=', $cap);
  402. if ($entry[0] == $name) {
  403. $result[] = $entry[1];
  404. }
  405. }
  406. return $result ?: false;
  407. }
  408. /**
  409. * Capabilities checker
  410. *
  411. * @param string $name Capability name
  412. *
  413. * @return mixed Capability values array for key=value pairs, true/false for others
  414. */
  415. public function getCapability($name)
  416. {
  417. $result = $this->hasCapability($name);
  418. if (!empty($result)) {
  419. return $result;
  420. }
  421. else if ($this->capability_readed) {
  422. return false;
  423. }
  424. // get capabilities (only once) because initial
  425. // optional CAPABILITY response may differ
  426. $result = $this->execute('CAPABILITY');
  427. if ($result[0] == self::ERROR_OK) {
  428. $this->parseCapability($result[1]);
  429. }
  430. $this->capability_readed = true;
  431. return $this->hasCapability($name);
  432. }
  433. /**
  434. * Clears detected server capabilities
  435. */
  436. public function clearCapability()
  437. {
  438. $this->capability = array();
  439. $this->capability_readed = false;
  440. }
  441. /**
  442. * DIGEST-MD5/CRAM-MD5/PLAIN Authentication
  443. *
  444. * @param string $user Username
  445. * @param string $pass Password
  446. * @param string $type Authentication type (PLAIN/CRAM-MD5/DIGEST-MD5)
  447. *
  448. * @return resource Connection resourse on success, error code on error
  449. */
  450. protected function authenticate($user, $pass, $type = 'PLAIN')
  451. {
  452. if ($type == 'CRAM-MD5' || $type == 'DIGEST-MD5') {
  453. if ($type == 'DIGEST-MD5' && !class_exists('Auth_SASL')) {
  454. $this->setError(self::ERROR_BYE,
  455. "The Auth_SASL package is required for DIGEST-MD5 authentication");
  456. return self::ERROR_BAD;
  457. }
  458. $this->putLine($this->nextTag() . " AUTHENTICATE $type");
  459. $line = trim($this->readReply());
  460. if ($line[0] == '+') {
  461. $challenge = substr($line, 2);
  462. }
  463. else {
  464. return $this->parseResult($line);
  465. }
  466. if ($type == 'CRAM-MD5') {
  467. // RFC2195: CRAM-MD5
  468. $ipad = '';
  469. $opad = '';
  470. $xor = function($str1, $str2) {
  471. $result = '';
  472. $size = strlen($str1);
  473. for ($i=0; $i<$size; $i++) {
  474. $result .= chr(ord($str1[$i]) ^ ord($str2[$i]));
  475. }
  476. return $result;
  477. };
  478. // initialize ipad, opad
  479. for ($i=0; $i<64; $i++) {
  480. $ipad .= chr(0x36);
  481. $opad .= chr(0x5C);
  482. }
  483. // pad $pass so it's 64 bytes
  484. $pass = str_pad($pass, 64, chr(0));
  485. // generate hash
  486. $hash = md5($xor($pass, $opad) . pack("H*",
  487. md5($xor($pass, $ipad) . base64_decode($challenge))));
  488. $reply = base64_encode($user . ' ' . $hash);
  489. // send result
  490. $this->putLine($reply, true, true);
  491. }
  492. else {
  493. // RFC2831: DIGEST-MD5
  494. // proxy authorization
  495. if (!empty($this->prefs['auth_cid'])) {
  496. $authc = $this->prefs['auth_cid'];
  497. $pass = $this->prefs['auth_pw'];
  498. }
  499. else {
  500. $authc = $user;
  501. $user = '';
  502. }
  503. $auth_sasl = new Auth_SASL;
  504. $auth_sasl = $auth_sasl->factory('digestmd5');
  505. $reply = base64_encode($auth_sasl->getResponse($authc, $pass,
  506. base64_decode($challenge), $this->host, 'imap', $user));
  507. // send result
  508. $this->putLine($reply, true, true);
  509. $line = trim($this->readReply());
  510. if ($line[0] != '+') {
  511. return $this->parseResult($line);
  512. }
  513. // check response
  514. $challenge = substr($line, 2);
  515. $challenge = base64_decode($challenge);
  516. if (strpos($challenge, 'rspauth=') === false) {
  517. $this->setError(self::ERROR_BAD,
  518. "Unexpected response from server to DIGEST-MD5 response");
  519. return self::ERROR_BAD;
  520. }
  521. $this->putLine('');
  522. }
  523. $line = $this->readReply();
  524. $result = $this->parseResult($line);
  525. }
  526. else if ($type == 'GSSAPI') {
  527. if (!extension_loaded('krb5')) {
  528. $this->setError(self::ERROR_BYE,
  529. "The krb5 extension is required for GSSAPI authentication");
  530. return self::ERROR_BAD;
  531. }
  532. if (empty($this->prefs['gssapi_cn'])) {
  533. $this->setError(self::ERROR_BYE,
  534. "The gssapi_cn parameter is required for GSSAPI authentication");
  535. return self::ERROR_BAD;
  536. }
  537. if (empty($this->prefs['gssapi_context'])) {
  538. $this->setError(self::ERROR_BYE,
  539. "The gssapi_context parameter is required for GSSAPI authentication");
  540. return self::ERROR_BAD;
  541. }
  542. putenv('KRB5CCNAME=' . $this->prefs['gssapi_cn']);
  543. try {
  544. $ccache = new KRB5CCache();
  545. $ccache->open($this->prefs['gssapi_cn']);
  546. $gssapicontext = new GSSAPIContext();
  547. $gssapicontext->acquireCredentials($ccache);
  548. $token = '';
  549. $success = $gssapicontext->initSecContext($this->prefs['gssapi_context'], null, null, null, $token);
  550. $token = base64_encode($token);
  551. }
  552. catch (Exception $e) {
  553. trigger_error($e->getMessage(), E_USER_WARNING);
  554. $this->setError(self::ERROR_BYE, "GSSAPI authentication failed");
  555. return self::ERROR_BAD;
  556. }
  557. $this->putLine($this->nextTag() . " AUTHENTICATE GSSAPI " . $token);
  558. $line = trim($this->readReply());
  559. if ($line[0] != '+') {
  560. return $this->parseResult($line);
  561. }
  562. try {
  563. $challenge = base64_decode(substr($line, 2));
  564. $gssapicontext->unwrap($challenge, $challenge);
  565. $gssapicontext->wrap($challenge, $challenge, true);
  566. }
  567. catch (Exception $e) {
  568. trigger_error($e->getMessage(), E_USER_WARNING);
  569. $this->setError(self::ERROR_BYE, "GSSAPI authentication failed");
  570. return self::ERROR_BAD;
  571. }
  572. $this->putLine(base64_encode($challenge));
  573. $line = $this->readReply();
  574. $result = $this->parseResult($line);
  575. }
  576. else { // PLAIN
  577. // proxy authorization
  578. if (!empty($this->prefs['auth_cid'])) {
  579. $authc = $this->prefs['auth_cid'];
  580. $pass = $this->prefs['auth_pw'];
  581. }
  582. else {
  583. $authc = $user;
  584. $user = '';
  585. }
  586. $reply = base64_encode($user . chr(0) . $authc . chr(0) . $pass);
  587. // RFC 4959 (SASL-IR): save one round trip
  588. if ($this->getCapability('SASL-IR')) {
  589. list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
  590. self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
  591. }
  592. else {
  593. $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
  594. $line = trim($this->readReply());
  595. if ($line[0] != '+') {
  596. return $this->parseResult($line);
  597. }
  598. // send result, get reply and process it
  599. $this->putLine($reply, true, true);
  600. $line = $this->readReply();
  601. $result = $this->parseResult($line);
  602. }
  603. }
  604. if ($result == self::ERROR_OK) {
  605. // optional CAPABILITY response
  606. if ($line && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
  607. $this->parseCapability($matches[1], true);
  608. }
  609. return $this->fp;
  610. }
  611. else {
  612. $this->setError($result, "AUTHENTICATE $type: $line");
  613. }
  614. return $result;
  615. }
  616. /**
  617. * LOGIN Authentication
  618. *
  619. * @param string $user Username
  620. * @param string $pass Password
  621. *
  622. * @return resource Connection resourse on success, error code on error
  623. */
  624. protected function login($user, $password)
  625. {
  626. list($code, $response) = $this->execute('LOGIN', array(
  627. $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
  628. // re-set capabilities list if untagged CAPABILITY response provided
  629. if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
  630. $this->parseCapability($matches[1], true);
  631. }
  632. if ($code == self::ERROR_OK) {
  633. return $this->fp;
  634. }
  635. return $code;
  636. }
  637. /**
  638. * Detects hierarchy delimiter
  639. *
  640. * @return string The delimiter
  641. */
  642. public function getHierarchyDelimiter()
  643. {
  644. if ($this->prefs['delimiter']) {
  645. return $this->prefs['delimiter'];
  646. }
  647. // try (LIST "" ""), should return delimiter (RFC2060 Sec 6.3.8)
  648. list($code, $response) = $this->execute('LIST',
  649. array($this->escape(''), $this->escape('')));
  650. if ($code == self::ERROR_OK) {
  651. $args = $this->tokenizeResponse($response, 4);
  652. $delimiter = $args[3];
  653. if (strlen($delimiter) > 0) {
  654. return ($this->prefs['delimiter'] = $delimiter);
  655. }
  656. }
  657. }
  658. /**
  659. * NAMESPACE handler (RFC 2342)
  660. *
  661. * @return array Namespace data hash (personal, other, shared)
  662. */
  663. public function getNamespace()
  664. {
  665. if (array_key_exists('namespace', $this->prefs)) {
  666. return $this->prefs['namespace'];
  667. }
  668. if (!$this->getCapability('NAMESPACE')) {
  669. return self::ERROR_BAD;
  670. }
  671. list($code, $response) = $this->execute('NAMESPACE');
  672. if ($code == self::ERROR_OK && preg_match('/^\* NAMESPACE /', $response)) {
  673. $response = substr($response, 11);
  674. $data = $this->tokenizeResponse($response);
  675. }
  676. if (!is_array($data)) {
  677. return $code;
  678. }
  679. $this->prefs['namespace'] = array(
  680. 'personal' => $data[0],
  681. 'other' => $data[1],
  682. 'shared' => $data[2],
  683. );
  684. return $this->prefs['namespace'];
  685. }
  686. /**
  687. * Connects to IMAP server and authenticates.
  688. *
  689. * @param string $host Server hostname or IP
  690. * @param string $user User name
  691. * @param string $password Password
  692. * @param array $options Connection and class options
  693. *
  694. * @return bool True on success, False on failure
  695. */
  696. public function connect($host, $user, $password, $options = array())
  697. {
  698. // configure
  699. $this->set_prefs($options);
  700. $this->host = $host;
  701. $this->user = $user;
  702. $this->logged = false;
  703. $this->selected = null;
  704. // check input
  705. if (empty($host)) {
  706. $this->setError(self::ERROR_BAD, "Empty host");
  707. return false;
  708. }
  709. if (empty($user)) {
  710. $this->setError(self::ERROR_NO, "Empty user");
  711. return false;
  712. }
  713. if (empty($password) && empty($options['gssapi_cn'])) {
  714. $this->setError(self::ERROR_NO, "Empty password");
  715. return false;
  716. }
  717. // Connect
  718. if (!$this->_connect($host)) {
  719. return false;
  720. }
  721. // Send ID info
  722. if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
  723. $this->data['ID'] = $this->id($this->prefs['ident']);
  724. }
  725. $auth_method = $this->prefs['auth_type'];
  726. $auth_methods = array();
  727. $result = null;
  728. // check for supported auth methods
  729. if ($auth_method == 'CHECK') {
  730. if ($auth_caps = $this->getCapability('AUTH')) {
  731. $auth_methods = $auth_caps;
  732. }
  733. // RFC 2595 (LOGINDISABLED) LOGIN disabled when connection is not secure
  734. $login_disabled = $this->getCapability('LOGINDISABLED');
  735. if (($key = array_search('LOGIN', $auth_methods)) !== false) {
  736. if ($login_disabled) {
  737. unset($auth_methods[$key]);
  738. }
  739. }
  740. else if (!$login_disabled) {
  741. $auth_methods[] = 'LOGIN';
  742. }
  743. // Use best (for security) supported authentication method
  744. $all_methods = array('DIGEST-MD5', 'CRAM-MD5', 'CRAM_MD5', 'PLAIN', 'LOGIN');
  745. if (!empty($this->prefs['gssapi_cn'])) {
  746. array_unshift($all_methods, 'GSSAPI');
  747. }
  748. foreach ($all_methods as $auth_method) {
  749. if (in_array($auth_method, $auth_methods)) {
  750. break;
  751. }
  752. }
  753. }
  754. else {
  755. // Prevent from sending credentials in plain text when connection is not secure
  756. if ($auth_method == 'LOGIN' && $this->getCapability('LOGINDISABLED')) {
  757. $this->setError(self::ERROR_BAD, "Login disabled by IMAP server");
  758. $this->closeConnection();
  759. return false;
  760. }
  761. // replace AUTH with CRAM-MD5 for backward compat.
  762. if ($auth_method == 'AUTH') {
  763. $auth_method = 'CRAM-MD5';
  764. }
  765. }
  766. // pre-login capabilities can be not complete
  767. $this->capability_readed = false;
  768. // Authenticate
  769. switch ($auth_method) {
  770. case 'CRAM_MD5':
  771. $auth_method = 'CRAM-MD5';
  772. case 'CRAM-MD5':
  773. case 'DIGEST-MD5':
  774. case 'PLAIN':
  775. case 'GSSAPI':
  776. $result = $this->authenticate($user, $password, $auth_method);
  777. break;
  778. case 'LOGIN':
  779. $result = $this->login($user, $password);
  780. break;
  781. default:
  782. $this->setError(self::ERROR_BAD, "Configuration error. Unknown auth method: $auth_method");
  783. }
  784. // Connected and authenticated
  785. if (is_resource($result)) {
  786. if ($this->prefs['force_caps']) {
  787. $this->clearCapability();
  788. }
  789. $this->logged = true;
  790. return true;
  791. }
  792. $this->closeConnection();
  793. return false;
  794. }
  795. /**
  796. * Connects to IMAP server.
  797. *
  798. * @param string $host Server hostname or IP
  799. *
  800. * @return bool True on success, False on failure
  801. */
  802. protected function _connect($host)
  803. {
  804. // initialize connection
  805. $this->error = '';
  806. $this->errornum = self::ERROR_OK;
  807. if (!$this->prefs['port']) {
  808. $this->prefs['port'] = 143;
  809. }
  810. // check for SSL
  811. if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
  812. $host = $this->prefs['ssl_mode'] . '://' . $host;
  813. }
  814. if ($this->prefs['timeout'] <= 0) {
  815. $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
  816. }
  817. if (!empty($this->prefs['socket_options'])) {
  818. $context = stream_context_create($this->prefs['socket_options']);
  819. $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr,
  820. $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context);
  821. }
  822. else {
  823. $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
  824. }
  825. if (!$this->fp) {
  826. $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s",
  827. $host, $this->prefs['port'], $errstr ?: "Unknown reason"));
  828. return false;
  829. }
  830. if ($this->prefs['timeout'] > 0) {
  831. stream_set_timeout($this->fp, $this->prefs['timeout']);
  832. }
  833. $line = trim(fgets($this->fp, 8192));
  834. if ($this->debug) {
  835. // set connection identifier for debug output
  836. preg_match('/#([0-9]+)/', (string) $this->fp, $m);
  837. $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
  838. if ($line) {
  839. $this->debug('S: '. $line);
  840. }
  841. }
  842. // Connected to wrong port or connection error?
  843. if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
  844. if ($line)
  845. $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
  846. else
  847. $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
  848. $this->setError(self::ERROR_BAD, $error);
  849. $this->closeConnection();
  850. return false;
  851. }
  852. $this->data['GREETING'] = trim(preg_replace('/\[[^\]]+\]\s*/', '', $line));
  853. // RFC3501 [7.1] optional CAPABILITY response
  854. if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
  855. $this->parseCapability($matches[1], true);
  856. }
  857. // TLS connection
  858. if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
  859. $res = $this->execute('STARTTLS');
  860. if ($res[0] != self::ERROR_OK) {
  861. $this->closeConnection();
  862. return false;
  863. }
  864. if (isset($this->prefs['socket_options']['ssl']['crypto_method'])) {
  865. $crypto_method = $this->prefs['socket_options']['ssl']['crypto_method'];
  866. }
  867. else {
  868. // There is no flag to enable all TLS methods. Net_SMTP
  869. // handles enabling TLS similarly.
  870. $crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT
  871. | @STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT
  872. | @STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
  873. }
  874. if (!stream_socket_enable_crypto($this->fp, true, $crypto_method)) {
  875. $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
  876. $this->closeConnection();
  877. return false;
  878. }
  879. // Now we're secure, capabilities need to be reread
  880. $this->clearCapability();
  881. }
  882. return true;
  883. }
  884. /**
  885. * Initializes environment
  886. */
  887. protected function set_prefs($prefs)
  888. {
  889. // set preferences
  890. if (is_array($prefs)) {
  891. $this->prefs = $prefs;
  892. }
  893. // set auth method
  894. if (!empty($this->prefs['auth_type'])) {
  895. $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
  896. }
  897. else {
  898. $this->prefs['auth_type'] = 'CHECK';
  899. }
  900. // disabled capabilities
  901. if (!empty($this->prefs['disabled_caps'])) {
  902. $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
  903. }
  904. // additional message flags
  905. if (!empty($this->prefs['message_flags'])) {
  906. $this->flags = array_merge($this->flags, $this->prefs['message_flags']);
  907. unset($this->prefs['message_flags']);
  908. }
  909. }
  910. /**
  911. * Checks connection status
  912. *
  913. * @return bool True if connection is active and user is logged in, False otherwise.
  914. */
  915. public function connected()
  916. {
  917. return $this->fp && $this->logged;
  918. }
  919. /**
  920. * Closes connection with logout.
  921. */
  922. public function closeConnection()
  923. {
  924. if ($this->logged && $this->putLine($this->nextTag() . ' LOGOUT')) {
  925. $this->readReply();
  926. }
  927. $this->closeSocket();
  928. }
  929. /**
  930. * Executes SELECT command (if mailbox is already not in selected state)
  931. *
  932. * @param string $mailbox Mailbox name
  933. * @param array $qresync_data QRESYNC data (RFC5162)
  934. *
  935. * @return boolean True on success, false on error
  936. */
  937. public function select($mailbox, $qresync_data = null)
  938. {
  939. if (!strlen($mailbox)) {
  940. return false;
  941. }
  942. if ($this->selected === $mailbox) {
  943. return true;
  944. }
  945. /*
  946. Temporary commented out because Courier returns \Noselect for INBOX
  947. Requires more investigation
  948. if (is_array($this->data['LIST']) && is_array($opts = $this->data['LIST'][$mailbox])) {
  949. if (in_array('\\Noselect', $opts)) {
  950. return false;
  951. }
  952. }
  953. */
  954. $params = array($this->escape($mailbox));
  955. // QRESYNC data items
  956. // 0. the last known UIDVALIDITY,
  957. // 1. the last known modification sequence,
  958. // 2. the optional set of known UIDs, and
  959. // 3. an optional parenthesized list of known sequence ranges and their
  960. // corresponding UIDs.
  961. if (!empty($qresync_data)) {
  962. if (!empty($qresync_data[2])) {
  963. $qresync_data[2] = self::compressMessageSet($qresync_data[2]);
  964. }
  965. $params[] = array('QRESYNC', $qresync_data);
  966. }
  967. list($code, $response) = $this->execute('SELECT', $params);
  968. if ($code == self::ERROR_OK) {
  969. $this->clear_mailbox_cache();
  970. $response = explode("\r\n", $response);
  971. foreach ($response as $line) {
  972. if (preg_match('/^\* OK \[/i', $line)) {
  973. $pos = strcspn($line, ' ]', 6);
  974. $token = strtoupper(substr($line, 6, $pos));
  975. $pos += 7;
  976. switch ($token) {
  977. case 'UIDNEXT':
  978. case 'UIDVALIDITY':
  979. case 'UNSEEN':
  980. if ($len = strspn($line, '0123456789', $pos)) {
  981. $this->data[$token] = (int) substr($line, $pos, $len);
  982. }
  983. break;
  984. case 'HIGHESTMODSEQ':
  985. if ($len = strspn($line, '0123456789', $pos)) {
  986. $this->data[$token] = (string) substr($line, $pos, $len);
  987. }
  988. break;
  989. case 'NOMODSEQ':
  990. $this->data[$token] = true;
  991. break;
  992. case 'PERMANENTFLAGS':
  993. $start = strpos($line, '(', $pos);
  994. $end = strrpos($line, ')');
  995. if ($start && $end) {
  996. $flags = substr($line, $start + 1, $end - $start - 1);
  997. $this->data[$token] = explode(' ', $flags);
  998. }
  999. break;
  1000. }
  1001. }
  1002. else if (preg_match('/^\* ([0-9]+) (EXISTS|RECENT|FETCH)/i', $line, $match)) {
  1003. $token = strtoupper($match[2]);
  1004. switch ($token) {
  1005. case 'EXISTS':
  1006. case 'RECENT':
  1007. $this->data[$token] = (int) $match[1];
  1008. break;
  1009. case 'FETCH':
  1010. // QRESYNC FETCH response (RFC5162)
  1011. $line = substr($line, strlen($match[0]));
  1012. $fetch_data = $this->tokenizeResponse($line, 1);
  1013. $data = array('id' => $match[1]);
  1014. for ($i=0, $size=count($fetch_data); $i<$size; $i+=2) {
  1015. $data[strtolower($fetch_data[$i])] = $fetch_data[$i+1];
  1016. }
  1017. $this->data['QRESYNC'][$data['uid']] = $data;
  1018. break;
  1019. }
  1020. }
  1021. // QRESYNC VANISHED response (RFC5162)
  1022. else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
  1023. $line = substr($line, strlen($match[0]));
  1024. $v_data = $this->tokenizeResponse($line, 1);
  1025. $this->data['VANISHED'] = $v_data;
  1026. }
  1027. }
  1028. $this->data['READ-WRITE'] = $this->resultcode != 'READ-ONLY';
  1029. $this->selected = $mailbox;
  1030. return true;
  1031. }
  1032. return false;
  1033. }
  1034. /**
  1035. * Executes STATUS command
  1036. *
  1037. * @param string $mailbox Mailbox name
  1038. * @param array $items Additional requested item names. By default
  1039. * MESSAGES and UNSEEN are requested. Other defined
  1040. * in RFC3501: UIDNEXT, UIDVALIDITY, RECENT
  1041. *
  1042. * @return array Status item-value hash
  1043. * @since 0.5-beta
  1044. */
  1045. public function status($mailbox, $items = array())
  1046. {
  1047. if (!strlen($mailbox)) {
  1048. return false;
  1049. }
  1050. if (!in_array('MESSAGES', $items)) {
  1051. $items[] = 'MESSAGES';
  1052. }
  1053. if (!in_array('UNSEEN', $items)) {
  1054. $items[] = 'UNSEEN';
  1055. }
  1056. list($code, $response) = $this->execute('STATUS', array($this->escape($mailbox),
  1057. '(' . implode(' ', (array) $items) . ')'));
  1058. if ($code == self::ERROR_OK && preg_match('/^\* STATUS /i', $response)) {
  1059. $result = array();
  1060. $response = substr($response, 9); // remove prefix "* STATUS "
  1061. list($mbox, $items) = $this->tokenizeResponse($response, 2);
  1062. // Fix for #1487859. Some buggy server returns not quoted
  1063. // folder name with spaces. Let's try to handle this situation
  1064. if (!is_array($items) && ($pos = strpos($response, '(')) !== false) {
  1065. $response = substr($response, $pos);
  1066. $items = $this->tokenizeResponse($response, 1);
  1067. if (!is_array($items)) {
  1068. return $result;
  1069. }
  1070. }
  1071. for ($i=0, $len=count($items); $i<$len; $i += 2) {
  1072. $result[$items[$i]] = $items[$i+1];
  1073. }
  1074. $this->data['STATUS:'.$mailbox] = $result;
  1075. return $result;
  1076. }
  1077. return false;
  1078. }
  1079. /**
  1080. * Executes EXPUNGE command
  1081. *
  1082. * @param string $mailbox Mailbox name
  1083. * @param string|array $messages Message UIDs to expunge
  1084. *
  1085. * @return boolean True on success, False on error
  1086. */
  1087. public function expunge($mailbox, $messages = null)
  1088. {
  1089. if (!$this->select($mailbox)) {
  1090. return false;
  1091. }
  1092. if (!$this->data['READ-WRITE']) {
  1093. $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
  1094. return false;
  1095. }
  1096. // Clear internal status cache
  1097. $this->clear_status_cache($mailbox);
  1098. if (!empty($messages) && $messages != '*' && $this->hasCapability('UIDPLUS')) {
  1099. $messages = self::compressMessageSet($messages);
  1100. $result = $this->execute('UID EXPUNGE', array($messages), self::COMMAND_NORESPONSE);
  1101. }
  1102. else {
  1103. $result = $this->execute('EXPUNGE', null, self::COMMAND_NORESPONSE);
  1104. }
  1105. if ($result == self::ERROR_OK) {
  1106. $this->selected = null; // state has changed, need to reselect
  1107. return true;
  1108. }
  1109. return false;
  1110. }
  1111. /**
  1112. * Executes CLOSE command
  1113. *
  1114. * @return boolean True on success, False on error
  1115. * @since 0.5
  1116. */
  1117. public function close()
  1118. {
  1119. $result = $this->execute('CLOSE', null, self::COMMAND_NORESPONSE);
  1120. if ($result == self::ERROR_OK) {
  1121. $this->selected = null;
  1122. return true;
  1123. }
  1124. return false;
  1125. }
  1126. /**
  1127. * Folder subscription (SUBSCRIBE)
  1128. *
  1129. * @param string $mailbox Mailbox name
  1130. *
  1131. * @return boolean True on success, False on error
  1132. */
  1133. public function subscribe($mailbox)
  1134. {
  1135. $result = $this->execute('SUBSCRIBE', array($this->escape($mailbox)),
  1136. self::COMMAND_NORESPONSE);
  1137. return $result == self::ERROR_OK;
  1138. }
  1139. /**
  1140. * Folder unsubscription (UNSUBSCRIBE)
  1141. *
  1142. * @param string $mailbox Mailbox name
  1143. *
  1144. * @return boolean True on success, False on error
  1145. */
  1146. public function unsubscribe($mailbox)
  1147. {
  1148. $result = $this->execute('UNSUBSCRIBE', array($this->escape($mailbox)),
  1149. self::COMMAND_NORESPONSE);
  1150. return $result == self::ERROR_OK;
  1151. }
  1152. /**
  1153. * Folder creation (CREATE)
  1154. *
  1155. * @param string $mailbox Mailbox name
  1156. * @param array $types Optional folder types (RFC 6154)
  1157. *
  1158. * @return bool True on success, False on error
  1159. */
  1160. public function createFolder($mailbox, $types = null)
  1161. {
  1162. $args = array($this->escape($mailbox));
  1163. // RFC 6154: CREATE-SPECIAL-USE
  1164. if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
  1165. $args[] = '(USE (' . implode(' ', $types) . '))';
  1166. }
  1167. $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
  1168. return $result == self::ERROR_OK;
  1169. }
  1170. /**
  1171. * Folder renaming (RENAME)
  1172. *
  1173. * @param string $mailbox Mailbox name
  1174. *
  1175. * @return bool True on success, False on error
  1176. */
  1177. public function renameFolder($from, $to)
  1178. {
  1179. $result = $this->execute('RENAME', array($this->escape($from), $this->escape($to)),
  1180. self::COMMAND_NORESPONSE);
  1181. return $result == self::ERROR_OK;
  1182. }
  1183. /**
  1184. * Executes DELETE command
  1185. *
  1186. * @param string $mailbox Mailbox name
  1187. *
  1188. * @return boolean True on success, False on error
  1189. */
  1190. public function deleteFolder($mailbox)
  1191. {
  1192. $result = $this->execute('DELETE', array($this->escape($mailbox)),
  1193. self::COMMAND_NORESPONSE);
  1194. return $result == self::ERROR_OK;
  1195. }
  1196. /**
  1197. * Removes all messages in a folder
  1198. *
  1199. * @param string $mailbox Mailbox name
  1200. *
  1201. * @return boolean True on success, False on error
  1202. */
  1203. public function clearFolder($mailbox)
  1204. {
  1205. if ($this->countMessages($mailbox) > 0) {
  1206. $res = $this->flag($mailbox, '1:*', 'DELETED');
  1207. }
  1208. if ($res) {
  1209. if ($this->selected === $mailbox) {
  1210. $res = $this->close();
  1211. }
  1212. else {
  1213. $res = $this->expunge($mailbox);
  1214. }
  1215. }
  1216. return $res;
  1217. }
  1218. /**
  1219. * Returns list of mailboxes
  1220. *
  1221. * @param string $ref Reference name
  1222. * @param string $mailbox Mailbox name
  1223. * @param array $return_opts (see self::_listMailboxes)
  1224. * @param array $select_opts (see self::_listMailboxes)
  1225. *
  1226. * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
  1227. * is requested, False on error.
  1228. */
  1229. public function listMailboxes($ref, $mailbox, $return_opts = array(), $select_opts = array())
  1230. {
  1231. return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts);
  1232. }
  1233. /**
  1234. * Returns list of subscribed mailboxes
  1235. *
  1236. * @param string $ref Reference name
  1237. * @param string $mailbox Mailbox name
  1238. * @param array $return_opts (see self::_listMailboxes)
  1239. *
  1240. * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
  1241. * is requested, False on error.
  1242. */
  1243. public function listSubscribed($ref, $mailbox, $return_opts = array())
  1244. {
  1245. return $this->_listMailboxes($ref, $mailbox, true, $return_opts, null);
  1246. }
  1247. /**
  1248. * IMAP LIST/LSUB command
  1249. *
  1250. * @param string $ref Reference name
  1251. * @param string $mailbox Mailbox name
  1252. * @param bool $subscribed Enables returning subscribed mailboxes only
  1253. * @param array $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
  1254. * Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
  1255. * MYRIGHTS, SUBSCRIBED, CHILDREN
  1256. * @param array $select_opts List of selection options (RFC5258: LIST-EXTENDED)
  1257. * Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
  1258. * SPECIAL-USE (RFC6154)
  1259. *
  1260. * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
  1261. * is requested, False on error.
  1262. */
  1263. protected function _listMailboxes($ref, $mailbox, $subscribed=false,
  1264. $return_opts=array(), $select_opts=array())
  1265. {
  1266. if (!strlen($mailbox)) {
  1267. $mailbox = '*';
  1268. }
  1269. $args = array();
  1270. $rets = array();
  1271. if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
  1272. $select_opts = (array) $select_opts;
  1273. $args[] = '(' . implode(' ', $select_opts) . ')';
  1274. }
  1275. $args[] = $this->escape($ref);
  1276. $args[] = $this->escape($mailbox);
  1277. if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
  1278. $ext_opts = array('SUBSCRIBED', 'CHILDREN');
  1279. $rets = array_intersect($return_opts, $ext_opts);
  1280. $return_opts = array_diff($return_opts, $rets);
  1281. }
  1282. if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
  1283. $lstatus = true;
  1284. $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN');
  1285. $opts = array_diff($return_opts, $status_opts);
  1286. $status_opts = array_diff($return_opts, $opts);
  1287. if (!empty($status_opts)) {
  1288. $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
  1289. }
  1290. if (!empty($opts)) {
  1291. $rets = array_merge($rets, $opts);
  1292. }
  1293. }
  1294. if (!empty($rets)) {
  1295. $args[] = 'RETURN (' . implode(' ', $rets) . ')';
  1296. }
  1297. list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
  1298. if ($code == self::ERROR_OK) {
  1299. $folders = array();
  1300. $last = 0;
  1301. $pos = 0;
  1302. $response .= "\r\n";
  1303. while ($pos = strpos($response, "\r\n", $pos+1)) {
  1304. // literal string, not real end-of-command-line
  1305. if ($response[$pos-1] == '}') {
  1306. continue;
  1307. }
  1308. $line = substr($response, $last, $pos - $last);
  1309. $last = $pos + 2;
  1310. if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) {
  1311. continue;
  1312. }
  1313. $cmd = strtoupper($m[1]);
  1314. $line = substr($line, strlen($m[0]));
  1315. // * LIST (<options>) <delimiter> <mailbox>
  1316. if ($cmd == 'LIST' || $cmd == 'LSUB') {
  1317. list($opts, $delim, $mailbox) = $this->tokenizeResponse($line, 3);
  1318. // Remove redundant separator at the end of folder name, UW-IMAP bug? (#1488879)
  1319. if ($delim) {
  1320. $mailbox = rtrim($mailbox, $delim);
  1321. }
  1322. // Add to result array
  1323. if (!$lstatus) {
  1324. $folders[] = $mailbox;
  1325. }
  1326. else {
  1327. $folders[$mailbox] = array();
  1328. }
  1329. // store folder options
  1330. if ($cmd == 'LIST') {
  1331. // Add to options array
  1332. if (empty($this->data['LIST'][$mailbox])) {
  1333. $this->data['LIST'][$mailbox] = $opts;
  1334. }
  1335. else if (!empty($opts)) {
  1336. $this->data['LIST'][$mailbox] = array_unique(array_merge(
  1337. $this->data['LIST'][$mailbox], $opts));
  1338. }
  1339. }
  1340. }
  1341. else if ($lstatus) {
  1342. // * STATUS <mailbox> (<result>)
  1343. if ($cmd == 'STATUS') {
  1344. list($mailbox, $status) = $this->tokenizeResponse($line, 2);
  1345. for ($i=0, $len=count($status); $i<$len; $i += 2) {
  1346. list($name, $value) = $this->tokenizeResponse($status, 2);
  1347. $folders[$mailbox][$name] = $value;
  1348. }
  1349. }
  1350. // * MYRIGHTS <mailbox> <acl>
  1351. else if ($cmd == 'MYRIGHTS') {
  1352. list($mailbox, $acl) = $this->tokenizeResponse($line, 2);
  1353. $folders[$mailbox]['MYRIGHTS'] = $acl;
  1354. }
  1355. }
  1356. }
  1357. return $folders;
  1358. }
  1359. return false;
  1360. }
  1361. /**
  1362. * Returns count of all messages in a folder
  1363. *
  1364. * @param string $mailbox Mailbox name
  1365. *
  1366. * @return int Number of messages, False on error
  1367. */
  1368. public function countMessages($mailbox)
  1369. {
  1370. if ($this->selected === $mailbox && isset($this->data['EXISTS'])) {
  1371. return $this->data['EXISTS'];
  1372. }
  1373. // Check internal cache
  1374. $cache = $this->data['STATUS:'.$mailbox];
  1375. if (!empty($cache) && isset($cache['MESSAGES'])) {
  1376. return (int) $cache['MESSAGES'];
  1377. }
  1378. // Try STATUS (should be faster than SELECT)
  1379. $counts = $this->status($mailbox);
  1380. if (is_array($counts)) {
  1381. return (int) $counts['MESSAGES'];
  1382. }
  1383. return false;
  1384. }
  1385. /**
  1386. * Returns count of messages with \Recent flag in a folder
  1387. *
  1388. * @param string $mailbox Mailbox name
  1389. *
  1390. * @return int Number of messages, False on error
  1391. */
  1392. public function countRecent($mailbox)
  1393. {
  1394. if ($this->selected === $mailbox && isset($this->data['RECENT'])) {
  1395. return $this->data['RECENT'];
  1396. }
  1397. // Check internal cache
  1398. $cache = $this->data['STATUS:'.$mailbox];
  1399. if (!empty($cache) && isset($cache['RECENT'])) {
  1400. return (int) $cache['RECENT'];
  1401. }
  1402. // Try STATUS (should be faster than SELECT)
  1403. $counts = $this->status($mailbox, array('RECENT'));
  1404. if (is_array($counts)) {
  1405. return (int) $counts['RECENT'];
  1406. }
  1407. return false;
  1408. }
  1409. /**
  1410. * Returns count of messages without \Seen flag in a specified folder
  1411. *
  1412. * @param string $mailbox Mailbox name
  1413. *
  1414. * @return int Number of messages, False on error
  1415. */
  1416. public function countUnseen($mailbox)
  1417. {
  1418. // Check internal cache
  1419. $cache = $this->data['STATUS:'.$mailbox];
  1420. if (!empty($cache) && isset($cache['UNSEEN'])) {
  1421. return (int) $cache['UNSEEN'];
  1422. }
  1423. // Try STATUS (should be faster than SELECT+SEARCH)
  1424. $counts = $this->status($mailbox);
  1425. if (is_array($counts)) {
  1426. return (int) $counts['UNSEEN'];
  1427. }
  1428. // Invoke SEARCH as a fallback
  1429. $index = $this->search($mailbox, 'ALL UNSEEN', false, array('COUNT'));
  1430. if (!$index->is_error()) {
  1431. return $index->count();
  1432. }
  1433. return false;
  1434. }
  1435. /**
  1436. * Executes ID command (RFC2971)
  1437. *
  1438. * @param array $items Client identification information key/value hash
  1439. *
  1440. * @return array Server identification information key/value hash
  1441. * @since 0.6
  1442. */
  1443. public function id($items = array())
  1444. {
  1445. if (is_array($items) && !empty($items)) {
  1446. foreach ($items as $key => $value) {
  1447. $args[] = $this->escape($key, true);
  1448. $args[] = $this->escape($value, true);
  1449. }
  1450. }
  1451. list($code, $response) = $this->execute('ID', array(
  1452. !empty($args) ? '(' . implode(' ', (array) $args) . ')' : $this->escape(null)
  1453. ));
  1454. if ($code == self::ERROR_OK && preg_match('/^\* ID /i', $response)) {
  1455. $response = substr($response, 5); // remove prefix "* ID "
  1456. $items = $this->tokenizeResponse($response, 1);
  1457. $result = null;
  1458. for ($i=0, $len=count($items); $i<$len; $i += 2) {
  1459. $result[$items[$i]] = $items[$i+1];
  1460. }
  1461. return $result;
  1462. }
  1463. return false;
  1464. }
  1465. /**
  1466. * Executes ENABLE command (RFC5161)
  1467. *
  1468. * @param mixed $extension Extension name to enable (or array of names)
  1469. *
  1470. * @return array|bool List of enabled extensions, False on error
  1471. * @since 0.6
  1472. */
  1473. public function enable($extension)
  1474. {
  1475. if (empty($extension)) {
  1476. return false;
  1477. }
  1478. if (!$this->hasCapability('ENABLE')) {
  1479. return false;
  1480. }
  1481. if (!is_array($extension)) {
  1482. $extension = array($extension);
  1483. }
  1484. if (!empty($this->extensions_enabled)) {
  1485. // check if all extensions are already enabled
  1486. $diff = array_diff($extension, $this->extensions_enabled);
  1487. if (empty($diff)) {
  1488. return $extension;
  1489. }
  1490. // Make sure the mailbox isn't selected, before enabling extension(s)
  1491. if ($this->selected !== null) {
  1492. $this->close();
  1493. }
  1494. }
  1495. list($code, $response) = $this->execute('ENABLE', $extension);
  1496. if ($code == self::ERROR_OK && preg_match('/^\* ENABLED /i', $response)) {
  1497. $response = substr($response, 10); // remove prefix "* ENABLED "
  1498. $result = (array) $this->tokenizeResponse($response);
  1499. $this->extensions_enabled = array_unique(array_merge((array)$this->extensions_enabled, $result));
  1500. return $this->extensions_enabled;
  1501. }
  1502. return false;
  1503. }
  1504. /**
  1505. * Executes SORT command
  1506. *
  1507. * @param string $mailbox Mailbox name
  1508. * @param string $field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
  1509. * @param string $criteria Searching criteria
  1510. * @param bool $return_uid Enables UID SORT usage
  1511. * @param string $encoding Character set
  1512. *
  1513. * @return rcube_result_index Response data
  1514. */
  1515. public function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
  1516. {
  1517. $old_sel = $this->selected;
  1518. $supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO');
  1519. $field = strtoupper($field);
  1520. if ($field == 'INTERNALDATE') {
  1521. $field = 'ARRIVAL';
  1522. }
  1523. if (!in_array($field, $supported)) {
  1524. return new rcube_result_index($mailbox);
  1525. }
  1526. if (!$this->select($mailbox)) {
  1527. return new rcube_result_index($mailbox);
  1528. }
  1529. // return empty result when folder is empty and we're just after SELECT
  1530. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1531. return new rcube_result_index($mailbox, '* SORT');
  1532. }
  1533. // RFC 5957: SORT=DISPLAY
  1534. if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
  1535. $field = 'DISPLAY' . $field;
  1536. }
  1537. $encoding = $encoding ? trim($encoding) : 'US-ASCII';
  1538. $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL';
  1539. list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
  1540. array("($field)", $encoding, $criteria));
  1541. if ($code != self::ERROR_OK) {
  1542. $response = null;
  1543. }
  1544. return new rcube_result_index($mailbox, $response);
  1545. }
  1546. /**
  1547. * Executes THREAD command
  1548. *
  1549. * @param string $mailbox Mailbox name
  1550. * @param string $algorithm Threading algorithm (ORDEREDSUBJECT, REFERENCES, REFS)
  1551. * @param string $criteria Searching criteria
  1552. * @param bool $return_uid Enables UIDs in result instead of sequence numbers
  1553. * @param string $encoding Character set
  1554. *
  1555. * @return rcube_result_thread Thread data
  1556. */
  1557. public function thread($mailbox, $algorithm = 'REFERENCES', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
  1558. {
  1559. $old_sel = $this->selected;
  1560. if (!$this->select($mailbox)) {
  1561. return new rcube_result_thread($mailbox);
  1562. }
  1563. // return empty result when folder is empty and we're just after SELECT
  1564. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1565. return new rcube_result_thread($mailbox, '* THREAD');
  1566. }
  1567. $encoding = $encoding ? trim($encoding) : 'US-ASCII';
  1568. $algorithm = $algorithm ? trim($algorithm) : 'REFERENCES';
  1569. $criteria = $criteria ? 'ALL '.trim($criteria) : 'ALL';
  1570. list($code, $response) = $this->execute($return_uid ? 'UID THREAD' : 'THREAD',
  1571. array($algorithm, $encoding, $criteria));
  1572. if ($code != self::ERROR_OK) {
  1573. $response = null;
  1574. }
  1575. return new rcube_result_thread($mailbox, $response);
  1576. }
  1577. /**
  1578. * Executes SEARCH command
  1579. *
  1580. * @param string $mailbox Mailbox name
  1581. * @param string $criteria Searching criteria
  1582. * @param bool $return_uid Enable UID in result instead of sequence ID
  1583. * @param array $items Return items (MIN, MAX, COUNT, ALL)
  1584. *
  1585. * @return rcube_result_index Result data
  1586. */
  1587. public function search($mailbox, $criteria, $return_uid = false, $items = array())
  1588. {
  1589. $old_sel = $this->selected;
  1590. if (!$this->select($mailbox)) {
  1591. return new rcube_result_index($mailbox);
  1592. }
  1593. // return empty result when folder is empty and we're just after SELECT
  1594. if ($old_sel != $mailbox && !$this->data['EXISTS']) {
  1595. return new rcube_result_index($mailbox, '* SEARCH');
  1596. }
  1597. // If ESEARCH is supported always use ALL
  1598. // but not when items are specified or using simple id2uid search
  1599. if (empty($items) && preg_match('/[^0-9]/', $criteria)) {
  1600. $items = array('ALL');
  1601. }
  1602. $esearch = empty($items) ? false : $this->getCapability('ESEARCH');
  1603. $criteria = trim($criteria);
  1604. $params = '';
  1605. // RFC4731: ESEARCH
  1606. if (!empty($items) && $esearch) {
  1607. $params .= 'RETURN (' . implode(' ', $items) . ')';
  1608. }
  1609. if (!empty($criteria)) {
  1610. $params .= ($params ? ' ' : '') . $criteria;
  1611. }
  1612. else {
  1613. $params .= 'ALL';
  1614. }
  1615. list($code, $response) = $this->execute($return_uid ? 'UID SEARCH' : 'SEARCH',
  1616. array($params));
  1617. if ($code != self::ERROR_OK) {
  1618. $response = null;
  1619. }
  1620. return new rcube_result_index($mailbox, $response);
  1621. }
  1622. /**
  1623. * Simulates SORT command by using FETCH and sorting.
  1624. *
  1625. * @param string $mailbox Mailbox name
  1626. * @param string|array $message_set Searching criteria (list of messages to return)
  1627. * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
  1628. * @param bool $skip_deleted Makes that DELETED messages will be skipped
  1629. * @param bool $uidfetch Enables UID FETCH usage
  1630. * @param bool $return_uid Enables returning UIDs instead of IDs
  1631. *
  1632. * @return rcube_result_index Response data
  1633. */
  1634. public function index($mailbox, $message_set, $index_field='', $skip_deleted=true,
  1635. $uidfetch=false, $return_uid=false)
  1636. {
  1637. $msg_index = $this->fetchHeaderIndex($mailbox, $message_set,
  1638. $index_field, $skip_deleted, $uidfetch, $return_uid);
  1639. if (!empty($msg_index)) {
  1640. asort($msg_index); // ASC
  1641. $msg_index = array_keys($msg_index);
  1642. $msg_index = '* SEARCH ' . implode(' ', $msg_index);
  1643. }
  1644. else {
  1645. $msg_index = is_array($msg_index) ? '* SEARCH' : null;
  1646. }
  1647. return new rcube_result_index($mailbox, $msg_index);
  1648. }
  1649. /**
  1650. * Fetches specified header/data value for a set of messages.
  1651. *
  1652. * @param string $mailbox Mailbox name
  1653. * @param string|array $message_set Searching criteria (list of messages to return)
  1654. * @param string $index_field Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
  1655. * @param bool $skip_deleted Makes that DELETED messages will be skipped
  1656. * @param bool $uidfetch Enables UID FETCH usage
  1657. * @param bool $return_uid Enables returning UIDs instead of IDs
  1658. *
  1659. * @return array|bool List of header values or False on failure
  1660. */
  1661. public function fetchHeaderIndex($mailbox, $message_set, $index_field = '', $skip_deleted = true,
  1662. $uidfetch = false, $return_uid = false)
  1663. {
  1664. if (is_array($message_set)) {
  1665. if (!($message_set = $this->compressMessageSet($message_set))) {
  1666. return false;
  1667. }
  1668. }
  1669. else {
  1670. list($from_idx, $to_idx) = explode(':', $message_set);
  1671. if (empty($message_set) ||
  1672. (isset($to_idx) && $to_idx != '*' && (int)$from_idx > (int)$to_idx)
  1673. ) {
  1674. return false;
  1675. }
  1676. }
  1677. $index_field = empty($index_field) ? 'DATE' : strtoupper($index_field);
  1678. $fields_a['DATE'] = 1;
  1679. $fields_a['INTERNALDATE'] = 4;
  1680. $fields_a['ARRIVAL'] = 4;
  1681. $fields_a['FROM'] = 1;
  1682. $fields_a['REPLY-TO'] = 1;
  1683. $fields_a['SENDER'] = 1;
  1684. $fields_a['TO'] = 1;
  1685. $fields_a['CC'] = 1;
  1686. $fields_a['SUBJECT'] = 1;
  1687. $fields_a['UID'] = 2;
  1688. $fields_a['SIZE'] = 2;
  1689. $fields_a['SEEN'] = 3;
  1690. $fields_a['RECENT'] = 3;
  1691. $fields_a['DELETED'] = 3;
  1692. if (!($mode = $fields_a[$index_field])) {
  1693. return false;
  1694. }
  1695. // Select the mailbox
  1696. if (!$this->select($mailbox)) {
  1697. return false;
  1698. }
  1699. // build FETCH command string
  1700. $key = $this->nextTag();
  1701. $cmd = $uidfetch ? 'UID FETCH' : 'FETCH';
  1702. $fields = array();
  1703. if ($return_uid) {
  1704. $fields[] = 'UID';
  1705. }
  1706. if ($skip_deleted) {
  1707. $fields[] = 'FLAGS';
  1708. }
  1709. if ($mode == 1) {
  1710. if ($index_field == 'DATE') {
  1711. $fields[] = 'INTERNALDATE';
  1712. }
  1713. $fields[] = "BODY.PEEK[HEADER.FIELDS ($index_field)]";
  1714. }
  1715. else if ($mode == 2) {
  1716. if ($index_field == 'SIZE') {
  1717. $fields[] = 'RFC822.SIZE';
  1718. }
  1719. else if (!$return_uid || $index_field != 'UID') {
  1720. $fields[] = $index_field;
  1721. }
  1722. }
  1723. else if ($mode == 3 && !$skip_deleted) {
  1724. $fields[] = 'FLAGS';
  1725. }
  1726. else if ($mode == 4) {
  1727. $fields[] = 'INTERNALDATE';
  1728. }
  1729. $request = "$key $cmd $message_set (" . implode(' ', $fields) . ")";
  1730. if (!$this->putLine($request)) {
  1731. $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
  1732. return false;
  1733. }
  1734. $result = array();
  1735. do {
  1736. $line = rtrim($this->readLine(200));
  1737. $line = $this->multLine($line);
  1738. if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
  1739. $id = $m[1];
  1740. $flags = null;
  1741. if ($return_uid) {
  1742. if (preg_match('/UID ([0-9]+)/', $line, $matches)) {
  1743. $id = (int) $matches[1];
  1744. }
  1745. else {
  1746. continue;
  1747. }
  1748. }
  1749. if ($skip_deleted && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
  1750. $flags = explode(' ', strtoupper($matches[1]));
  1751. if (in_array('\\DELETED', $flags)) {
  1752. continue;
  1753. }
  1754. }
  1755. if ($mode == 1 && $index_field == 'DATE') {
  1756. if (preg_match('/BODY\[HEADER\.FIELDS \("*DATE"*\)\] (.*)/', $line, $matches)) {
  1757. $value = preg_replace(array('/^"*[a-z]+:/i'), '', $matches[1]);
  1758. $value = trim($value);
  1759. $result[$id] = rcube_utils::strtotime($value);
  1760. }
  1761. // non-existent/empty Date: header, use INTERNALDATE
  1762. if (empty($result[$id])) {
  1763. if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
  1764. $result[$id] = rcube_utils::strtotime($matches[1]);
  1765. }
  1766. else {
  1767. $result[$id] = 0;
  1768. }
  1769. }
  1770. }
  1771. else if ($mode == 1) {
  1772. if (preg_match('/BODY\[HEADER\.FIELDS \("?(FROM|REPLY-TO|SENDER|TO|SUBJECT)"?\)\] (.*)/', $line, $matches)) {
  1773. $value = preg_replace(array('/^"*[a-z]+:/i', '/\s+$/sm'), array('', ''), $matches[2]);
  1774. $result[$id] = trim($value);
  1775. }
  1776. else {
  1777. $result[$id] = '';
  1778. }
  1779. }
  1780. else if ($mode == 2) {
  1781. if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) {
  1782. $result[$id] = trim($matches[1]);
  1783. }
  1784. else {
  1785. $result[$id] = 0;
  1786. }
  1787. }
  1788. else if ($mode == 3) {
  1789. if (!$flags && preg_match('/FLAGS \(([^)]+)\)/', $line, $matches)) {
  1790. $flags = explode(' ', $matches[1]);
  1791. }
  1792. $result[$id] = in_array("\\".$index_field, (array) $flags) ? 1 : 0;
  1793. }
  1794. else if ($mode == 4) {
  1795. if (preg_match('/INTERNALDATE "([^"]+)"/', $line, $matches)) {
  1796. $result[$id] = rcube_utils::strtotime($matches[1]);
  1797. }
  1798. else {
  1799. $result[$id] = 0;
  1800. }
  1801. }
  1802. }
  1803. }
  1804. while (!$this->startsWith($line, $key, true, true));
  1805. return $result;
  1806. }
  1807. /**
  1808. * Returns message sequence identifier
  1809. *
  1810. * @param string $mailbox Mailbox name
  1811. * @param int $uid Message unique identifier (UID)
  1812. *
  1813. * @return int Message sequence identifier
  1814. */
  1815. public function UID2ID($mailbox, $uid)
  1816. {
  1817. if ($uid > 0) {
  1818. $index = $this->search($mailbox, "UID $uid");
  1819. if ($index->count() == 1) {
  1820. $arr = $index->get();
  1821. return (int) $arr[0];
  1822. }
  1823. }
  1824. }
  1825. /**
  1826. * Returns message unique identifier (UID)
  1827. *
  1828. * @param string $mailbox Mailbox name
  1829. * @param int $uid Message sequence identifier
  1830. *
  1831. * @return int Message unique identifier
  1832. */
  1833. public function ID2UID($mailbox, $id)
  1834. {
  1835. if (empty($id) || $id < 0) {
  1836. return null;
  1837. }
  1838. if (!$this->select($mailbox)) {
  1839. return null;
  1840. }
  1841. if ($uid = $this->data['UID-MAP'][$id]) {
  1842. return $uid;
  1843. }
  1844. if (isset($this->data['EXISTS']) && $id > $this->data['EXISTS']) {
  1845. return null;
  1846. }
  1847. $index = $this->search($mailbox, $id, true);
  1848. if ($index->count() == 1) {
  1849. $arr = $index->get();
  1850. return $this->data['UID-MAP'][$id] = (int) $arr[0];
  1851. }
  1852. }
  1853. /**
  1854. * Sets flag of the message(s)
  1855. *
  1856. * @param string $mailbox Mailbox name
  1857. * @param string|array $messages Message UID(s)
  1858. * @param string $flag Flag name
  1859. *
  1860. * @return bool True on success, False on failure
  1861. */
  1862. public function flag($mailbox, $messages, $flag)
  1863. {
  1864. return $this->modFlag($mailbox, $messages, $flag, '+');
  1865. }
  1866. /**
  1867. * Unsets flag of the message(s)
  1868. *
  1869. * @param string $mailbox Mailbox name
  1870. * @param string|array $messages Message UID(s)
  1871. * @param string $flag Flag name
  1872. *
  1873. * @return bool True on success, False on failure
  1874. */
  1875. public function unflag($mailbox, $messages, $flag)
  1876. {
  1877. return $this->modFlag($mailbox, $messages, $flag, '-');
  1878. }
  1879. /**
  1880. * Changes flag of the message(s)
  1881. *
  1882. * @param string $mailbox Mailbox name
  1883. * @param string|array $messages Message UID(s)
  1884. * @param string $flag Flag name
  1885. * @param string $mod Modifier [+|-]. Default: "+".
  1886. *
  1887. * @return bool True on success, False on failure
  1888. */
  1889. protected function modFlag($mailbox, $messages, $flag, $mod = '+')
  1890. {
  1891. if (!$flag) {
  1892. return false;
  1893. }
  1894. if (!$this->select($mailbox)) {
  1895. return false;
  1896. }
  1897. if (!$this->data['READ-WRITE']) {
  1898. $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
  1899. return false;
  1900. }
  1901. if ($this->flags[strtoupper($flag)]) {
  1902. $flag = $this->flags[strtoupper($flag)];
  1903. }
  1904. // if PERMANENTFLAGS is not specified all flags are allowed
  1905. if (!empty($this->data['PERMANENTFLAGS'])
  1906. && !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
  1907. && !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
  1908. ) {
  1909. return false;
  1910. }
  1911. // Clear internal status cache
  1912. if ($flag == 'SEEN') {
  1913. unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
  1914. }
  1915. if ($mod != '+' && $mod != '-') {
  1916. $mod = '+';
  1917. }
  1918. $result = $this->execute('UID STORE', array(
  1919. $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
  1920. self::COMMAND_NORESPONSE);
  1921. return $result == self::ERROR_OK;
  1922. }
  1923. /**
  1924. * Copies message(s) from one folder to another
  1925. *
  1926. * @param string|array $messages Message UID(s)
  1927. * @param string $from Mailbox name
  1928. * @param string $to Destination mailbox name
  1929. *
  1930. * @return bool True on success, False on failure
  1931. */
  1932. public function copy($messages, $from, $to)
  1933. {
  1934. // Clear last COPYUID data
  1935. unset($this->data['COPYUID']);
  1936. if (!$this->select($from)) {
  1937. return false;
  1938. }
  1939. // Clear internal status cache
  1940. unset($this->data['STATUS:'.$to]);
  1941. $result = $this->execute('UID COPY', array(
  1942. $this->compressMessageSet($messages), $this->escape($to)),
  1943. self::COMMAND_NORESPONSE);
  1944. return $result == self::ERROR_OK;
  1945. }
  1946. /**
  1947. * Moves message(s) from one folder to another.
  1948. *
  1949. * @param string|array $messages Message UID(s)
  1950. * @param string $from Mailbox name
  1951. * @param string $to Destination mailbox name
  1952. *
  1953. * @return bool True on success, False on failure
  1954. */
  1955. public function move($messages, $from, $to)
  1956. {
  1957. if (!$this->select($from)) {
  1958. return false;
  1959. }
  1960. if (!$this->data['READ-WRITE']) {
  1961. $this->setError(self::ERROR_READONLY, "Mailbox is read-only");
  1962. return false;
  1963. }
  1964. // use MOVE command (RFC 6851)
  1965. if ($this->hasCapability('MOVE')) {
  1966. // Clear last COPYUID data
  1967. unset($this->data['COPYUID']);
  1968. // Clear internal status cache
  1969. unset($this->data['STATUS:'.$to]);
  1970. $this->clear_status_cache($from);
  1971. $result = $this->execute('UID MOVE', array(
  1972. $this->compressMessageSet($messages), $this->escape($to)),
  1973. self::COMMAND_NORESPONSE);
  1974. return $result == self::ERROR_OK;
  1975. }
  1976. // use COPY + STORE +FLAGS.SILENT \Deleted + EXPUNGE
  1977. $result = $this->copy($messages, $from, $to);
  1978. if ($result) {
  1979. // Clear internal status cache
  1980. unset($this->data['STATUS:'.$from]);
  1981. $result = $this->flag($from, $messages, 'DELETED');
  1982. if ($messages == '*') {
  1983. // CLOSE+SELECT should be faster than EXPUNGE
  1984. $this->close();
  1985. }
  1986. else {
  1987. $this->expunge($from, $messages);
  1988. }
  1989. }
  1990. return $result;
  1991. }
  1992. /**
  1993. * FETCH command (RFC3501)
  1994. *
  1995. * @param string $mailbox Mailbox name
  1996. * @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
  1997. * @param bool $is_uid True if $message_set contains UIDs
  1998. * @param array $query_items FETCH command data items
  1999. * @param string $mod_seq Modification sequence for CHANGEDSINCE (RFC4551) query
  2000. * @param bool $vanished Enables VANISHED parameter (RFC5162) for CHANGEDSINCE query
  2001. *
  2002. * @return array List of rcube_message_header elements, False on error
  2003. * @since 0.6
  2004. */
  2005. public function fetch($mailbox, $message_set, $is_uid = false, $query_items = array(),
  2006. $mod_seq = null, $vanished = false)
  2007. {
  2008. if (!$this->select($mailbox)) {
  2009. return false;
  2010. }
  2011. $message_set = $this->compressMessageSet($message_set);
  2012. $result = array();
  2013. $key = $this->nextTag();
  2014. $cmd = ($is_uid ? 'UID ' : '') . 'FETCH';
  2015. $request = "$key $cmd $message_set (" . implode(' ', $query_items) . ")";
  2016. if ($mod_seq !== null && $this->hasCapability('CONDSTORE')) {
  2017. $request .= " (CHANGEDSINCE $mod_seq" . ($vanished ? " VANISHED" : '') .")";
  2018. }
  2019. if (!$this->putLine($request)) {
  2020. $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
  2021. return false;
  2022. }
  2023. do {
  2024. $line = $this->readLine(4096);
  2025. if (!$line) {
  2026. break;
  2027. }
  2028. // Sample reply line:
  2029. // * 321 FETCH (UID 2417 RFC822.SIZE 2730 FLAGS (\Seen)
  2030. // INTERNALDATE "16-Nov-2008 21:08:46 +0100" BODYSTRUCTURE (...)
  2031. // BODY[HEADER.FIELDS ...
  2032. if (preg_match('/^\* ([0-9]+) FETCH/', $line, $m)) {
  2033. $id = intval($m[1]);
  2034. $result[$id] = new rcube_message_header;
  2035. $result[$id]->id = $id;
  2036. $result[$id]->subject = '';
  2037. $result[$id]->messageID = 'mid:' . $id;
  2038. $headers = null;
  2039. $lines = array();
  2040. $line = substr($line, strlen($m[0]) + 2);
  2041. $ln = 0;
  2042. // get complete entry
  2043. while (preg_match('/\{([0-9]+)\}\r\n$/', $line, $m)) {
  2044. $bytes = $m[1];
  2045. $out = '';
  2046. while (strlen($out) < $bytes) {
  2047. $out = $this->readBytes($bytes);
  2048. if ($out === null) {
  2049. break;
  2050. }
  2051. $line .= $out;
  2052. }
  2053. $str = $this->readLine(4096);
  2054. if ($str === false) {
  2055. break;
  2056. }
  2057. $line .= $str;
  2058. }
  2059. // Tokenize response and assign to object properties
  2060. while (list($name, $value) = $this->tokenizeResponse($line, 2)) {
  2061. if ($name == 'UID') {
  2062. $result[$id]->uid = intval($value);
  2063. }
  2064. else if ($name == 'RFC822.SIZE') {
  2065. $result[$id]->size = intval($value);
  2066. }
  2067. else if ($name == 'RFC822.TEXT') {
  2068. $result[$id]->body = $value;
  2069. }
  2070. else if ($name == 'INTERNALDATE') {
  2071. $result[$id]->internaldate = $value;
  2072. $result[$id]->date = $value;
  2073. $result[$id]->timestamp = rcube_utils::strtotime($value);
  2074. }
  2075. else if ($name == 'FLAGS') {
  2076. if (!empty($value)) {
  2077. foreach ((array)$value as $flag) {
  2078. $flag = str_replace(array('$', "\\"), '', $flag);
  2079. $flag = strtoupper($flag);
  2080. $result[$id]->flags[$flag] = true;
  2081. }
  2082. }
  2083. }
  2084. else if ($name == 'MODSEQ') {
  2085. $result[$id]->modseq = $value[0];
  2086. }
  2087. else if ($name == 'ENVELOPE') {
  2088. $result[$id]->envelope = $value;
  2089. }
  2090. else if ($name == 'BODYSTRUCTURE' || ($name == 'BODY' && count($value) > 2)) {
  2091. if (!is_array($value[0]) && (strtolower($value[0]) == 'message' && strtolower($value[1]) == 'rfc822')) {
  2092. $value = array($value);
  2093. }
  2094. $result[$id]->bodystructure = $value;
  2095. }
  2096. else if ($name == 'RFC822') {
  2097. $result[$id]->body = $value;
  2098. }
  2099. else if (stripos($name, 'BODY[') === 0) {
  2100. $name = str_replace(']', '', substr($name, 5));
  2101. if ($name == 'HEADER.FIELDS') {
  2102. // skip ']' after headers list
  2103. $this->tokenizeResponse($line, 1);
  2104. $headers = $this->tokenizeResponse($line, 1);
  2105. }
  2106. else if (strlen($name)) {
  2107. $result[$id]->bodypart[$name] = $value;
  2108. }
  2109. else {
  2110. $result[$id]->body = $value;
  2111. }
  2112. }
  2113. }
  2114. // create array with header field:data
  2115. if (!empty($headers)) {
  2116. $headers = explode("\n", trim($headers));
  2117. foreach ($headers as $resln) {
  2118. if (ord($resln[0]) <= 32) {
  2119. $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . trim($resln);
  2120. }
  2121. else {
  2122. $lines[++$ln] = trim($resln);
  2123. }
  2124. }
  2125. foreach ($lines as $str) {
  2126. list($field, $string) = explode(':', $str, 2);
  2127. $field = strtolower($field);
  2128. $string = preg_replace('/\n[\t\s]*/', ' ', trim($string));
  2129. switch ($field) {
  2130. case 'date';
  2131. $result[$id]->date = $string;
  2132. $result[$id]->timestamp = rcube_utils::strtotime($string);
  2133. break;
  2134. case 'to':
  2135. $result[$id]->to = preg_replace('/undisclosed-recipients:[;,]*/', '', $string);
  2136. break;
  2137. case 'from':
  2138. case 'subject':
  2139. case 'cc':
  2140. case 'bcc':
  2141. case 'references':
  2142. $result[$id]->{$field} = $string;
  2143. break;
  2144. case 'reply-to':
  2145. $result[$id]->replyto = $string;
  2146. break;
  2147. case 'content-transfer-encoding':
  2148. $result[$id]->encoding = $string;
  2149. break;
  2150. case 'content-type':
  2151. $ctype_parts = preg_split('/[; ]+/', $string);
  2152. $result[$id]->ctype = strtolower(array_shift($ctype_parts));
  2153. if (preg_match('/charset\s*=\s*"?([a-z0-9\-\.\_]+)"?/i', $string, $regs)) {
  2154. $result[$id]->charset = $regs[1];
  2155. }
  2156. break;
  2157. case 'in-reply-to':
  2158. $result[$id]->in_reply_to = str_replace(array("\n", '<', '>'), '', $string);
  2159. break;
  2160. case 'return-receipt-to':
  2161. case 'disposition-notification-to':
  2162. case 'x-confirm-reading-to':
  2163. $result[$id]->mdn_to = $string;
  2164. break;
  2165. case 'message-id':
  2166. $result[$id]->messageID = $string;
  2167. break;
  2168. case 'x-priority':
  2169. if (preg_match('/^(\d+)/', $string, $matches)) {
  2170. $result[$id]->priority = intval($matches[1]);
  2171. }
  2172. break;
  2173. default:
  2174. if (strlen($field) < 3) {
  2175. break;
  2176. }
  2177. if ($result[$id]->others[$field]) {
  2178. $string = array_merge((array)$result[$id]->others[$field], (array)$string);
  2179. }
  2180. $result[$id]->others[$field] = $string;
  2181. }
  2182. }
  2183. }
  2184. }
  2185. // VANISHED response (QRESYNC RFC5162)
  2186. // Sample: * VANISHED (EARLIER) 300:310,405,411
  2187. else if (preg_match('/^\* VANISHED [()EARLIER]*/i', $line, $match)) {
  2188. $line = substr($line, strlen($match[0]));
  2189. $v_data = $this->tokenizeResponse($line, 1);
  2190. $this->data['VANISHED'] = $v_data;
  2191. }
  2192. }
  2193. while (!$this->startsWith($line, $key, true));
  2194. return $result;
  2195. }
  2196. /**
  2197. * Returns message(s) data (flags, headers, etc.)
  2198. *
  2199. * @param string $mailbox Mailbox name
  2200. * @param mixed $message_set Message(s) sequence identifier(s) or UID(s)
  2201. * @param bool $is_uid True if $message_set contains UIDs
  2202. * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
  2203. * @param array $add_headers List of additional headers
  2204. *
  2205. * @return bool|array List of rcube_message_header elements, False on error
  2206. */
  2207. public function fetchHeaders($mailbox, $message_set, $is_uid = false, $bodystr = false, $add_headers = array())
  2208. {
  2209. $query_items = array('UID', 'RFC822.SIZE', 'FLAGS', 'INTERNALDATE');
  2210. $headers = array('DATE', 'FROM', 'TO', 'SUBJECT', 'CONTENT-TYPE', 'CC', 'REPLY-TO',
  2211. 'LIST-POST', 'DISPOSITION-NOTIFICATION-TO', 'X-PRIORITY');
  2212. if (!empty($add_headers)) {
  2213. $add_headers = array_map('strtoupper', $add_headers);
  2214. $headers = array_unique(array_merge($headers, $add_headers));
  2215. }
  2216. if ($bodystr) {
  2217. $query_items[] = 'BODYSTRUCTURE';
  2218. }
  2219. $query_items[] = 'BODY.PEEK[HEADER.FIELDS (' . implode(' ', $headers) . ')]';
  2220. return $this->fetch($mailbox, $message_set, $is_uid, $query_items);
  2221. }
  2222. /**
  2223. * Returns message data (flags, headers, etc.)
  2224. *
  2225. * @param string $mailbox Mailbox name
  2226. * @param int $id Message sequence identifier or UID
  2227. * @param bool $is_uid True if $id is an UID
  2228. * @param bool $bodystr Enable to add BODYSTRUCTURE data to the result
  2229. * @param array $add_headers List of additional headers
  2230. *
  2231. * @return bool|rcube_message_header Message data, False on error
  2232. */
  2233. public function fetchHeader($mailbox, $id, $is_uid = false, $bodystr = false, $add_headers = array())
  2234. {
  2235. $a = $this->fetchHeaders($mailbox, $id, $is_uid, $bodystr, $add_headers);
  2236. if (is_array($a)) {
  2237. return array_shift($a);
  2238. }
  2239. return false;
  2240. }
  2241. /**
  2242. * Sort messages by specified header field
  2243. *
  2244. * @param array $messages Array of rcube_message_header objects
  2245. * @param string $field Name of the property to sort by
  2246. * @param string $flag Sorting order (ASC|DESC)
  2247. *
  2248. * @return array Sorted input array
  2249. */
  2250. public static function sortHeaders($messages, $field, $flag)
  2251. {
  2252. // Strategy: First, we'll create an "index" array.
  2253. // Then, we'll use sort() on that array, and use that to sort the main array.
  2254. $field = empty($field) ? 'uid' : strtolower($field);
  2255. $flag = empty($flag) ? 'ASC' : strtoupper($flag);
  2256. $index = array();
  2257. $result = array();
  2258. reset($messages);
  2259. while (list($key, $headers) = each($messages)) {
  2260. $value = null;
  2261. switch ($field) {
  2262. case 'arrival':
  2263. $field = 'internaldate';
  2264. case 'date':
  2265. case 'internaldate':
  2266. case 'timestamp':
  2267. $value = rcube_utils::strtotime($headers->$field);
  2268. if (!$value && $field != 'timestamp') {
  2269. $value = $headers->timestamp;
  2270. }
  2271. break;
  2272. default:
  2273. // @TODO: decode header value, convert to UTF-8
  2274. $value = $headers->$field;
  2275. if (is_string($value)) {
  2276. $value = str_replace('"', '', $value);
  2277. if ($field == 'subject') {
  2278. $value = preg_replace('/^(Re:\s*|Fwd:\s*|Fw:\s*)+/i', '', $value);
  2279. }
  2280. $data = strtoupper($value);
  2281. }
  2282. }
  2283. $index[$key] = $value;
  2284. }
  2285. if (!empty($index)) {
  2286. // sort index
  2287. if ($flag == 'ASC') {
  2288. asort($index);
  2289. }
  2290. else {
  2291. arsort($index);
  2292. }
  2293. // form new array based on index
  2294. while (list($key, $val) = each($index)) {
  2295. $result[$key] = $messages[$key];
  2296. }
  2297. }
  2298. return $result;
  2299. }
  2300. /**
  2301. * Fetch MIME headers of specified message parts
  2302. *
  2303. * @param string $mailbox Mailbox name
  2304. * @param int $uid Message UID
  2305. * @param array $parts Message part identifiers
  2306. * @param bool $mime Use MIME instad of HEADER
  2307. *
  2308. * @return array|bool Array containing headers string for each specified body
  2309. * False on failure.
  2310. */
  2311. public function fetchMIMEHeaders($mailbox, $uid, $parts, $mime = true)
  2312. {
  2313. if (!$this->select($mailbox)) {
  2314. return false;
  2315. }
  2316. $result = false;
  2317. $parts = (array) $parts;
  2318. $key = $this->nextTag();
  2319. $peeks = array();
  2320. $type = $mime ? 'MIME' : 'HEADER';
  2321. // format request
  2322. foreach ($parts as $part) {
  2323. $peeks[] = "BODY.PEEK[$part.$type]";
  2324. }
  2325. $request = "$key UID FETCH $uid (" . implode(' ', $peeks) . ')';
  2326. // send request
  2327. if (!$this->putLine($request)) {
  2328. $this->setError(self::ERROR_COMMAND, "Failed to send UID FETCH command");
  2329. return false;
  2330. }
  2331. do {
  2332. $line = $this->readLine(1024);
  2333. if (preg_match('/^\* [0-9]+ FETCH [0-9UID( ]+/', $line, $m)) {
  2334. $line = ltrim(substr($line, strlen($m[0])));
  2335. while (preg_match('/^BODY\[([0-9\.]+)\.'.$type.'\]/', $line, $matches)) {
  2336. $line = substr($line, strlen($matches[0]));
  2337. $result[$matches[1]] = trim($this->multLine($line));
  2338. $line = $this->readLine(1024);
  2339. }
  2340. }
  2341. }
  2342. while (!$this->startsWith($line, $key, true));
  2343. return $result;
  2344. }
  2345. /**
  2346. * Fetches message part header
  2347. */
  2348. public function fetchPartHeader($mailbox, $id, $is_uid = false, $part = null)
  2349. {
  2350. $part = empty($part) ? 'HEADER' : $part.'.MIME';
  2351. return $this->handlePartBody($mailbox, $id, $is_uid, $part);
  2352. }
  2353. /**
  2354. * Fetches body of the specified message part
  2355. */
  2356. public function handlePartBody($mailbox, $id, $is_uid=false, $part='', $encoding=null, $print=null, $file=null, $formatted=false, $max_bytes=0)
  2357. {
  2358. if (!$this->select($mailbox)) {
  2359. return false;
  2360. }
  2361. $binary = true;
  2362. do {
  2363. if (!$initiated) {
  2364. switch ($encoding) {
  2365. case 'base64':
  2366. $mode = 1;
  2367. break;
  2368. case 'quoted-printable':
  2369. $mode = 2;
  2370. break;
  2371. case 'x-uuencode':
  2372. case 'x-uue':
  2373. case 'uue':
  2374. case 'uuencode':
  2375. $mode = 3;
  2376. break;
  2377. default:
  2378. $mode = 0;
  2379. }
  2380. // Use BINARY extension when possible (and safe)
  2381. $binary = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
  2382. $fetch_mode = $binary ? 'BINARY' : 'BODY';
  2383. $partial = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
  2384. // format request
  2385. $key = $this->nextTag();
  2386. $cmd = ($is_uid ? 'UID ' : '') . 'FETCH';
  2387. $request = "$key $cmd $id ($fetch_mode.PEEK[$part]$partial)";
  2388. $result = false;
  2389. $found = false;
  2390. $initiated = true;
  2391. // send request
  2392. if (!$this->putLine($request)) {
  2393. $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
  2394. return false;
  2395. }
  2396. if ($binary) {
  2397. // WARNING: Use $formatted argument with care, this may break binary data stream
  2398. $mode = -1;
  2399. }
  2400. }
  2401. $line = trim($this->readLine(1024));
  2402. if (!$line) {
  2403. break;
  2404. }
  2405. // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
  2406. if ($binary && !$found && preg_match('/^' . $key . ' NO \[UNKNOWN-CTE\]/i', $line)) {
  2407. $binary = $initiated = false;
  2408. continue;
  2409. }
  2410. // skip irrelevant untagged responses (we have a result already)
  2411. if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
  2412. continue;
  2413. }
  2414. $line = $m[2];
  2415. // handle one line response
  2416. if ($line[0] == '(' && substr($line, -1) == ')') {
  2417. // tokenize content inside brackets
  2418. // the content can be e.g.: (UID 9844 BODY[2.4] NIL)
  2419. $tokens = $this->tokenizeResponse(preg_replace('/(^\(|\)$)/', '', $line));
  2420. for ($i=0; $i<count($tokens); $i+=2) {
  2421. if (preg_match('/^(BODY|BINARY)/i', $tokens[$i])) {
  2422. $result = $tokens[$i+1];
  2423. $found = true;
  2424. break;
  2425. }
  2426. }
  2427. if ($result !== false) {
  2428. if ($mode == 1) {
  2429. $result = base64_decode($result);
  2430. }
  2431. else if ($mode == 2) {
  2432. $result = quoted_printable_decode($result);
  2433. }
  2434. else if ($mode == 3) {
  2435. $result = convert_uudecode($result);
  2436. }
  2437. }
  2438. }
  2439. // response with string literal
  2440. else if (preg_match('/\{([0-9]+)\}$/', $line, $m)) {
  2441. $bytes = (int) $m[1];
  2442. $prev = '';
  2443. $found = true;
  2444. // empty body
  2445. if (!$bytes) {
  2446. $result = '';
  2447. }
  2448. else while ($bytes > 0) {
  2449. $line = $this->readLine(8192);
  2450. if ($line === null) {
  2451. break;
  2452. }
  2453. $len = strlen($line);
  2454. if ($len > $bytes) {
  2455. $line = substr($line, 0, $bytes);
  2456. $len = strlen($line);
  2457. }
  2458. $bytes -= $len;
  2459. // BASE64
  2460. if ($mode == 1) {
  2461. $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line);
  2462. // create chunks with proper length for base64 decoding
  2463. $line = $prev.$line;
  2464. $length = strlen($line);
  2465. if ($length % 4) {
  2466. $length = floor($length / 4) * 4;
  2467. $prev = substr($line, $length);
  2468. $line = substr($line, 0, $length);
  2469. }
  2470. else {
  2471. $prev = '';
  2472. }
  2473. $line = base64_decode($line);
  2474. }
  2475. // QUOTED-PRINTABLE
  2476. else if ($mode == 2) {
  2477. $line = rtrim($line, "\t\r\0\x0B");
  2478. $line = quoted_printable_decode($line);
  2479. }
  2480. // UUENCODE
  2481. else if ($mode == 3) {
  2482. $line = rtrim($line, "\t\r\n\0\x0B");
  2483. if ($line == 'end' || preg_match('/^begin\s+[0-7]+\s+.+$/', $line)) {
  2484. continue;
  2485. }
  2486. $line = convert_uudecode($line);
  2487. }
  2488. // default
  2489. else if ($formatted) {
  2490. $line = rtrim($line, "\t\r\n\0\x0B") . "\n";
  2491. }
  2492. if ($file) {
  2493. if (fwrite($file, $line) === false) {
  2494. break;
  2495. }
  2496. }
  2497. else if ($print) {
  2498. echo $line;
  2499. }
  2500. else {
  2501. $result .= $line;
  2502. }
  2503. }
  2504. }
  2505. }
  2506. while (!$this->startsWith($line, $key, true) || !$initiated);
  2507. if ($result !== false) {
  2508. if ($file) {
  2509. return fwrite($file, $result);
  2510. }
  2511. else if ($print) {
  2512. echo $result;
  2513. return true;
  2514. }
  2515. return $result;
  2516. }
  2517. return false;
  2518. }
  2519. /**
  2520. * Handler for IMAP APPEND command
  2521. *
  2522. * @param string $mailbox Mailbox name
  2523. * @param string|array $message The message source string or array (of strings and file pointers)
  2524. * @param array $flags Message flags
  2525. * @param string $date Message internal date
  2526. * @param bool $binary Enable BINARY append (RFC3516)
  2527. *
  2528. * @return string|bool On success APPENDUID response (if available) or True, False on failure
  2529. */
  2530. public function append($mailbox, &$message, $flags = array(), $date = null, $binary = false)
  2531. {
  2532. unset($this->data['APPENDUID']);
  2533. if ($mailbox === null || $mailbox === '') {
  2534. return false;
  2535. }
  2536. $binary = $binary && $this->getCapability('BINARY');
  2537. $literal_plus = !$binary && $this->prefs['literal+'];
  2538. $len = 0;
  2539. $msg = is_array($message) ? $message : array(&$message);
  2540. $chunk_size = 512000;
  2541. for ($i=0, $cnt=count($msg); $i<$cnt; $i++) {
  2542. if (is_resource($msg[$i])) {
  2543. $stat = fstat($msg[$i]);
  2544. if ($stat === false) {
  2545. return false;
  2546. }
  2547. $len += $stat['size'];
  2548. }
  2549. else {
  2550. if (!$binary) {
  2551. $msg[$i] = str_replace("\r", '', $msg[$i]);
  2552. $msg[$i] = str_replace("\n", "\r\n", $msg[$i]);
  2553. }
  2554. $len += strlen($msg[$i]);
  2555. }
  2556. }
  2557. if (!$len) {
  2558. return false;
  2559. }
  2560. // build APPEND command
  2561. $key = $this->nextTag();
  2562. $request = "$key APPEND " . $this->escape($mailbox) . ' (' . $this->flagsToStr($flags) . ')';
  2563. if (!empty($date)) {
  2564. $request .= ' ' . $this->escape($date);
  2565. }
  2566. $request .= ' ' . ($binary ? '~' : '') . '{' . $len . ($literal_plus ? '+' : '') . '}';
  2567. // send APPEND command
  2568. if (!$this->putLine($request)) {
  2569. $this->setError(self::ERROR_COMMAND, "Failed to send APPEND command");
  2570. return false;
  2571. }
  2572. // Do not wait when LITERAL+ is supported
  2573. if (!$literal_plus) {
  2574. $line = $this->readReply();
  2575. if ($line[0] != '+') {
  2576. $this->parseResult($line, 'APPEND: ');
  2577. return false;
  2578. }
  2579. }
  2580. foreach ($msg as $msg_part) {
  2581. // file pointer
  2582. if (is_resource($msg_part)) {
  2583. rewind($msg_part);
  2584. while (!feof($msg_part) && $this->fp) {
  2585. $buffer = fread($msg_part, $chunk_size);
  2586. $this->putLine($buffer, false);
  2587. }
  2588. fclose($msg_part);
  2589. }
  2590. // string
  2591. else {
  2592. $size = strlen($msg_part);
  2593. // Break up the data by sending one chunk (up to 512k) at a time.
  2594. // This approach reduces our peak memory usage
  2595. for ($offset = 0; $offset < $size; $offset += $chunk_size) {
  2596. $chunk = substr($msg_part, $offset, $chunk_size);
  2597. if (!$this->putLine($chunk, false)) {
  2598. return false;
  2599. }
  2600. }
  2601. }
  2602. }
  2603. if (!$this->putLine('')) { // \r\n
  2604. return false;
  2605. }
  2606. do {
  2607. $line = $this->readLine();
  2608. } while (!$this->startsWith($line, $key, true, true));
  2609. // Clear internal status cache
  2610. unset($this->data['STATUS:'.$mailbox]);
  2611. if ($this->parseResult($line, 'APPEND: ') != self::ERROR_OK) {
  2612. return false;
  2613. }
  2614. if (!empty($this->data['APPENDUID'])) {
  2615. return $this->data['APPENDUID'];
  2616. }
  2617. return true;
  2618. }
  2619. /**
  2620. * Handler for IMAP APPEND command.
  2621. *
  2622. * @param string $mailbox Mailbox name
  2623. * @param string $path Path to the file with message body
  2624. * @param string $headers Message headers
  2625. * @param array $flags Message flags
  2626. * @param string $date Message internal date
  2627. * @param bool $binary Enable BINARY append (RFC3516)
  2628. *
  2629. * @return string|bool On success APPENDUID response (if available) or True, False on failure
  2630. */
  2631. public function appendFromFile($mailbox, $path, $headers=null, $flags = array(), $date = null, $binary = false)
  2632. {
  2633. // open message file
  2634. if (file_exists(realpath($path))) {
  2635. $fp = fopen($path, 'r');
  2636. }
  2637. if (!$fp) {
  2638. $this->setError(self::ERROR_UNKNOWN, "Couldn't open $path for reading");
  2639. return false;
  2640. }
  2641. $message = array();
  2642. if ($headers) {
  2643. $message[] = trim($headers, "\r\n") . "\r\n\r\n";
  2644. }
  2645. $message[] = $fp;
  2646. return $this->append($mailbox, $message, $flags, $date, $binary);
  2647. }
  2648. /**
  2649. * Returns QUOTA information
  2650. *
  2651. * @param string $mailbox Mailbox name
  2652. *
  2653. * @return array Quota information
  2654. */
  2655. public function getQuota($mailbox = null)
  2656. {
  2657. if ($mailbox === null || $mailbox === '') {
  2658. $mailbox = 'INBOX';
  2659. }
  2660. // a0001 GETQUOTAROOT INBOX
  2661. // * QUOTAROOT INBOX user/sample
  2662. // * QUOTA user/sample (STORAGE 654 9765)
  2663. // a0001 OK Completed
  2664. list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox)));
  2665. $result = false;
  2666. $min_free = PHP_INT_MAX;
  2667. $all = array();
  2668. if ($code == self::ERROR_OK) {
  2669. foreach (explode("\n", $response) as $line) {
  2670. if (preg_match('/^\* QUOTA /', $line)) {
  2671. list(, , $quota_root) = $this->tokenizeResponse($line, 3);
  2672. while ($line) {
  2673. list($type, $used, $total) = $this->tokenizeResponse($line, 1);
  2674. $type = strtolower($type);
  2675. if ($type && $total) {
  2676. $all[$quota_root][$type]['used'] = intval($used);
  2677. $all[$quota_root][$type]['total'] = intval($total);
  2678. }
  2679. }
  2680. if (empty($all[$quota_root]['storage'])) {
  2681. continue;
  2682. }
  2683. $used = $all[$quota_root]['storage']['used'];
  2684. $total = $all[$quota_root]['storage']['total'];
  2685. $free = $total - $used;
  2686. // calculate lowest available space from all storage quotas
  2687. if ($free < $min_free) {
  2688. $min_free = $free;
  2689. $result['used'] = $used;
  2690. $result['total'] = $total;
  2691. $result['percent'] = min(100, round(($used/max(1,$total))*100));
  2692. $result['free'] = 100 - $result['percent'];
  2693. }
  2694. }
  2695. }
  2696. }
  2697. if (!empty($result)) {
  2698. $result['all'] = $all;
  2699. }
  2700. return $result;
  2701. }
  2702. /**
  2703. * Send the SETACL command (RFC4314)
  2704. *
  2705. * @param string $mailbox Mailbox name
  2706. * @param string $user User name
  2707. * @param mixed $acl ACL string or array
  2708. *
  2709. * @return boolean True on success, False on failure
  2710. *
  2711. * @since 0.5-beta
  2712. */
  2713. public function setACL($mailbox, $user, $acl)
  2714. {
  2715. if (is_array($acl)) {
  2716. $acl = implode('', $acl);
  2717. }
  2718. $result = $this->execute('SETACL', array(
  2719. $this->escape($mailbox), $this->escape($user), strtolower($acl)),
  2720. self::COMMAND_NORESPONSE);
  2721. return ($result == self::ERROR_OK);
  2722. }
  2723. /**
  2724. * Send the DELETEACL command (RFC4314)
  2725. *
  2726. * @param string $mailbox Mailbox name
  2727. * @param string $user User name
  2728. *
  2729. * @return boolean True on success, False on failure
  2730. *
  2731. * @since 0.5-beta
  2732. */
  2733. public function deleteACL($mailbox, $user)
  2734. {
  2735. $result = $this->execute('DELETEACL', array(
  2736. $this->escape($mailbox), $this->escape($user)),
  2737. self::COMMAND_NORESPONSE);
  2738. return ($result == self::ERROR_OK);
  2739. }
  2740. /**
  2741. * Send the GETACL command (RFC4314)
  2742. *
  2743. * @param string $mailbox Mailbox name
  2744. *
  2745. * @return array User-rights array on success, NULL on error
  2746. * @since 0.5-beta
  2747. */
  2748. public function getACL($mailbox)
  2749. {
  2750. list($code, $response) = $this->execute('GETACL', array($this->escape($mailbox)));
  2751. if ($code == self::ERROR_OK && preg_match('/^\* ACL /i', $response)) {
  2752. // Parse server response (remove "* ACL ")
  2753. $response = substr($response, 6);
  2754. $ret = $this->tokenizeResponse($response);
  2755. $mbox = array_shift($ret);
  2756. $size = count($ret);
  2757. // Create user-rights hash array
  2758. // @TODO: consider implementing fixACL() method according to RFC4314.2.1.1
  2759. // so we could return only standard rights defined in RFC4314,
  2760. // excluding 'c' and 'd' defined in RFC2086.
  2761. if ($size % 2 == 0) {
  2762. for ($i=0; $i<$size; $i++) {
  2763. $ret[$ret[$i]] = str_split($ret[++$i]);
  2764. unset($ret[$i-1]);
  2765. unset($ret[$i]);
  2766. }
  2767. return $ret;
  2768. }
  2769. $this->setError(self::ERROR_COMMAND, "Incomplete ACL response");
  2770. }
  2771. }
  2772. /**
  2773. * Send the LISTRIGHTS command (RFC4314)
  2774. *
  2775. * @param string $mailbox Mailbox name
  2776. * @param string $user User name
  2777. *
  2778. * @return array List of user rights
  2779. * @since 0.5-beta
  2780. */
  2781. public function listRights($mailbox, $user)
  2782. {
  2783. list($code, $response) = $this->execute('LISTRIGHTS', array(
  2784. $this->escape($mailbox), $this->escape($user)));
  2785. if ($code == self::ERROR_OK && preg_match('/^\* LISTRIGHTS /i', $response)) {
  2786. // Parse server response (remove "* LISTRIGHTS ")
  2787. $response = substr($response, 13);
  2788. $ret_mbox = $this->tokenizeResponse($response, 1);
  2789. $ret_user = $this->tokenizeResponse($response, 1);
  2790. $granted = $this->tokenizeResponse($response, 1);
  2791. $optional = trim($response);
  2792. return array(
  2793. 'granted' => str_split($granted),
  2794. 'optional' => explode(' ', $optional),
  2795. );
  2796. }
  2797. }
  2798. /**
  2799. * Send the MYRIGHTS command (RFC4314)
  2800. *
  2801. * @param string $mailbox Mailbox name
  2802. *
  2803. * @return array MYRIGHTS response on success, NULL on error
  2804. * @since 0.5-beta
  2805. */
  2806. public function myRights($mailbox)
  2807. {
  2808. list($code, $response) = $this->execute('MYRIGHTS', array($this->escape($mailbox)));
  2809. if ($code == self::ERROR_OK && preg_match('/^\* MYRIGHTS /i', $response)) {
  2810. // Parse server response (remove "* MYRIGHTS ")
  2811. $response = substr($response, 11);
  2812. $ret_mbox = $this->tokenizeResponse($response, 1);
  2813. $rights = $this->tokenizeResponse($response, 1);
  2814. return str_split($rights);
  2815. }
  2816. }
  2817. /**
  2818. * Send the SETMETADATA command (RFC5464)
  2819. *
  2820. * @param string $mailbox Mailbox name
  2821. * @param array $entries Entry-value array (use NULL value as NIL)
  2822. *
  2823. * @return boolean True on success, False on failure
  2824. * @since 0.5-beta
  2825. */
  2826. public function setMetadata($mailbox, $entries)
  2827. {
  2828. if (!is_array($entries) || empty($entries)) {
  2829. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
  2830. return false;
  2831. }
  2832. foreach ($entries as $name => $value) {
  2833. $entries[$name] = $this->escape($name) . ' ' . $this->escape($value, true);
  2834. }
  2835. $entries = implode(' ', $entries);
  2836. $result = $this->execute('SETMETADATA', array(
  2837. $this->escape($mailbox), '(' . $entries . ')'),
  2838. self::COMMAND_NORESPONSE);
  2839. return ($result == self::ERROR_OK);
  2840. }
  2841. /**
  2842. * Send the SETMETADATA command with NIL values (RFC5464)
  2843. *
  2844. * @param string $mailbox Mailbox name
  2845. * @param array $entries Entry names array
  2846. *
  2847. * @return boolean True on success, False on failure
  2848. *
  2849. * @since 0.5-beta
  2850. */
  2851. public function deleteMetadata($mailbox, $entries)
  2852. {
  2853. if (!is_array($entries) && !empty($entries)) {
  2854. $entries = explode(' ', $entries);
  2855. }
  2856. if (empty($entries)) {
  2857. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETMETADATA command");
  2858. return false;
  2859. }
  2860. foreach ($entries as $entry) {
  2861. $data[$entry] = null;
  2862. }
  2863. return $this->setMetadata($mailbox, $data);
  2864. }
  2865. /**
  2866. * Send the GETMETADATA command (RFC5464)
  2867. *
  2868. * @param string $mailbox Mailbox name
  2869. * @param array $entries Entries
  2870. * @param array $options Command options (with MAXSIZE and DEPTH keys)
  2871. *
  2872. * @return array GETMETADATA result on success, NULL on error
  2873. *
  2874. * @since 0.5-beta
  2875. */
  2876. public function getMetadata($mailbox, $entries, $options=array())
  2877. {
  2878. if (!is_array($entries)) {
  2879. $entries = array($entries);
  2880. }
  2881. // create entries string
  2882. foreach ($entries as $idx => $name) {
  2883. $entries[$idx] = $this->escape($name);
  2884. }
  2885. $optlist = '';
  2886. $entlist = '(' . implode(' ', $entries) . ')';
  2887. // create options string
  2888. if (is_array($options)) {
  2889. $options = array_change_key_case($options, CASE_UPPER);
  2890. $opts = array();
  2891. if (!empty($options['MAXSIZE'])) {
  2892. $opts[] = 'MAXSIZE '.intval($options['MAXSIZE']);
  2893. }
  2894. if (!empty($options['DEPTH'])) {
  2895. $opts[] = 'DEPTH '.intval($options['DEPTH']);
  2896. }
  2897. if ($opts) {
  2898. $optlist = '(' . implode(' ', $opts) . ')';
  2899. }
  2900. }
  2901. $optlist .= ($optlist ? ' ' : '') . $entlist;
  2902. list($code, $response) = $this->execute('GETMETADATA', array(
  2903. $this->escape($mailbox), $optlist));
  2904. if ($code == self::ERROR_OK) {
  2905. $result = array();
  2906. $data = $this->tokenizeResponse($response);
  2907. // The METADATA response can contain multiple entries in a single
  2908. // response or multiple responses for each entry or group of entries
  2909. if (!empty($data) && ($size = count($data))) {
  2910. for ($i=0; $i<$size; $i++) {
  2911. if (isset($mbox) && is_array($data[$i])) {
  2912. $size_sub = count($data[$i]);
  2913. for ($x=0; $x<$size_sub; $x+=2) {
  2914. if ($data[$i][$x+1] !== null)
  2915. $result[$mbox][$data[$i][$x]] = $data[$i][$x+1];
  2916. }
  2917. unset($data[$i]);
  2918. }
  2919. else if ($data[$i] == '*') {
  2920. if ($data[$i+1] == 'METADATA') {
  2921. $mbox = $data[$i+2];
  2922. unset($data[$i]); // "*"
  2923. unset($data[++$i]); // "METADATA"
  2924. unset($data[++$i]); // Mailbox
  2925. }
  2926. // get rid of other untagged responses
  2927. else {
  2928. unset($mbox);
  2929. unset($data[$i]);
  2930. }
  2931. }
  2932. else if (isset($mbox)) {
  2933. if ($data[++$i] !== null)
  2934. $result[$mbox][$data[$i-1]] = $data[$i];
  2935. unset($data[$i]);
  2936. unset($data[$i-1]);
  2937. }
  2938. else {
  2939. unset($data[$i]);
  2940. }
  2941. }
  2942. }
  2943. return $result;
  2944. }
  2945. }
  2946. /**
  2947. * Send the SETANNOTATION command (draft-daboo-imap-annotatemore)
  2948. *
  2949. * @param string $mailbox Mailbox name
  2950. * @param array $data Data array where each item is an array with
  2951. * three elements: entry name, attribute name, value
  2952. *
  2953. * @return boolean True on success, False on failure
  2954. * @since 0.5-beta
  2955. */
  2956. public function setAnnotation($mailbox, $data)
  2957. {
  2958. if (!is_array($data) || empty($data)) {
  2959. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
  2960. return false;
  2961. }
  2962. foreach ($data as $entry) {
  2963. // ANNOTATEMORE drafts before version 08 require quoted parameters
  2964. $entries[] = sprintf('%s (%s %s)', $this->escape($entry[0], true),
  2965. $this->escape($entry[1], true), $this->escape($entry[2], true));
  2966. }
  2967. $entries = implode(' ', $entries);
  2968. $result = $this->execute('SETANNOTATION', array(
  2969. $this->escape($mailbox), $entries), self::COMMAND_NORESPONSE);
  2970. return ($result == self::ERROR_OK);
  2971. }
  2972. /**
  2973. * Send the SETANNOTATION command with NIL values (draft-daboo-imap-annotatemore)
  2974. *
  2975. * @param string $mailbox Mailbox name
  2976. * @param array $data Data array where each item is an array with
  2977. * two elements: entry name and attribute name
  2978. *
  2979. * @return boolean True on success, False on failure
  2980. *
  2981. * @since 0.5-beta
  2982. */
  2983. public function deleteAnnotation($mailbox, $data)
  2984. {
  2985. if (!is_array($data) || empty($data)) {
  2986. $this->setError(self::ERROR_COMMAND, "Wrong argument for SETANNOTATION command");
  2987. return false;
  2988. }
  2989. return $this->setAnnotation($mailbox, $data);
  2990. }
  2991. /**
  2992. * Send the GETANNOTATION command (draft-daboo-imap-annotatemore)
  2993. *
  2994. * @param string $mailbox Mailbox name
  2995. * @param array $entries Entries names
  2996. * @param array $attribs Attribs names
  2997. *
  2998. * @return array Annotations result on success, NULL on error
  2999. *
  3000. * @since 0.5-beta
  3001. */
  3002. public function getAnnotation($mailbox, $entries, $attribs)
  3003. {
  3004. if (!is_array($entries)) {
  3005. $entries = array($entries);
  3006. }
  3007. // create entries string
  3008. // ANNOTATEMORE drafts before version 08 require quoted parameters
  3009. foreach ($entries as $idx => $name) {
  3010. $entries[$idx] = $this->escape($name, true);
  3011. }
  3012. $entries = '(' . implode(' ', $entries) . ')';
  3013. if (!is_array($attribs)) {
  3014. $attribs = array($attribs);
  3015. }
  3016. // create attributes string
  3017. foreach ($attribs as $idx => $name) {
  3018. $attribs[$idx] = $this->escape($name, true);
  3019. }
  3020. $attribs = '(' . implode(' ', $attribs) . ')';
  3021. list($code, $response) = $this->execute('GETANNOTATION', array(
  3022. $this->escape($mailbox), $entries, $attribs));
  3023. if ($code == self::ERROR_OK) {
  3024. $result = array();
  3025. $data = $this->tokenizeResponse($response);
  3026. // Here we returns only data compatible with METADATA result format
  3027. if (!empty($data) && ($size = count($data))) {
  3028. for ($i=0; $i<$size; $i++) {
  3029. $entry = $data[$i];
  3030. if (isset($mbox) && is_array($entry)) {
  3031. $attribs = $entry;
  3032. $entry = $last_entry;
  3033. }
  3034. else if ($entry == '*') {
  3035. if ($data[$i+1] == 'ANNOTATION') {
  3036. $mbox = $data[$i+2];
  3037. unset($data[$i]); // "*"
  3038. unset($data[++$i]); // "ANNOTATION"
  3039. unset($data[++$i]); // Mailbox
  3040. }
  3041. // get rid of other untagged responses
  3042. else {
  3043. unset($mbox);
  3044. unset($data[$i]);
  3045. }
  3046. continue;
  3047. }
  3048. else if (isset($mbox)) {
  3049. $attribs = $data[++$i];
  3050. }
  3051. else {
  3052. unset($data[$i]);
  3053. continue;
  3054. }
  3055. if (!empty($attribs)) {
  3056. for ($x=0, $len=count($attribs); $x<$len;) {
  3057. $attr = $attribs[$x++];
  3058. $value = $attribs[$x++];
  3059. if ($attr == 'value.priv' && $value !== null) {
  3060. $result[$mbox]['/private' . $entry] = $value;
  3061. }
  3062. else if ($attr == 'value.shared' && $value !== null) {
  3063. $result[$mbox]['/shared' . $entry] = $value;
  3064. }
  3065. }
  3066. }
  3067. $last_entry = $entry;
  3068. unset($data[$i]);
  3069. }
  3070. }
  3071. return $result;
  3072. }
  3073. }
  3074. /**
  3075. * Returns BODYSTRUCTURE for the specified message.
  3076. *
  3077. * @param string $mailbox Folder name
  3078. * @param int $id Message sequence number or UID
  3079. * @param bool $is_uid True if $id is an UID
  3080. *
  3081. * @return array/bool Body structure array or False on error.
  3082. * @since 0.6
  3083. */
  3084. public function getStructure($mailbox, $id, $is_uid = false)
  3085. {
  3086. $result = $this->fetch($mailbox, $id, $is_uid, array('BODYSTRUCTURE'));
  3087. if (is_array($result)) {
  3088. $result = array_shift($result);
  3089. return $result->bodystructure;
  3090. }
  3091. return false;
  3092. }
  3093. /**
  3094. * Returns data of a message part according to specified structure.
  3095. *
  3096. * @param array $structure Message structure (getStructure() result)
  3097. * @param string $part Message part identifier
  3098. *
  3099. * @return array Part data as hash array (type, encoding, charset, size)
  3100. */
  3101. public static function getStructurePartData($structure, $part)
  3102. {
  3103. $part_a = self::getStructurePartArray($structure, $part);
  3104. $data = array();
  3105. if (empty($part_a)) {
  3106. return $data;
  3107. }
  3108. // content-type
  3109. if (is_array($part_a[0])) {
  3110. $data['type'] = 'multipart';
  3111. }
  3112. else {
  3113. $data['type'] = strtolower($part_a[0]);
  3114. // encoding
  3115. $data['encoding'] = strtolower($part_a[5]);
  3116. // charset
  3117. if (is_array($part_a[2])) {
  3118. while (list($key, $val) = each($part_a[2])) {
  3119. if (strcasecmp($val, 'charset') == 0) {
  3120. $data['charset'] = $part_a[2][$key+1];
  3121. break;
  3122. }
  3123. }
  3124. }
  3125. }
  3126. // size
  3127. $data['size'] = intval($part_a[6]);
  3128. return $data;
  3129. }
  3130. public static function getStructurePartArray($a, $part)
  3131. {
  3132. if (!is_array($a)) {
  3133. return false;
  3134. }
  3135. if (empty($part)) {
  3136. return $a;
  3137. }
  3138. $ctype = is_string($a[0]) && is_string($a[1]) ? $a[0] . '/' . $a[1] : '';
  3139. if (strcasecmp($ctype, 'message/rfc822') == 0) {
  3140. $a = $a[8];
  3141. }
  3142. if (strpos($part, '.') > 0) {
  3143. $orig_part = $part;
  3144. $pos = strpos($part, '.');
  3145. $rest = substr($orig_part, $pos+1);
  3146. $part = substr($orig_part, 0, $pos);
  3147. return self::getStructurePartArray($a[$part-1], $rest);
  3148. }
  3149. else if ($part > 0) {
  3150. return (is_array($a[$part-1])) ? $a[$part-1] : $a;
  3151. }
  3152. }
  3153. /**
  3154. * Creates next command identifier (tag)
  3155. *
  3156. * @return string Command identifier
  3157. * @since 0.5-beta
  3158. */
  3159. public function nextTag()
  3160. {
  3161. $this->cmd_num++;
  3162. $this->cmd_tag = sprintf('A%04d', $this->cmd_num);
  3163. return $this->cmd_tag;
  3164. }
  3165. /**
  3166. * Sends IMAP command and parses result
  3167. *
  3168. * @param string $command IMAP command
  3169. * @param array $arguments Command arguments
  3170. * @param int $options Execution options
  3171. *
  3172. * @return mixed Response code or list of response code and data
  3173. * @since 0.5-beta
  3174. */
  3175. public function execute($command, $arguments=array(), $options=0)
  3176. {
  3177. $tag = $this->nextTag();
  3178. $query = $tag . ' ' . $command;
  3179. $noresp = ($options & self::COMMAND_NORESPONSE);
  3180. $response = $noresp ? null : '';
  3181. if (!empty($arguments)) {
  3182. foreach ($arguments as $arg) {
  3183. $query .= ' ' . self::r_implode($arg);
  3184. }
  3185. }
  3186. // Send command
  3187. if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
  3188. preg_match('/^[A-Z0-9]+ ((UID )?[A-Z]+)/', $query, $matches);
  3189. $cmd = $matches[1] ?: 'UNKNOWN';
  3190. $this->setError(self::ERROR_COMMAND, "Failed to send $cmd command");
  3191. return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
  3192. }
  3193. // Parse response
  3194. do {
  3195. $line = $this->readLine(4096);
  3196. if ($response !== null) {
  3197. $response .= $line;
  3198. }
  3199. }
  3200. while (!$this->startsWith($line, $tag . ' ', true, true));
  3201. $code = $this->parseResult($line, $command . ': ');
  3202. // Remove last line from response
  3203. if ($response) {
  3204. $line_len = min(strlen($response), strlen($line) + 2);
  3205. $response = substr($response, 0, -$line_len);
  3206. }
  3207. // optional CAPABILITY response
  3208. if (($options & self::COMMAND_CAPABILITY) && $code == self::ERROR_OK
  3209. && preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)
  3210. ) {
  3211. $this->parseCapability($matches[1], true);
  3212. }
  3213. // return last line only (without command tag, result and response code)
  3214. if ($line && ($options & self::COMMAND_LASTLINE)) {
  3215. $response = preg_replace("/^$tag (OK|NO|BAD|BYE|PREAUTH)?\s*(\[[a-z-]+\])?\s*/i", '', trim($line));
  3216. }
  3217. return $noresp ? $code : array($code, $response);
  3218. }
  3219. /**
  3220. * Splits IMAP response into string tokens
  3221. *
  3222. * @param string &$str The IMAP's server response
  3223. * @param int $num Number of tokens to return
  3224. *
  3225. * @return mixed Tokens array or string if $num=1
  3226. * @since 0.5-beta
  3227. */
  3228. public static function tokenizeResponse(&$str, $num=0)
  3229. {
  3230. $result = array();
  3231. while (!$num || count($result) < $num) {
  3232. // remove spaces from the beginning of the string
  3233. $str = ltrim($str);
  3234. switch ($str[0]) {
  3235. // String literal
  3236. case '{':
  3237. if (($epos = strpos($str, "}\r\n", 1)) == false) {
  3238. // error
  3239. }
  3240. if (!is_numeric(($bytes = substr($str, 1, $epos - 1)))) {
  3241. // error
  3242. }
  3243. $result[] = $bytes ? substr($str, $epos + 3, $bytes) : '';
  3244. $str = substr($str, $epos + 3 + $bytes);
  3245. break;
  3246. // Quoted string
  3247. case '"':
  3248. $len = strlen($str);
  3249. for ($pos=1; $pos<$len; $pos++) {
  3250. if ($str[$pos] == '"') {
  3251. break;
  3252. }
  3253. if ($str[$pos] == "\\") {
  3254. if ($str[$pos + 1] == '"' || $str[$pos + 1] == "\\") {
  3255. $pos++;
  3256. }
  3257. }
  3258. }
  3259. // we need to strip slashes for a quoted string
  3260. $result[] = stripslashes(substr($str, 1, $pos - 1));
  3261. $str = substr($str, $pos + 1);
  3262. break;
  3263. // Parenthesized list
  3264. case '(':
  3265. $str = substr($str, 1);
  3266. $result[] = self::tokenizeResponse($str);
  3267. break;
  3268. case ')':
  3269. $str = substr($str, 1);
  3270. return $result;
  3271. // String atom, number, astring, NIL, *, %
  3272. default:
  3273. // empty string
  3274. if ($str === '' || $str === null) {
  3275. break 2;
  3276. }
  3277. // excluded chars: SP, CTL, ), DEL
  3278. // we do not exclude [ and ] (#1489223)
  3279. if (preg_match('/^([^\x00-\x20\x29\x7F]+)/', $str, $m)) {
  3280. $result[] = $m[1] == 'NIL' ? null : $m[1];
  3281. $str = substr($str, strlen($m[1]));
  3282. }
  3283. break;
  3284. }
  3285. }
  3286. return $num == 1 ? $result[0] : $result;
  3287. }
  3288. /**
  3289. * Joins IMAP command line elements (recursively)
  3290. */
  3291. protected static function r_implode($element)
  3292. {
  3293. $string = '';
  3294. if (is_array($element)) {
  3295. reset($element);
  3296. foreach ($element as $value) {
  3297. $string .= ' ' . self::r_implode($value);
  3298. }
  3299. }
  3300. else {
  3301. return $element;
  3302. }
  3303. return '(' . trim($string) . ')';
  3304. }
  3305. /**
  3306. * Converts message identifiers array into sequence-set syntax
  3307. *
  3308. * @param array $messages Message identifiers
  3309. * @param bool $force Forces compression of any size
  3310. *
  3311. * @return string Compressed sequence-set
  3312. */
  3313. public static function compressMessageSet($messages, $force=false)
  3314. {
  3315. // given a comma delimited list of independent mid's,
  3316. // compresses by grouping sequences together
  3317. if (!is_array($messages)) {
  3318. // if less than 255 bytes long, let's not bother
  3319. if (!$force && strlen($messages)<255) {
  3320. return $messages;
  3321. }
  3322. // see if it's already been compressed
  3323. if (strpos($messages, ':') !== false) {
  3324. return $messages;
  3325. }
  3326. // separate, then sort
  3327. $messages = explode(',', $messages);
  3328. }
  3329. sort($messages);
  3330. $result = array();
  3331. $start = $prev = $messages[0];
  3332. foreach ($messages as $id) {
  3333. $incr = $id - $prev;
  3334. if ($incr > 1) { // found a gap
  3335. if ($start == $prev) {
  3336. $result[] = $prev; // push single id
  3337. }
  3338. else {
  3339. $result[] = $start . ':' . $prev; // push sequence as start_id:end_id
  3340. }
  3341. $start = $id; // start of new sequence
  3342. }
  3343. $prev = $id;
  3344. }
  3345. // handle the last sequence/id
  3346. if ($start == $prev) {
  3347. $result[] = $prev;
  3348. }
  3349. else {
  3350. $result[] = $start.':'.$prev;
  3351. }
  3352. // return as comma separated string
  3353. return implode(',', $result);
  3354. }
  3355. /**
  3356. * Converts message sequence-set into array
  3357. *
  3358. * @param string $messages Message identifiers
  3359. *
  3360. * @return array List of message identifiers
  3361. */
  3362. public static function uncompressMessageSet($messages)
  3363. {
  3364. if (empty($messages)) {
  3365. return array();
  3366. }
  3367. $result = array();
  3368. $messages = explode(',', $messages);
  3369. foreach ($messages as $idx => $part) {
  3370. $items = explode(':', $part);
  3371. $max = max($items[0], $items[1]);
  3372. for ($x=$items[0]; $x<=$max; $x++) {
  3373. $result[] = (int)$x;
  3374. }
  3375. unset($messages[$idx]);
  3376. }
  3377. return $result;
  3378. }
  3379. /**
  3380. * Clear internal status cache
  3381. */
  3382. protected function clear_status_cache($mailbox)
  3383. {
  3384. unset($this->data['STATUS:' . $mailbox]);
  3385. $keys = array('EXISTS', 'RECENT', 'UNSEEN', 'UID-MAP');
  3386. foreach ($keys as $key) {
  3387. unset($this->data[$key]);
  3388. }
  3389. }
  3390. /**
  3391. * Clear internal cache of the current mailbox
  3392. */
  3393. protected function clear_mailbox_cache()
  3394. {
  3395. $this->clear_status_cache($this->selected);
  3396. $keys = array('UIDNEXT', 'UIDVALIDITY', 'HIGHESTMODSEQ', 'NOMODSEQ',
  3397. 'PERMANENTFLAGS', 'QRESYNC', 'VANISHED', 'READ-WRITE');
  3398. foreach ($keys as $key) {
  3399. unset($this->data[$key]);
  3400. }
  3401. }
  3402. /**
  3403. * Converts flags array into string for inclusion in IMAP command
  3404. *
  3405. * @param array $flags Flags (see self::flags)
  3406. *
  3407. * @return string Space-separated list of flags
  3408. */
  3409. protected function flagsToStr($flags)
  3410. {
  3411. foreach ((array)$flags as $idx => $flag) {
  3412. if ($flag = $this->flags[strtoupper($flag)]) {
  3413. $flags[$idx] = $flag;
  3414. }
  3415. }
  3416. return implode(' ', (array)$flags);
  3417. }
  3418. /**
  3419. * CAPABILITY response parser
  3420. */
  3421. protected function parseCapability($str, $trusted=false)
  3422. {
  3423. $str = preg_replace('/^\* CAPABILITY /i', '', $str);
  3424. $this->capability = explode(' ', strtoupper($str));
  3425. if (!empty($this->prefs['disabled_caps'])) {
  3426. $this->capability = array_diff($this->capability, $this->prefs['disabled_caps']);
  3427. }
  3428. if (!isset($this->prefs['literal+']) && in_array('LITERAL+', $this->capability)) {
  3429. $this->prefs['literal+'] = true;
  3430. }
  3431. if ($trusted) {
  3432. $this->capability_readed = true;
  3433. }
  3434. }
  3435. /**
  3436. * Escapes a string when it contains special characters (RFC3501)
  3437. *
  3438. * @param string $string IMAP string
  3439. * @param boolean $force_quotes Forces string quoting (for atoms)
  3440. *
  3441. * @return string String atom, quoted-string or string literal
  3442. * @todo lists
  3443. */
  3444. public static function escape($string, $force_quotes=false)
  3445. {
  3446. if ($string === null) {
  3447. return 'NIL';
  3448. }
  3449. if ($string === '') {
  3450. return '""';
  3451. }
  3452. // atom-string (only safe characters)
  3453. if (!$force_quotes && !preg_match('/[\x00-\x20\x22\x25\x28-\x2A\x5B-\x5D\x7B\x7D\x80-\xFF]/', $string)) {
  3454. return $string;
  3455. }
  3456. // quoted-string
  3457. if (!preg_match('/[\r\n\x00\x80-\xFF]/', $string)) {
  3458. return '"' . addcslashes($string, '\\"') . '"';
  3459. }
  3460. // literal-string
  3461. return sprintf("{%d}\r\n%s", strlen($string), $string);
  3462. }
  3463. /**
  3464. * Set the value of the debugging flag.
  3465. *
  3466. * @param boolean $debug New value for the debugging flag.
  3467. * @param callback $handler Logging handler function
  3468. *
  3469. * @since 0.5-stable
  3470. */
  3471. public function setDebug($debug, $handler = null)
  3472. {
  3473. $this->debug = $debug;
  3474. $this->debug_handler = $handler;
  3475. }
  3476. /**
  3477. * Write the given debug text to the current debug output handler.
  3478. *
  3479. * @param string $message Debug mesage text.
  3480. *
  3481. * @since 0.5-stable
  3482. */
  3483. protected function debug($message)
  3484. {
  3485. if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
  3486. $diff = $len - self::DEBUG_LINE_LENGTH;
  3487. $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
  3488. . "... [truncated $diff bytes]";
  3489. }
  3490. if ($this->resourceid) {
  3491. $message = sprintf('[%s] %s', $this->resourceid, $message);
  3492. }
  3493. if ($this->debug_handler) {
  3494. call_user_func_array($this->debug_handler, array(&$this, $message));
  3495. }
  3496. else {
  3497. echo "DEBUG: $message\n";
  3498. }
  3499. }
  3500. }