1: <?php
2: /**
3: * An abstracted API interface to IMAP backends supporting the IMAP4rev1
4: * protocol (RFC 3501).
5: *
6: * Copyright 2008-2012 Horde LLC (http://www.horde.org/)
7: *
8: * See the enclosed file COPYING for license information (LGPL). If you
9: * did not receive this file, see http://www.horde.org/licenses/lgpl21.
10: *
11: * @author Michael Slusarz <slusarz@horde.org>
12: * @category Horde
13: * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
14: * @package Imap_Client
15: *
16: * @property Horde_Imap_Client_Utils $utils A Utils object.
17: */
18: abstract class Horde_Imap_Client_Base implements Serializable
19: {
20: /* Serialized version. */
21: const VERSION = 1;
22:
23: /* Cache names for miscellaneous data. */
24: const CACHE_MODSEQ = 'HICmodseq';
25: const CACHE_SEARCH = 'HICsearch';
26:
27: /**
28: * The Horde_Imap_Client_Cache object.
29: *
30: * @var Horde_Imap_Client_Cache
31: */
32: public $cache = null;
33:
34: /**
35: * The list of fetch fields that can be cached, and their cache names.
36: *
37: * @var array
38: */
39: public $cacheFields = array(
40: Horde_Imap_Client::FETCH_ENVELOPE => 'HICenv',
41: Horde_Imap_Client::FETCH_FLAGS => 'HICflags',
42: Horde_Imap_Client::FETCH_HEADERS => 'HIChdrs',
43: Horde_Imap_Client::FETCH_IMAPDATE => 'HICdate',
44: Horde_Imap_Client::FETCH_SIZE => 'HICsize',
45: Horde_Imap_Client::FETCH_STRUCTURE => 'HICstruct'
46: );
47:
48: /**
49: * Has the internal configuration changed?
50: *
51: * @var boolean
52: */
53: public $changed = false;
54:
55: /**
56: * The debug stream.
57: *
58: * @var resource
59: */
60: protected $_debug = null;
61:
62: /**
63: * The fetch data object type to return.
64: *
65: * @var string
66: */
67: protected $_fetchDataClass = 'Horde_Imap_Client_Data_Fetch';
68:
69: /**
70: * Cached server data.
71: *
72: * @var array
73: */
74: protected $_init;
75:
76: /**
77: * Is there an active authenticated connection to the IMAP Server?
78: *
79: * @var boolean
80: */
81: protected $_isAuthenticated = false;
82:
83: /**
84: * Is there a secure connection to the IMAP Server?
85: *
86: * @var boolean
87: */
88: protected $_isSecure = false;
89:
90: /**
91: * The current mailbox selection mode.
92: *
93: * @var integer
94: */
95: protected $_mode = 0;
96:
97: /**
98: * Hash containing connection parameters.
99: * This hash never changes.
100: *
101: * @var array
102: */
103: protected $_params = array();
104:
105: /**
106: * The currently selected mailbox.
107: *
108: * @var Horde_Imap_Client_Mailbox
109: */
110: protected $_selected = null;
111:
112: /**
113: * Temp array (destroyed at end of process).
114: *
115: * @var array
116: */
117: protected $_temp = array();
118:
119: /**
120: * The Horde_Imap_Client_Utils object.
121: *
122: * @var Horde_Imap_Client_Utils
123: */
124: protected $_utils;
125:
126: /**
127: * The utils class to use.
128: *
129: * @var string
130: */
131: protected $_utilsClass = 'Horde_Imap_Client_Utils';
132:
133: /**
134: * Constructor.
135: *
136: * @see Horde_Imap_Client::factory()
137: *
138: * @param array $params A hash containing configuration parameters.
139: * See Horde_Imap_Client::factory().
140: */
141: public function __construct(array $params = array())
142: {
143: if (!isset($params['username']) || !isset($params['password'])) {
144: throw new InvalidArgumentException('Horde_Imap_Client requires a username and password.');
145: }
146:
147: $this->_setInit();
148:
149: // Default values.
150: $params = array_merge(array(
151: 'encryptKey' => null,
152: 'hostspec' => 'localhost',
153: 'log' => null,
154: 'port' => ((isset($params['secure']) && ($params['secure'] == 'ssl')) ? 993 : 143),
155: 'secure' => false,
156: 'timeout' => 30
157: ), array_filter($params));
158:
159: if (empty($params['cache'])) {
160: $params['cache'] = array('fields' => array());
161: } elseif (empty($params['cache']['fields'])) {
162: $params['cache']['fields'] = $this->cacheFields;
163: } else {
164: $params['cache']['fields'] = array_flip($params['cache']['fields']);
165: }
166:
167: if (empty($params['cache']['fetch_ignore'])) {
168: $params['cache']['fetch_ignore'] = array();
169: }
170:
171: $this->_params = $params;
172: $this->setParam('password', $this->_params['password']);
173:
174: $this->changed = true;
175: $this->_initOb();
176: }
177:
178: /**
179: * Get encryption key.
180: *
181: * @return string The encryption key.
182: */
183: protected function _getEncryptKey()
184: {
185: if (is_callable($this->_params['encryptKey'])) {
186: return call_user_func($this->_params['encryptKey']);
187: }
188:
189: throw new InvalidArgumentException('encryptKey parameter is not a valid callback.');
190: }
191:
192: /**
193: * Exception wrapper - logs an error message before (optionally) throwing
194: * exception.
195: *
196: * Server debug information, if present, will be stored in the 'details'
197: * property of the exception object.
198: *
199: * @param mixed $msg Error message/error object. If an array,
200: * the first entry is used as the exception
201: * message and the second entry is taken
202: * to be server debug information.
203: * @param integer|string $code Error code. If string, will convert from
204: * the Exception constant of the same name.
205: * If 'NO_SUPPORT', throws a non-supported
206: * extension exception.
207: * @param boolean $logonly If true, log only and don't throw
208: * exception.
209: *
210: * @throws Horde_Imap_Client_Exception
211: * @throws Horde_Imap_Client_Exception_NoSupportExtension
212: */
213: protected function _exception($msg, $code = 0, $logonly = false)
214: {
215: if (is_array($msg)) {
216: $details = $msg[1];
217: $msg = $msg[0];
218: } else {
219: $details = null;
220: }
221:
222: if (is_integer($code)) {
223: $e = new Horde_Imap_Client_Exception($msg, $code);
224: } elseif ($code == 'NO_SUPPORT') {
225: $e = new Horde_Imap_Client_Exception_NoSupportExtension($msg);
226: } else {
227: $e = new Horde_Imap_Client_Exception($msg, constant('Horde_Imap_Client_Exception::' . $code));
228: }
229:
230: if (!is_null($details)) {
231: $e->details = $details;
232: }
233:
234: if (is_callable($this->_params['log'])) {
235: call_user_func($this->_params['log'], $e);
236: }
237:
238: if (!$logonly) {
239: throw $e;
240: }
241: }
242:
243: /**
244: * Do initialization tasks.
245: */
246: protected function _initOb()
247: {
248: if (!empty($this->_params['debug'])) {
249: if (is_resource($this->_params['debug'])) {
250: $this->_debug = $this->_params['debug'];
251: } else {
252: $this->_debug = @fopen($this->_params['debug'], 'a');
253: }
254: }
255: }
256:
257: /**
258: * Destructor.
259: */
260: public function __destruct()
261: {
262: $this->logout();
263:
264: /* Close debugging output. */
265: if (is_resource($this->_debug)) {
266: fflush($this->_debug);
267: fclose($this->_debug);
268: $this->_debug = null;
269: }
270: }
271:
272: /**
273: */
274: public function serialize()
275: {
276: return serialize(array(
277: 'i' => $this->_init,
278: 'p' => $this->_params,
279: 'v' => self::VERSION
280: ));
281: }
282:
283: /**
284: */
285: public function unserialize($data)
286: {
287: $data = @unserialize($data);
288: if (!is_array($data) ||
289: !isset($data['v']) ||
290: ($data['v'] != self::VERSION)) {
291: throw new Exception('Cache version change');
292: }
293:
294: $this->_init = $data['i'];
295: $this->_params = $data['p'];
296:
297: $this->_initOb();
298: }
299:
300: /**
301: */
302: public function __get($name)
303: {
304: switch ($name) {
305: case 'utils':
306: if (!isset($this->_utils)) {
307: $this->_utils = new $this->_utilsClass();
308: }
309: return $this->_utils;
310: }
311: }
312:
313: /**
314: * Set an initialization value.
315: *
316: * @param string $key The initialization key. If null, resets all keys.
317: * @param mixed $val The cached value. If null, removes the key.
318: */
319: public function _setInit($key = null, $val = null)
320: {
321: if (is_null($key)) {
322: $this->_init = array(
323: 'enabled' => array(),
324: 'namespace' => array(),
325: 's_charset' => array()
326: );
327: } elseif (is_null($val)) {
328: unset($this->_init[$key]);
329: } else {
330: switch ($key) {
331: case 'capability':
332: if (!empty($this->_params['capability_ignore'])) {
333: if ($this->_debug &&
334: ($ignored = array_intersect_key($val, array_flip($this->_params['capability_ignore'])))) {
335: $this->writeDebug(sprintf("IGNORING these IMAP capabilities: %s\n", implode(', ', array_keys($ignored))), Horde_Imap_Client::DEBUG_INFO);
336: }
337: $val = array_diff_key($val, array_flip($this->_params['capability_ignore']));
338: }
339: break;
340: }
341:
342: $this->_init[$key] = $val;
343: }
344: $this->changed = true;
345: }
346:
347: /**
348: * Initialize the Horde_Imap_Client_Cache object, if necessary.
349: *
350: * @param boolean $current If true, we are going to update the currently
351: * selected mailbox. Add an additional check to
352: * see if caching is available in current
353: * mailbox.
354: *
355: * @return boolean Returns true if caching is enabled.
356: */
357: protected function _initCache($current = false)
358: {
359: if (empty($this->_params['cache']['fields']) ||
360: !empty($this->_temp['nocache'])) {
361: return false;
362: }
363:
364: if (is_null($this->cache)) {
365: try {
366: $this->cache = new Horde_Imap_Client_Cache(array_merge($this->getParam('cache'), array(
367: 'baseob' => $this,
368: 'debug' => (bool)$this->_debug
369: )));
370: } catch (InvalidArgumentException $e) {
371: return false;
372: }
373: }
374:
375: if (!$current) {
376: return true;
377: }
378:
379: /* If UIDs are labeled as not sticky, don't cache since UIDs will
380: * change on every access. */
381: $status = $this->status($this->_selected, Horde_Imap_Client::STATUS_UIDNOTSTICKY);
382: return !$status['uidnotsticky'];
383: }
384:
385: /**
386: * Update the list of ignored mailboxes for caching FETCH data.
387: *
388: * @param array $mboxes The list of mailboxes to ignore.
389: */
390: public function fetchCacheIgnore(array $mboxes)
391: {
392: $this->_params['cache']['fetch_ignore'] = $mboxes;
393: $this->changed = true;
394: }
395:
396: /**
397: * Returns a value from the internal params array.
398: *
399: * @param string $key The param key.
400: *
401: * @return mixed The param value, or null if not found.
402: */
403: public function getParam($key)
404: {
405: /* Passwords may be stored encrypted. */
406: if (($key == 'password') && !empty($this->_params['_passencrypt'])) {
407: try {
408: $secret = new Horde_Secret();
409: return $secret->read($this->_getEncryptKey(), $this->_params['password']);
410: } catch (Exception $e) {
411: return null;
412: }
413: }
414:
415: return isset($this->_params[$key])
416: ? $this->_params[$key]
417: : null;
418: }
419:
420: /**
421: * Sets a configuration parameter value.
422: *
423: * @since 1.5.0
424: *
425: * @param string $key The param key.
426: * @param mixed $value The param value.
427: */
428: public function setParam($key, $val)
429: {
430: switch ($key) {
431: case 'password':
432: // Encrypt password.
433: try {
434: $encrypt_key = $this->_getEncryptKey();
435: if (strlen($encrypt_key)) {
436: $secret = new Horde_Secret();
437: $val = $secret->write($encrypt_key, $val);
438: $this->_params['_passencrypt'] = true;
439: }
440: } catch (Exception $e) {
441: $this->_params['_passencrypt'] = false;
442: }
443: break;
444: }
445:
446: $this->_params[$key] = $val;
447: $this->changed = true;
448: }
449:
450: /**
451: * Returns the Horde_Imap_Client_Cache object used, if available.
452: *
453: * @return mixed Either the object or null.
454: */
455: public function getCache()
456: {
457: $this->_initCache();
458: return $this->cache;
459: }
460:
461: /**
462: * Returns the correct IDs object for use with this driver.
463: *
464: * @param mixed $ids See self::add().
465: * @param boolean $sequence Are $ids message sequence numbers?
466: *
467: * @return Horde_Imap_Client_Ids The IDs object.
468: */
469: public function getIdsOb($ids = null, $sequence = false)
470: {
471: return new Horde_Imap_Client_Ids($ids, $sequence);
472: }
473:
474: /**
475: * Returns whether the IMAP server supports the given capability
476: * (See RFC 3501 [6.1.1]).
477: *
478: * @param string $capability The capability string to query.
479: *
480: * @param mixed True if the server supports the queried capability,
481: * false if it doesn't, or an array if the capability can
482: * contain multiple values.
483: */
484: public function queryCapability($capability)
485: {
486: if (!isset($this->_init['capability'])) {
487: try {
488: $this->capability();
489: } catch (Horde_Imap_Client_Exception $e) {
490: return false;
491: }
492: }
493:
494: $capability = strtoupper($capability);
495:
496: if (!isset($this->_init['capability'][$capability])) {
497: return false;
498: }
499:
500: /* Check for capability requirements. */
501: if (isset(Horde_Imap_Client::$capability_deps[$capability])) {
502: foreach (Horde_Imap_Client::$capability_deps[$capability] as $val) {
503: if (!$this->queryCapability($val)) {
504: return false;
505: }
506: }
507: }
508:
509: return $this->_init['capability'][$capability];
510: }
511:
512: /**
513: * Get CAPABILITY information from the IMAP server.
514: *
515: * @return array The capability array.
516: *
517: * @throws Horde_Imap_Client_Exception
518: */
519: public function capability()
520: {
521: if (!isset($this->_init['capability'])) {
522: $this->_setInit('capability', $this->_capability());
523: }
524:
525: return $this->_init['capability'];
526: }
527:
528: /**
529: * Get CAPABILITY information from the IMAP server.
530: *
531: * @return array The capability array.
532: *
533: * @throws Horde_Imap_Client_Exception
534: */
535: abstract protected function _capability();
536:
537: /**
538: * Send a NOOP command (RFC 3501 [6.1.2]).
539: *
540: * @throws Horde_Imap_Client_Exception
541: */
542: public function noop()
543: {
544: // NOOP only useful if we are already authenticated.
545: if ($this->_isAuthenticated) {
546: $this->_noop();
547: }
548: }
549:
550: /**
551: * Send a NOOP command.
552: *
553: * @throws Horde_Imap_Client_Exception
554: */
555: abstract protected function _noop();
556:
557: /**
558: * Get the NAMESPACE information from the IMAP server (RFC 2342).
559: *
560: * @param array $additional If the server supports namespaces, any
561: * additional namespaces to add to the
562: * namespace list that are not broadcast by
563: * the server. The namespaces must be UTF-8
564: * strings.
565: *
566: * @return array An array of namespace information with the name as the
567: * key (UTF7-IMAP) and the following values:
568: * <ul>
569: * <li>delimiter: (string) The namespace delimiter.</li>
570: * <li>hidden: (boolean) Is this a hidden namespace?</li>
571: * <li>name: (string) The namespace name (UTF7-IMAP).</li>
572: * <li>
573: * translation: (string) Returns the translated name of the namespace
574: * (UTF-8). Requires RFC 5255 and a previous call to setLanguage().
575: * </li>
576: * <li>
577: * type: (integer) The namespace type. Either:
578: * <ul>
579: * <li>Horde_Imap_Client::NS_PERSONAL</li>
580: * <li>Horde_Imap_Client::NS_OTHER</li>
581: * <li>Horde_Imap_Client::NS_SHARED</li>
582: * </ul>
583: * </li>
584: * </ul>
585: *
586: * @throws Horde_Imap_Client_Exception
587: */
588: public function getNamespaces(array $additional = array())
589: {
590: $this->login();
591:
592: $sig = hash('md5', serialize($additional));
593:
594: if (isset($this->_init['namespace'][$sig])) {
595: return $this->_init['namespace'][$sig];
596: }
597:
598: $ns = $this->_getNamespaces();
599:
600: foreach (array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $additional) as $val) {
601: /* Skip namespaces if we have already auto-detected them. Also,
602: * hidden namespaces cannot be empty. */
603: if (!strlen($val) || isset($ns[$val->utf7imap])) {
604: continue;
605: }
606:
607: $mbox = $this->listMailboxes($val, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true, 'utf8' => true));
608: $first = reset($mbox);
609:
610: if ($first && ($first['mailbox'] == $val)) {
611: $ns[$val->utf7imap] = array(
612: 'delimiter' => $first['delimiter'],
613: 'hidden' => true,
614: 'name' => $val->utf7imap,
615: 'translation' => '',
616: 'type' => Horde_Imap_Client::NS_SHARED
617: );
618: }
619: }
620:
621: if (empty($ns)) {
622: /* This accurately determines the namespace information of the
623: * base namespace if the NAMESPACE command is not supported.
624: * See: RFC 3501 [6.3.8] */
625: $mbox = $this->listMailboxes('', Horde_Imap_Client::MBOX_ALL, array('delimiter' => true, 'utf8' => true));
626: $first = reset($mbox);
627: $ns[''] = array(
628: 'delimiter' => $first['delimiter'],
629: 'hidden' => false,
630: 'name' => '',
631: 'translation' => '',
632: 'type' => Horde_Imap_Client::NS_PERSONAL
633: );
634: }
635:
636: $this->_setInit('namespace', array_merge($this->_init['namespace'], array($sig => $ns)));
637:
638: return $ns;
639: }
640:
641: /**
642: * Get the NAMESPACE information from the IMAP server.
643: *
644: * @return array An array of namespace information. See getNamespaces()
645: * for format.
646: *
647: * @throws Horde_Imap_Client_Exception
648: */
649: abstract protected function _getNamespaces();
650:
651: /**
652: * Display if connection to the server has been secured via TLS or SSL.
653: *
654: * @return boolean True if the IMAP connection is secured.
655: */
656: public function isSecureConnection()
657: {
658: return $this->_isSecure;
659: }
660:
661: /**
662: * Return a list of alerts that MUST be presented to the user (RFC 3501
663: * [7.1]).
664: *
665: * @return array An array of alert messages.
666: */
667: abstract public function alerts();
668:
669: /**
670: * Login to the IMAP server.
671: *
672: * @throws Horde_Imap_Client_Exception
673: */
674: public function login()
675: {
676: if ($this->_isAuthenticated) {
677: return;
678: }
679:
680: if ($this->_login()) {
681: if (!empty($this->_params['id'])) {
682: try {
683: $this->sendID();
684: } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
685: // Ignore if server doesn't support ID extension.
686: }
687: }
688:
689: if (!empty($this->_params['comparator'])) {
690: try {
691: $this->setComparator();
692: } catch (Horde_Imap_Client_Exception_NoSupportExtension $e) {
693: // Ignore if server doesn't support I18NLEVEL=2
694: }
695: }
696:
697: /* Check for ability to cache flags here. */
698: if (!isset($this->_init['enabled']['CONDSTORE'])) {
699: unset($this->_params['cache']['fields'][Horde_Imap_Client::FETCH_FLAGS]);
700: }
701: }
702:
703: $this->_isAuthenticated = true;
704: }
705:
706: /**
707: * Login to the IMAP server.
708: *
709: * @return boolean Return true if global login tasks should be run.
710: *
711: * @throws Horde_Imap_Client_Exception
712: */
713: abstract protected function _login();
714:
715: /**
716: * Logout from the IMAP server (see RFC 3501 [6.1.3]).
717: */
718: public function logout()
719: {
720: if ($this->_isAuthenticated) {
721: $this->_logout();
722: $this->_isAuthenticated = false;
723: }
724: $this->_selected = null;
725: $this->_mode = 0;
726: }
727:
728: /**
729: * Logout from the IMAP server (see RFC 3501 [6.1.3]).
730: */
731: abstract protected function _logout();
732:
733: /**
734: * Send ID information to the IMAP server (RFC 2971).
735: *
736: * @param array $info Overrides the value of the 'id' param and sends
737: * this information instead.
738: *
739: * @throws Horde_Imap_Client_Exception
740: * @throws Horde_Imap_Client_Exception_NoSupportExtension
741: */
742: public function sendID($info = null)
743: {
744: if (!$this->queryCapability('ID')) {
745: $this->_exception('The IMAP server does not support the ID extension.', 'NO_SUPPORT');
746: }
747:
748: $this->_sendID(is_null($info) ? (empty($this->_params['id']) ? array() : $this->_params['id']) : $info);
749: }
750:
751: /**
752: * Send ID information to the IMAP server (RFC 2971).
753: *
754: * @param array $info The information to send to the server.
755: *
756: * @throws Horde_Imap_Client_Exception
757: */
758: abstract protected function _sendID($info);
759:
760: /**
761: * Return ID information from the IMAP server (RFC 2971).
762: *
763: * @return array An array of information returned, with the keys as the
764: * 'field' and the values as the 'value'.
765: *
766: * @throws Horde_Imap_Client_Exception
767: */
768: public function getID()
769: {
770: if (!$this->queryCapability('ID')) {
771: $this->_exception('The IMAP server does not support the ID extension.', 'NO_SUPPORT');
772: }
773:
774: return $this->_getID();
775: }
776:
777: /**
778: * Return ID information from the IMAP server (RFC 2971).
779: *
780: * @return array An array of information returned, with the keys as the
781: * 'field' and the values as the 'value'.
782: *
783: * @throws Horde_Imap_Client_Exception
784: */
785: abstract protected function _getID();
786:
787: /**
788: * Sets the preferred language for server response messages (RFC 5255).
789: *
790: * @param array $langs Overrides the value of the 'lang' param and sends
791: * this list of preferred languages instead. The
792: * special string 'i-default' can be used to restore
793: * the language to the server default.
794: *
795: * @return string The language accepted by the server, or null if the
796: * default language is used.
797: *
798: * @throws Horde_Imap_Client_Exception
799: */
800: public function setLanguage($langs = null)
801: {
802: $lang = null;
803:
804: if ($this->queryCapability('LANGUAGE')) {
805: $lang = is_null($langs)
806: ? (empty($this->_params['lang']) ? null : $this->_params['lang'])
807: : $langs;
808: }
809:
810: return is_null($lang)
811: ? null
812: : $this->_setLanguage($lang);
813: }
814:
815: /**
816: * Sets the preferred language for server response messages (RFC 5255).
817: *
818: * @param array $langs The preferred list of languages.
819: *
820: * @return string The language accepted by the server, or null if the
821: * default language is used.
822: *
823: * @throws Horde_Imap_Client_Exception
824: */
825: abstract protected function _setLanguage($langs);
826:
827: /**
828: * Gets the preferred language for server response messages (RFC 5255).
829: *
830: * @param array $list If true, return the list of available languages.
831: *
832: * @return mixed If $list is true, the list of languages available on the
833: * server (may be empty). If false, the language used by
834: * the server, or null if the default language is used.
835: *
836: * @throws Horde_Imap_Client_Exception
837: */
838: public function getLanguage($list = false)
839: {
840: if (!$this->queryCapability('LANGUAGE')) {
841: return $list ? array() : null;
842: }
843:
844: return $this->_getLanguage($list);
845: }
846:
847: /**
848: * Gets the preferred language for server response messages (RFC 5255).
849: *
850: * @param array $list If true, return the list of available languages.
851: *
852: * @return mixed If $list is true, the list of languages available on the
853: * server (may be empty). If false, the language used by
854: * the server, or null if the default language is used.
855: *
856: * @throws Horde_Imap_Client_Exception
857: */
858: abstract protected function _getLanguage($list);
859:
860: /**
861: * Open a mailbox.
862: *
863: * @param mixed $mailbox The mailbox to open. Either a
864: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
865: * or a string (UTF-8).
866: * @param integer $mode The access mode. Either
867: * - Horde_Imap_Client::OPEN_READONLY
868: * - Horde_Imap_Client::OPEN_READWRITE
869: * - Horde_Imap_Client::OPEN_AUTO
870: *
871: * @throws Horde_Imap_Client_Exception
872: */
873: public function openMailbox($mailbox, $mode = Horde_Imap_Client::OPEN_AUTO)
874: {
875: $this->login();
876:
877: $change = false;
878: $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
879:
880: if ($mode == Horde_Imap_Client::OPEN_AUTO) {
881: if (is_null($this->_selected) ||
882: !$mailbox->equals($this->_selected)) {
883: $mode = Horde_Imap_Client::OPEN_READONLY;
884: $change = true;
885: }
886: } elseif (is_null($this->_selected) ||
887: !$mailbox->equals($this->_selected) ||
888: ($mode != $this->_mode)) {
889: $change = true;
890: }
891:
892: if ($change) {
893: $this->_openMailbox($mailbox, $mode);
894: $this->_selected = $mailbox;
895: $this->_mode = $mode;
896: unset($this->_temp['statuscache'][strval($mailbox)]);
897: }
898: }
899:
900: /**
901: * Open a mailbox.
902: *
903: * @param Horde_Imap_Client_Mailbox $mailbox The mailbox to open.
904: * @param integer $mode The access mode.
905: *
906: * @throws Horde_Imap_Client_Exception
907: */
908: abstract protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox,
909: $mode);
910:
911: /**
912: * Return the currently opened mailbox and access mode.
913: *
914: * @param array $options Additional options:
915: * - utf8: (boolean) True if 'mailbox' should be in UTF-8 [DEPRECATED].
916: * DEFAULT: 'mailbox' returned in UTF7-IMAP.
917: *
918: * @return mixed Null if no mailbox selected, or an array with two
919: * elements:
920: * - mailbox: (mixed) If 'utf8' is true, returns a
921: * Horde_Imap_Client_Mailbox object. Otherwise, returns a
922: * string (UTF7-IMAP; DEPRECATED).
923: * - mode: (integer) Current mode.
924: *
925: * @throws Horde_Imap_Client_Exception
926: */
927: public function currentMailbox()
928: {
929: return is_null($this->_selected)
930: ? null
931: : array(
932: 'mailbox' => (empty($options['utf8']) ? $this->_selected->utf7imap : clone($this->_selected)),
933: 'mode' => $this->_mode
934: );
935: }
936:
937: /**
938: * Create a mailbox.
939: *
940: * @param mixed $mailbox The mailbox to create. Either a
941: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
942: * or a string (UTF-8).
943: * @param array $opts Additional options:
944: * - special_use: (array) An array of special-use flags to mark the
945: * mailbox with. The server MUST support RFC 6154.
946: *
947: * @throws Horde_Imap_Client_Exception
948: */
949: public function createMailbox($mailbox, array $opts = array())
950: {
951: $this->login();
952:
953: if (!$this->queryCapability('CREATE-SPECIAL-USE')) {
954: unset($opts['special_use']);
955: }
956:
957: $this->_createMailbox(Horde_Imap_Client_Mailbox::get($mailbox, null), $opts);
958: }
959:
960: /**
961: * Create a mailbox.
962: *
963: * @param Horde_Imap_Client_Mailbox $mailbox The mailbox to create.
964: * @param array $opts Additional options. See
965: * createMailbox().
966: *
967: * @throws Horde_Imap_Client_Exception
968: */
969: abstract protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox,
970: $opts);
971:
972: /**
973: * Delete a mailbox.
974: *
975: * @param mixed $mailbox The mailbox to delete. Either a
976: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
977: * or a string (UTF-8).
978: *
979: * @throws Horde_Imap_Client_Exception
980: */
981: public function deleteMailbox($mailbox)
982: {
983: $this->login();
984:
985: $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
986:
987: $this->_deleteMailbox($mailbox);
988:
989: /* Delete mailbox caches. */
990: if ($this->_initCache()) {
991: $this->cache->deleteMailbox($mailbox);
992: }
993: unset($this->_temp['statuscache'][strval($mailbox)]);
994:
995: /* Unsubscribe from mailbox. */
996: try {
997: $this->subscribeMailbox($mailbox, false);
998: } catch (Horde_Imap_Client_Exception $e) {
999: // Ignore failed unsubscribe request
1000: }
1001: }
1002:
1003: /**
1004: * Delete a mailbox.
1005: *
1006: * @param Horde_Imap_Client_Mailbox $mailbox The mailbox to delete.
1007: *
1008: * @throws Horde_Imap_Client_Exception
1009: */
1010: abstract protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox);
1011:
1012: /**
1013: * Rename a mailbox.
1014: *
1015: * @param mixed $old The old mailbox name. Either a
1016: * Horde_Imap_Client_Mailbox object (as of 1.2.0) or a
1017: * string (UTF-8).
1018: * @param mixed $new The new mailbox name. Either a
1019: * Horde_Imap_Client_Mailbox object (as of 1.2.0) or a
1020: * string (UTF-8).
1021: *
1022: * @throws Horde_Imap_Client_Exception
1023: */
1024: public function renameMailbox($old, $new)
1025: {
1026: // Login will be handled by first listMailboxes() call.
1027:
1028: $old = Horde_Imap_Client_Mailbox::get($old, null);
1029: $new = Horde_Imap_Client_Mailbox::get($new, null);
1030:
1031: /* Check if old mailbox(es) were subscribed to. */
1032: $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_SUBSCRIBED, array('delimiter' => true, 'utf8' => true));
1033: if (empty($base)) {
1034: $base = $this->listMailboxes($old, Horde_Imap_Client::MBOX_ALL, array('delimiter' => true, 'utf8' => true));
1035: $base = reset($base);
1036: $subscribed = array();
1037: } else {
1038: $base = reset($base);
1039: $subscribed = array($base['mailbox']);
1040: }
1041:
1042: $all_mboxes = array($base['mailbox']);
1043: if (strlen($base['delimiter'])) {
1044: $all_mboxes = array_merge($all_mboxes, $this->listMailboxes($old . $base['delimiter'] . '*', Horde_Imap_Client::MBOX_ALL, array('flat' => true, 'utf8' => true)));
1045: $subscribed = array_merge($subscribed, $this->listMailboxes($old . $base['delimiter'] . '*', Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true, 'utf8' => true)));
1046: }
1047:
1048: $this->_renameMailbox($old, $new);
1049:
1050: /* Delete mailbox caches. */
1051: foreach ($all_mboxes as $val) {
1052: if ($this->_initCache()) {
1053: $this->cache->deleteMailbox($val);
1054: }
1055: unset($this->_temp['statuscache'][strval($val)]);
1056: }
1057:
1058: foreach ($subscribed as $val) {
1059: /* Clean up subscription information. */
1060: try {
1061: $this->subscribeMailbox($val, false);
1062: $this->subscribeMailbox(new Horde_Imap_Client_Mailbox(substr_replace($val, $new, 0, strlen($old))));
1063: } catch (Horde_Imap_Client_Exception $e) {
1064: // Ignore failed unsubscribe request
1065: }
1066: }
1067: }
1068:
1069: /**
1070: * Rename a mailbox.
1071: *
1072: * @param Horde_Imap_Client_Mailbox $old The old mailbox name.
1073: * @param Horde_Imap_Client_Mailbox $new The new mailbox name.
1074: *
1075: * @throws Horde_Imap_Client_Exception
1076: */
1077: abstract protected function _renameMailbox(Horde_Imap_Client_Mailbox$old,
1078: Horde_Imap_Client_Mailbox $new);
1079:
1080: /**
1081: * Manage subscription status for a mailbox.
1082: *
1083: * @param mixed $mailbox The mailbox to [un]subscribe to. Either a
1084: * Horde_Imap_Client_Mailbox object (as of
1085: * 1.2.0) or a string (UTF-8).
1086: * @param boolean $subscribe True to subscribe, false to unsubscribe.
1087: *
1088: * @throws Horde_Imap_Client_Exception
1089: */
1090: public function subscribeMailbox($mailbox, $subscribe = true)
1091: {
1092: $this->login();
1093: $this->_subscribeMailbox(Horde_Imap_Client_Mailbox::get($mailbox, null), (bool)$subscribe);
1094: }
1095:
1096: /**
1097: * Manage subscription status for a mailbox.
1098: *
1099: * @param Horde_Imap_Client_Mailbox $mailbox The mailbox to [un]subscribe
1100: * to.
1101: * @param boolean $subscribe True to subscribe, false to
1102: * unsubscribe.
1103: *
1104: * @throws Horde_Imap_Client_Exception
1105: */
1106: abstract protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox,
1107: $subscribe);
1108:
1109: /**
1110: * Obtain a list of mailboxes matching a pattern.
1111: *
1112: * @param mixed $pattern The mailbox search pattern(s) (see RFC 3501
1113: * [6.3.8] for the format). A UTF-8 string or an
1114: * array of strings.
1115: * @param integer $mode Which mailboxes to return. Either:
1116: * - Horde_Imap_Client::MBOX_SUBSCRIBED
1117: * - Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS
1118: * - Horde_Imap_Client::MBOX_UNSUBSCRIBED
1119: * - Horde_Imap_Client::MBOX_ALL
1120: * @param array $options Additional options:
1121: * <ul>
1122: * <li>
1123: * attributes: (boolean) If true, return attribute information under
1124: * the 'attributes' key.
1125: * DEFAULT: Do not return this information.
1126: * </li>
1127: * <li>
1128: * children: (boolean) Tell server to return children attribute
1129: * information (\HasChildren, \HasNoChildren). Requires the
1130: * LIST-EXTENDED extension to guarantee this information is returned.
1131: * Server MAY return this attribute without this option, or if the
1132: * CHILDREN extension is available, but it is not guaranteed.
1133: * DEFAULT: false
1134: * </li>
1135: * <li>
1136: * delimiter: (boolean) If true, return delimiter information under the
1137: * 'delimiter' key.
1138: * DEFAULT: Do not return this information.
1139: * </li>
1140: * <li>
1141: * flat: (boolean) If true, return a flat list of mailbox names only.
1142: * Overrides both the 'attributes' and 'delimiter' options.
1143: * DEFAULT: Do not return flat list.
1144: * </li>
1145: * <li>
1146: * recursivematch: (boolean) Force the server to return information
1147: * about parent mailboxes that don't match other selection options, but
1148: * have some submailboxes that do. Information about children is
1149: * returned in the CHILDINFO extended data item ('extended'). Requires
1150: * the LIST-EXTENDED extension.
1151: * DEFAULT: false
1152: * </li>
1153: * <li>
1154: * remote: (boolean) Tell server to return mailboxes that reside on
1155: * another server. Requires the LIST-EXTENDED extension.
1156: * DEFAULT: false
1157: * </li>
1158: * <li>
1159: * special_use: (boolean) Tell server to return special-use attribute
1160: * information (\Drafts, \Flagged, \Junk, \Sent, \Trash, \All,
1161: * \Archive). Server must support the SPECIAL-USE return option for this
1162: * setting to have any effect. Server MAY return this attribute without
1163: * this option.
1164: * DEFAULT: false
1165: * <li>
1166: * status: (integer) Tell server to return status information. The
1167: * value is a bitmask that may contain any of:
1168: * <ul>
1169: * <li>Horde_Imap_Client::STATUS_MESSAGES</li>
1170: * <li>Horde_Imap_Client::STATUS_RECENT</li>
1171: * <li>Horde_Imap_Client::STATUS_UIDNEXT</li>
1172: * <li>Horde_Imap_Client::STATUS_UIDVALIDITY</li>
1173: * <li>Horde_Imap_Client::STATUS_UNSEEN</li>
1174: * <li>Horde_Imap_Client::STATUS_HIGHESTMODSEQ</li>
1175: * </ul>
1176: * Requires the LIST-STATUS extension.
1177: * DEFAULT: 0
1178: * </li>
1179: * <li>
1180: * sort: (boolean) If true, return a sorted list of mailboxes?
1181: * DEFAULT: Do not sort the list.
1182: * </li>
1183: * <li>
1184: * sort_delimiter: (string) If 'sort' is true, this is the delimiter
1185: * used to sort the mailboxes.
1186: * DEFAULT: '.'
1187: * </li>
1188: * <li>
1189: * utf8: (boolean) True to return mailbox names in UTF-8.
1190: * DEFAULT: Names are returned in UTF7-IMAP.
1191: * </li>
1192: * </ul>
1193: *
1194: * @return array If 'flat' option is true, the array values are a list
1195: * of Horde_Imap_Client_Mailbox objects (if the 'utf8'
1196: * parameter is true) or a list of UTF7-IMAP strings.
1197: * Otherwise, the array values are arrays with these keys:
1198: * - attributes: (array) List of lower-cased attributes [only if
1199: * 'attributes' option is true].
1200: * - delimiter: (string) The delimiter for the mailbox [only if
1201: * 'delimiter' option is true].
1202: * - extended: (TODO) TODO [only if 'recursivematch' option is true and
1203: * LIST-EXTENDED extension is supported on the server].
1204: * - mailbox: (mixed) The mailbox. A Horde_Imap_Client_Mailbox object if
1205: * the 'utf8' parameter is true, or a UTF7-IMAP string.
1206: * - status: (array) See status() [only if 'status' option is true and
1207: * LIST-STATUS extension is supported on the server].
1208: *
1209: * @throws Horde_Imap_Client_Exception
1210: */
1211: public function listMailboxes($pattern, $mode = Horde_Imap_Client::MBOX_ALL,
1212: $options = array())
1213: {
1214: $this->login();
1215:
1216: if (!is_array($pattern)) {
1217: $pattern = array($pattern);
1218: }
1219:
1220: if (isset($options['special_use']) &&
1221: !$this->queryCapability('SPECIAL-USE')) {
1222: unset($options['special_use']);
1223: }
1224:
1225: $ret = $this->_listMailboxes(
1226: array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $pattern, array_fill(0, count($pattern), null)),
1227: $mode,
1228: $options
1229: );
1230:
1231: if (!empty($options['sort'])) {
1232: Horde_Imap_Client_Sort::sortMailboxes($ret, array('delimiter' => empty($options['sort_delimiter']) ? '.' : $options['sort_delimiter'], 'index' => false, 'keysort' => empty($options['flat'])));
1233: }
1234:
1235: return $ret;
1236: }
1237:
1238: /**
1239: * Obtain a list of mailboxes matching a pattern.
1240: *
1241: * @param array $pattern The mailbox search patterns (UTF7-IMAP strings).
1242: * @param integer $mode Which mailboxes to return.
1243: * @param array $options Additional options.
1244: *
1245: * @return array See listMailboxes().
1246: *
1247: * @throws Horde_Imap_Client_Exception
1248: */
1249: abstract protected function _listMailboxes($pattern, $mode, $options);
1250:
1251: /**
1252: * Obtain status information for a mailbox.
1253: *
1254: * @param mixed $mailbox The mailbox to query. Either a
1255: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
1256: * or a string (UTF-8).
1257: * @param integer $flags A bitmask of information requested from the
1258: * server. Allowed flags:
1259: * <ul>
1260: * <li>
1261: * Horde_Imap_Client::STATUS_MESSAGES
1262: * <ul>
1263: * <li>
1264: * Return key: messages
1265: * </li>
1266: * <li>
1267: * Return format: (integer) The number of messages in the mailbox.
1268: * </li>
1269: * </ul>
1270: * </li>
1271: * <li>
1272: * Horde_Imap_Client::STATUS_RECENT
1273: * <ul>
1274: * <li>
1275: * Return key: recent
1276: * </li>
1277: * <li>
1278: * Return format: (integer) The number of messages with the \Recent
1279: * flag set
1280: * </li>
1281: * </ul>
1282: * </li>
1283: * <li>
1284: * Horde_Imap_Client::STATUS_UIDNEXT
1285: * <ul>
1286: * <li>
1287: * Return key: uidnext
1288: * </li>
1289: * <li>
1290: * Return format: (integer) The next UID to be assigned in the
1291: * mailbox.
1292: * </li>
1293: * </ul>
1294: * </li>
1295: * <li>
1296: * Horde_Imap_Client::STATUS_UIDVALIDITY
1297: * <ul>
1298: * <li>
1299: * Return key: uidvalidity
1300: * </li>
1301: * <li>
1302: * Return format: (integer) The unique identifier validity of the
1303: * mailbox.
1304: * </li>
1305: * </ul>
1306: * </li>
1307: * <li>
1308: * Horde_Imap_Client::STATUS_UNSEEN
1309: * <ul>
1310: * <li>
1311: * Return key: unseen
1312: * </li>
1313: * <li>
1314: * Return format: (integer) The number of messages which do not have
1315: * the \Seen flag set.
1316: * </li>
1317: * </ul>
1318: * </li>
1319: * <li>
1320: * Horde_Imap_Client::STATUS_FIRSTUNSEEN
1321: * <ul>
1322: * <li>
1323: * Return key: firstunseen
1324: * </li>
1325: * <li>
1326: * Return format: (integer) The sequence number of the first unseen
1327: * message in the mailbox.
1328: * </li>
1329: * </ul>
1330: * </li>
1331: * <li>
1332: * Horde_Imap_Client::STATUS_FLAGS
1333: * <ul>
1334: * <li>
1335: * Return key: flags
1336: * </li>
1337: * <li>
1338: * Return format: (array) The list of defined flags in the mailbox
1339: * (all flags are in lowercase).
1340: * </li>
1341: * </ul>
1342: * </li>
1343: * <li>
1344: * Horde_Imap_Client::STATUS_PERMFLAGS
1345: * <ul>
1346: * <li>
1347: * Return key: permflags
1348: * </li>
1349: * <li>
1350: * Return format: (array) The list of flags that a client can change
1351: * permanently (all flags are in lowercase).
1352: * </li>
1353: * </ul>
1354: * </li>
1355: * <li>
1356: * Horde_Imap_Client::STATUS_HIGHESTMODSEQ
1357: * <ul>
1358: * <li>
1359: * Return key: highestmodseq
1360: * </li>
1361: * <li>
1362: * Return format: (integer) If the server supports the CONDSTORE
1363: * IMAP extension, this will be the highest mod-sequence value of all
1364: * messages in the mailbox. Else 0 if CONDSTORE not available or the
1365: * mailbox does not support mod-sequences.
1366: * </li>
1367: * </ul>
1368: * </li>
1369: * <li>
1370: * Horde_Imap_Client::STATUS_LASTMODSEQ
1371: * <ul>
1372: * <li>
1373: * Return key: lastmodseq
1374: * </li>
1375: * <li>
1376: * Return format: (integer) If the server supports the CONDSTORE
1377: * IMAP extension, this will be the cached mod-sequence value of the
1378: * mailbox when it was first opened if HIGHESTMODSEQ changed. Else 0
1379: * if CONDSTORE not available, the mailbox does not support
1380: * mod-sequences, or the mod-sequence did not change.
1381: * </li>
1382: * </ul>
1383: * </li>
1384: * <li>
1385: * Horde_Imap_Client::STATUS_LASTMODSEQUIDS
1386: * <ul>
1387: * <li>
1388: * Return key: lastmodsequids
1389: * </li>
1390: * <li>
1391: * Return format: (array) If the server supports the QRESYNC IMAP
1392: * extension, this will be the list of UIDs changed in the mailbox
1393: * when it was first opened if HIGHESTMODSEQ changed. Else an empty
1394: * array if QRESYNC not available, the mailbox does not support
1395: * mod-sequences, or the mod-sequence did not change.
1396: * </li>
1397: * </ul>
1398: * </li>
1399: * <li>
1400: * Horde_Imap_Client::STATUS_UIDNOTSTICKY
1401: * <ul>
1402: * <li>
1403: * Return key: uidnotsticky
1404: * </li>
1405: * <li>
1406: * Return format: (boolean) If the server supports the UIDPLUS IMAP
1407: * extension, and the queried mailbox does not support persistent
1408: * UIDs, this value will be true. In all other cases, this value will
1409: * be false.
1410: * </li>
1411: * </ul>
1412: * </li>
1413: * <li>
1414: * Horde_Imap_Client::STATUS_ALL (DEFAULT)
1415: * <ul>
1416: * <li>
1417: * Shortcut to return 'messages', 'recent', 'uidnext', 'uidvalidity',
1418: * and 'unseen' values.
1419: * </li>
1420: * </ul>
1421: * </li>
1422: * </ul>
1423: *
1424: * @return array An array with the requested keys (see above).
1425: *
1426: * @throws Horde_Imap_Client_Exception
1427: */
1428: public function status($mailbox, $flags = Horde_Imap_Client::STATUS_ALL)
1429: {
1430: $this->login();
1431:
1432: $unselected_flags = array(
1433: 'messages' => Horde_Imap_Client::STATUS_MESSAGES,
1434: 'recent' => Horde_Imap_Client::STATUS_RECENT,
1435: 'unseen' => Horde_Imap_Client::STATUS_UNSEEN,
1436: 'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT,
1437: 'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY
1438: );
1439:
1440: if ($flags & Horde_Imap_Client::STATUS_ALL) {
1441: foreach ($unselected_flags as $val) {
1442: $flags |= $val;
1443: }
1444: }
1445:
1446: $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
1447: $ret = array();
1448:
1449: /* Check for cached information. */
1450: if ($mailbox->equals($this->_selected) &&
1451: isset($this->_temp['statuscache'][strval($mailbox)])) {
1452: $ptr = &$this->_temp['statuscache'][strval($mailbox)];
1453:
1454: foreach ($unselected_flags as $key => $val) {
1455: if (($flags & $val) && isset($ptr[$key])) {
1456: $ret[$key] = $ptr[$key];
1457: $flags &= ~$val;
1458: }
1459: }
1460: }
1461:
1462: /* Catch flags that are not supported. */
1463: if (($flags & Horde_Imap_Client::STATUS_HIGHESTMODSEQ) &&
1464: !isset($this->_init['enabled']['CONDSTORE'])) {
1465: $ret['highestmodseq'] = 0;
1466: $flags &= ~Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
1467: }
1468:
1469: if (($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY) &&
1470: !$this->queryCapability('UIDPLUS')) {
1471: $ret['uidnotsticky'] = false;
1472: $flags &= ~Horde_Imap_Client::STATUS_UIDNOTSTICKY;
1473: }
1474:
1475: /* Handle LASTMODSEQ related options. */
1476: if ($flags & Horde_Imap_Client::STATUS_LASTMODSEQ) {
1477: $ret['lastmodseq'] = 0;
1478: if (isset($this->_init['enabled']['CONDSTORE']) &&
1479: isset($this->_temp['lastmodseq'][strval($mailbox)])) {
1480: $ret['lastmodseq'] = $this->_temp['lastmodseq'][strval($mailbox)];
1481: }
1482: $flags &= ~Horde_Imap_Client::STATUS_LASTMODSEQ;
1483: }
1484:
1485: if ($flags & Horde_Imap_Client::STATUS_LASTMODSEQUIDS) {
1486: $ret['lastmodsequids'] = array();
1487: if (isset($this->_init['enabled']['CONDSTORE']) &&
1488: isset($this->_temp['lastmodsequids'][strval($mailbox)])) {
1489: $ret['lastmodsequids'] = $this->utils->fromSequenceString($this->_temp['lastmodsequids'][strval($mailbox)]);
1490: }
1491: $flags &= ~Horde_Imap_Client::STATUS_LASTMODSEQUIDS;
1492: }
1493:
1494: if (!$flags) {
1495: return $ret;
1496: }
1497:
1498: /* STATUS_PERMFLAGS requires a read/write mailbox. */
1499: if ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) {
1500: $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
1501: }
1502:
1503: $ret = array_merge($ret, $this->_status($mailbox, $flags));
1504:
1505: if (!$mailbox->equals($this->_selected)) {
1506: if (!isset($this->_temp['statuscache'])) {
1507: $this->_temp['statuscache'] = array();
1508: }
1509: $ptr = &$this->_temp['statuscache'];
1510:
1511: $ptr[strval($mailbox)] = isset($ptr[strval($mailbox)])
1512: ? array_merge($ptr[strval($mailbox)], $ret)
1513: : $ret;
1514: }
1515:
1516: return $ret;
1517: }
1518:
1519: /**
1520: * Obtain status information for a mailbox.
1521: *
1522: * @param Horde_Imap_Client_Mailbox $mailbox The mailbox to query.
1523: * @param integer $flags A bitmask of information
1524: * requested from the server.
1525: *
1526: * @return array See status().
1527: *
1528: * @throws Horde_Imap_Client_Exception
1529: */
1530: abstract protected function _status(Horde_Imap_Client_Mailbox $mailbox,
1531: $flags);
1532:
1533: /**
1534: * Perform a STATUS call on multiple mailboxes at the same time.
1535: *
1536: * This method leverages the LIST-EXTENDED and LIST-STATUS extensions on
1537: * the IMAP server to improve the efficiency of this operation.
1538: *
1539: * @param array $mailboxes The mailboxes to query. Either
1540: * Horde_Imap_Client_Mailbox objects (as of
1541: * 1.2.0), strings (UTF-8), or a combination of
1542: * the two.
1543: * @param integer $flags See status().
1544: * @param array $opts Additional options:
1545: * - sort: (boolean) If true, sort the list of mailboxes?
1546: * DEFAULT: Do not sort the list.
1547: * - sort_delimiter: (string) If 'sort' is true, this is the delimiter
1548: * used to sort the mailboxes.
1549: * DEFAULT: '.'
1550: *
1551: * @return array An array with the keys as the mailbox names (UTF7-IMAP)
1552: * and the values as arrays with the requested keys (from
1553: * the mask given in $flags).
1554: */
1555: public function statusMultiple($mailboxes,
1556: $flags = Horde_Imap_Client::STATUS_ALL,
1557: array $opts = array())
1558: {
1559: if (empty($mailboxes)) {
1560: return array();
1561: }
1562:
1563: $this->login();
1564:
1565: $opts = array_merge(array(
1566: 'sort' => false,
1567: 'sort_delimiter' => '.'
1568: ), $opts);
1569:
1570: $need_status = true;
1571: $ret = array();
1572:
1573: /* Optimization: If there is one mailbox in list, and we are already
1574: * in that mailbox, we should just do a straight STATUS call. */
1575: if ($this->queryCapability('LIST-STATUS') &&
1576: ((count($mailboxes) != 1) ||
1577: !Horde_Imap_Client_Mailbox::get(reset($mailboxes), null)->equals($this->_selected))) {
1578: $status = $to_process = array();
1579: foreach ($mailboxes as $val) {
1580: // Force mailboxes containing wildcards to be accessed via
1581: // STATUS so that wildcards do not return a bunch of mailboxes
1582: // in the LIST-STATUS response.
1583: if (strpbrk($val, '*%') === false) {
1584: $to_process[] = $val;
1585: }
1586: $status[strval($val)] = 1;
1587: }
1588:
1589: try {
1590: foreach ($this->listMailboxes($to_process, Horde_Imap_Client::MBOX_ALL, array_merge($opts, array('status' => $flags, 'utf8' => true))) as $val) {
1591: if (isset($val['status'])) {
1592: $ret[$val['mailbox']->utf7imap] = $val['status'];
1593: }
1594: unset($status[strval($val['mailbox'])]);
1595: }
1596: } catch (Horde_Imap_Client_Exception $e) {}
1597:
1598: if (empty($status)) {
1599: $need_status = false;
1600: } else {
1601: $mailboxes = array_keys($status);
1602: }
1603: }
1604:
1605: if ($need_status) {
1606: foreach ($mailboxes as $val) {
1607: $val = Horde_Imap_Client_Mailbox::get($val, null);
1608: try {
1609: $ret[$val->utf7imap] = $this->status($val, $flags);
1610: } catch (Horde_Imap_Client_Exception $e) {}
1611: }
1612: }
1613:
1614: if ($opts['sort']) {
1615: Horde_Imap_Client_Sort::sortMailboxes($ret, array(
1616: 'delimiter' => $opts['sort_delimiter'],
1617: 'keysort' => true
1618: ));
1619: }
1620:
1621: return $ret;
1622: }
1623:
1624: /**
1625: * Append message(s) to a mailbox.
1626: *
1627: * @param mixed $mailbox The mailbox to append the message(s) to. Either
1628: * a Horde_Imap_Client_Mailbox object (as of 1.2.0)
1629: * or a string (UTF-8).
1630: * @param array $data The message data to append, along with
1631: * additional options. An array of arrays with
1632: * each embedded array having the following
1633: * entries:
1634: * <ul>
1635: * <li>
1636: * data: (mixed) The data to append. If a string or a stream resource,
1637: * this will be used as the entire contents of a single message. If an
1638: * array, will catenate all given parts into a single message. This
1639: * array contains one or more arrays with two keys:
1640: * <ul>
1641: * <li>
1642: * t: (string) Either 'url' or 'text'.
1643: * </li>
1644: * <li>
1645: * v: (mixed) If 't' is 'url', this is the IMAP URL to the message
1646: * part to append. If 't' is 'text', this is either a string or
1647: * resource representation of the message part data.
1648: * DEFAULT: NONE (entry is MANDATORY)
1649: * </li>
1650: * </ul>
1651: * </li>
1652: * <li>
1653: * flags: (array) An array of flags/keywords to set on the appended
1654: * message.
1655: * DEFAULT: Only the \Recent flag is set.
1656: * </li>
1657: * <li>
1658: * internaldate: (DateTime) The internaldate to set for the appended
1659: * message.
1660: * DEFAULT: internaldate will be the same date as when the message was
1661: * appended.
1662: * </li>
1663: * </ul>
1664: * @param array $options Additonal options:
1665: * - create: (boolean) Try to create $mailbox if it does not exist?
1666: * DEFAULT: No.
1667: *
1668: * @return Horde_Imap_Client_Ids The UIDs of the appended messages.
1669: *
1670: * @throws Horde_Imap_Client_Exception
1671: */
1672: public function append($mailbox, $data, $options = array())
1673: {
1674: $this->login();
1675:
1676: $mailbox = Horde_Imap_Client_Mailbox::get($mailbox, null);
1677:
1678: $ret = $this->_append($mailbox, $data, $options);
1679: unset($this->_temp['statuscache'][strval($mailbox)]);
1680:
1681: if ($ret instanceof Horde_Imap_Client_Ids) {
1682: return $ret;
1683: }
1684:
1685: $uids = $this->getIdsOb();
1686:
1687: while (list(,$val) = each($data)) {
1688: if (is_string($val['data'])) {
1689: $text = $val['data'];
1690: } elseif (is_resource($val['data'])) {
1691: $text = '';
1692: rewind($val['data']);
1693: while (!feof($val['data'])) {
1694: $text .= fread($val['data'], 512);
1695: if (preg_match("/\n\r{2,}/", $text)) {
1696: break;
1697: }
1698: }
1699: }
1700:
1701: $headers = Horde_Mime_Headers::parseHeaders($text);
1702: $msgid = $headers->getValue('message-id');
1703:
1704: if ($msgid) {
1705: $search_query = new Horde_Imap_Client_Search_Query();
1706: $search_query->headerText('Message-ID', $msgid);
1707: $uidsearch = $this->search($mailbox, $search_query);
1708: $uids->add($uidsearch['match']);
1709: }
1710: }
1711:
1712: return $uids;
1713: }
1714:
1715: /**
1716: * Append message(s) to a mailbox.
1717: *
1718: * @param Horde_Imap_Client_Mailbox $mailbox The mailbox to append the
1719: * message(s) to.
1720: * @param array $data The message data.
1721: * @param array $options Additional options.
1722: *
1723: * @return mixed A Horde_Imap_Client_Ids object containing the UIDs of
1724: * the appended messages (if server supports UIDPLUS
1725: * extension) or true.
1726: *
1727: * @throws Horde_Imap_Client_Exception
1728: */
1729: abstract protected function _append(Horde_Imap_Client_Mailbox $mailbox,
1730: $data, $options);
1731:
1732: /**
1733: * Request a checkpoint of the currently selected mailbox (RFC 3501
1734: * [6.4.1]).
1735: *
1736: * @throws Horde_Imap_Client_Exception
1737: */
1738: public function check()
1739: {
1740: // CHECK only useful if we are already authenticated.
1741: if ($this->_isAuthenticated) {
1742: $this->_check();
1743: }
1744: }
1745:
1746: /**
1747: * Request a checkpoint of the currently selected mailbox.
1748: *
1749: * @throws Horde_Imap_Client_Exception
1750: */
1751: abstract protected function _check();
1752:
1753: /**
1754: * Close the connection to the currently selected mailbox, optionally
1755: * expunging all deleted messages (RFC 3501 [6.4.2]).
1756: *
1757: * @param array $options Additional options:
1758: * - expunge: (boolean) Expunge all messages flagged as deleted?
1759: * DEFAULT: No
1760: *
1761: * @throws Horde_Imap_Client_Exception
1762: */
1763: public function close($options = array())
1764: {
1765: // This check catches the non-logged in case.
1766: if (is_null($this->_selected)) {
1767: return;
1768: }
1769:
1770: /* If we are caching, search for deleted messages. */
1771: if (!empty($options['expunge']) &&
1772: $this->_initCache(true)) {
1773: $search_query = new Horde_Imap_Client_Search_Query();
1774: $search_query->flag(Horde_Imap_Client::FLAG_DELETED, true);
1775: $search_res = $this->search($this->_selected, $search_query);
1776: $mbox = $this->_selected;
1777: } else {
1778: $search_res = null;
1779: }
1780:
1781: $this->_close($options);
1782: $this->_selected = null;
1783: $this->_mode = 0;
1784:
1785: if (!is_null($search_res)) {
1786: $this->_deleteMsgs($mbox, $search_res['match']->ids);
1787: }
1788: }
1789:
1790: /**
1791: * Close the connection to the currently selected mailbox, optionally
1792: * expunging all deleted messages (RFC 3501 [6.4.2]).
1793: *
1794: * @param array $options Additional options.
1795: *
1796: * @throws Horde_Imap_Client_Exception
1797: */
1798: abstract protected function _close($options);
1799:
1800: /**
1801: * Expunge deleted messages from the given mailbox.
1802: *
1803: * @param mixed $mailbox The mailbox to expunge. Either a
1804: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
1805: * or a string (UTF-8).
1806: * @param array $options Additional options:
1807: * - ids: (Horde_Imap_Client_Ids) A list of messages to expunge, but
1808: * only if they are also flagged as deleted.
1809: * DEFAULT: All messages marked as deleted will be expunged.
1810: * - list: (boolean) If true, returns the list of expunged messages.
1811: * DEFAULT: false
1812: *
1813: * @return Horde_Imap_Client_Ids If 'list' option is true, returns the
1814: * list of expunged messages.
1815: *
1816: * @throws Horde_Imap_Client_Exception
1817: */
1818: public function expunge($mailbox, $options = array())
1819: {
1820: // Open mailbox call will handle the login.
1821: $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
1822:
1823: if (empty($options['ids'])) {
1824: $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
1825: } elseif ($options['ids']->isEmpty()) {
1826: return array();
1827: }
1828:
1829: return $this->_expunge($options);
1830: }
1831:
1832: /**
1833: * Expunge all deleted messages from the given mailbox.
1834: *
1835: * @param array $options Additional options.
1836: *
1837: * @return Horde_Imap_Client_Ids If 'list' option is true, returns the
1838: * list of expunged messages.
1839: *
1840: * @throws Horde_Imap_Client_Exception
1841: */
1842: abstract protected function _expunge($options);
1843:
1844: /**
1845: * Search a mailbox.
1846: *
1847: * @param mixed $mailbox The mailbox to search.
1848: * Either a
1849: * Horde_Imap_Client_Mailbox
1850: * object (as of 1.2.0) or a
1851: * string (UTF-8).
1852: * @param Horde_Imap_Client_Search_Query $query The search query.
1853: * Defaults to an ALL
1854: * search.
1855: * @param array $options Additional options:
1856: * <ul>
1857: * <li>
1858: * nocache: (boolean) Don't cache the results.
1859: * DEFAULT: false (results cached, if possible)
1860: * </li>
1861: * <li>
1862: * partial: (mixed) The range of results to return (message sequence
1863: * numbers).
1864: * DEFAULT: All messages are returned.
1865: * </li>
1866: * <li>
1867: * results: (array) The data to return. Consists of zero or more of
1868: * the following flags:
1869: * <ul>
1870: * <li>Horde_Imap_Client::SEARCH_RESULTS_COUNT</li>
1871: * <li>Horde_Imap_Client::SEARCH_RESULTS_MATCH (DEFAULT)</li>
1872: * <li>Horde_Imap_Client::SEARCH_RESULTS_MAX</li>
1873: * <li>Horde_Imap_Client::SEARCH_RESULTS_MIN</li>
1874: * <li>
1875: * Horde_Imap_Client::SEARCH_RESULTS_SAVE (This option is currently
1876: * meant for internal use only)
1877: * </li>
1878: * <li>Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY</li>
1879: * </ul>
1880: * </li>
1881: * <li>
1882: * sequence: (boolean) If true, returns an array of sequence numbers.
1883: * DEFAULT: Returns an array of UIDs
1884: * </li>
1885: * <li>
1886: * sort: (array) Sort the returned list of messages. Multiple sort
1887: * criteria can be specified. Any sort criteria can be sorted in reverse
1888: * order (instead of the default ascending order) by adding a
1889: * Horde_Imap_Client::SORT_REVERSE element to the array directly before
1890: * adding the sort element. The following sort criteria are available:
1891: * <ul>
1892: * <li>Horde_Imap_Client::SORT_ARRIVAL</li>
1893: * <li>Horde_Imap_Client::SORT_CC</li>
1894: * <li>Horde_Imap_Client::SORT_DATE</li>
1895: * <li>Horde_Imap_Client::SORT_FROM</li>
1896: * <li>Horde_Imap_Client::SORT_SEQUENCE</li>
1897: * <li>Horde_Imap_Client::SORT_SIZE</li>
1898: * <li>Horde_Imap_Client::SORT_SUBJECT</li>
1899: * <li>Horde_Imap_Client::SORT_TO</li>
1900: * <li>
1901: * [On servers that support SORT=DISPLAY, these criteria are also
1902: * available:]
1903: * <ul>
1904: * <li>Horde_Imap_Client::SORT_DISPLAYFROM</li>
1905: * <li>Horde_Imap_Client::SORT_DISPLAYTO</li>
1906: * </ul>
1907: * </li>
1908: * <li>
1909: * [On servers that support SEARCH=FUZZY, this criteria is also
1910: * available:]
1911: * <ul>
1912: * <li>Horde_Imap_Client::SORT_RELEVANCY</li>
1913: * </ul>
1914: * </li>
1915: * </ul>
1916: * </li>
1917: * </ul>
1918: *
1919: * @return array An array with the following keys:
1920: * - count: (integer) The number of messages that match the search
1921: * criteria. Always returned.
1922: * - match: (Horde_Imap_Client_Ids) The IDs that match $criteria, sorted
1923: * if the 'sort' modifier was set. Returned if
1924: * Horde_Imap_Client::SEARCH_RESULTS_MATCH is set.
1925: * - max: (integer) The UID (default) or message sequence number (if
1926: * 'sequence' is true) of the highest message that satisifies
1927: * $criteria. Returns null if no matches found. Returned if
1928: * Horde_Imap_Client::SEARCH_RESULTS_MAX is set.
1929: * - min: (integer) The UID (default) or message sequence number (if
1930: * 'sequence' is true) of the lowest message that satisifies
1931: * $criteria. Returns null if no matches found. Returned if
1932: * Horde_Imap_Client::SEARCH_RESULTS_MIN is set.
1933: * - modseq: (integer) The highest mod-sequence for all messages being
1934: * returned. Returned if 'sort' is false, the search query
1935: * includes a MODSEQ command, and the server supports the
1936: * CONDSTORE IMAP extension.
1937: * - relevancy: (array) The list of relevancy scores. Returned if
1938: * Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY is set and
1939: * the server supports FUZZY search matching.
1940: * - save: (boolean) Whether the search results were saved. This value
1941: * is meant for internal use only. Returned if 'sort' is false
1942: * and Horde_Imap_Client::SEARCH_RESULTS_SAVE is set.
1943: *
1944: * @throws Horde_Imap_Client_Exception
1945: */
1946: public function search($mailbox, $query = null, $options = array())
1947: {
1948: $this->login();
1949:
1950: if (empty($options['results'])) {
1951: $options['results'] = array(
1952: Horde_Imap_Client::SEARCH_RESULTS_MATCH,
1953: Horde_Imap_Client::SEARCH_RESULTS_COUNT
1954: );
1955: }
1956:
1957: // Default to an ALL search.
1958: if (is_null($query)) {
1959: $query = new Horde_Imap_Client_Search_Query();
1960: }
1961:
1962: $options['_query'] = $query->build($this->capability());
1963:
1964: /* RFC 6203: MUST NOT request relevancy results if we are not using
1965: * FUZZY searching. */
1966: if (in_array(Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY, $options['results']) &&
1967: !in_array('SEARCH=FUZZY', $options['_query']['exts_used'])) {
1968: throw new InvalidArgumentException('Cannot specify RELEVANCY results if not doing a FUZZY search.');
1969: }
1970:
1971: /* Optimization - if query is just for a count of either RECENT or
1972: * ALL messages, we can send status information instead. Can't
1973: * optimize with unseen queries because we may cause an infinite loop
1974: * between here and the status() call. */
1975: if ((count($options['results']) == 1) &&
1976: (reset($options['results']) == Horde_Imap_Client::SEARCH_RESULTS_COUNT)) {
1977: switch ($options['_query']['query']) {
1978: case 'ALL':
1979: $ret = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
1980: return array('count' => $ret['messages']);
1981:
1982: case 'RECENT':
1983: $ret = $this->status($this->_selected, Horde_Imap_Client::STATUS_RECENT);
1984: return array('count' => $ret['recent']);
1985: }
1986: }
1987:
1988: $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
1989:
1990: /* Take advantage of search result caching. If CONDSTORE available,
1991: * we can cache all queries and invalidate the cache when the MODSEQ
1992: * changes. If CONDSTORE not available, we can only store queries
1993: * that don't involve flags. We store results by hashing the options
1994: * array - the generated query is already added to '_query' key
1995: * above. */
1996: $cache = null;
1997: if (empty($options['nocache']) &&
1998: $this->_initCache(true) &&
1999: (isset($this->_init['enabled']['CONDSTORE']) ||
2000: !$query->flagSearch())) {
2001: $cache = $this->_getSearchCache('search', $this->_selected, $options);
2002: if (is_array($cache)) {
2003: if (isset($cache['data']['match'])) {
2004: $cache['data']['match'] = $this->getIdsOb($cache['data']['match']);
2005: }
2006: return $cache['data'];
2007: }
2008: }
2009:
2010: /* Optimization: Catch when there are no messages in a mailbox. */
2011: $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_HIGHESTMODSEQ);
2012: if ($status_res['messages'] ||
2013: in_array(Horde_Imap_Client::SEARCH_RESULTS_SAVE, $options['results'])) {
2014: $ret = $this->_search($query, $options);
2015: } else {
2016: $ret = array(
2017: 'count' => 0
2018: );
2019:
2020: foreach ($options['results'] as $val) {
2021: switch ($val) {
2022: case Horde_Imap_Client::SEARCH_RESULTS_MATCH:
2023: $ret['match'] = $this->getIdsOb();
2024: break;
2025:
2026: case Horde_Imap_Client::SEARCH_RESULTS_MAX:
2027: $ret['max'] = null;
2028: break;
2029:
2030: case Horde_Imap_Client::SEARCH_RESULTS_MIN:
2031: $ret['min'] = null;
2032: break;
2033:
2034: case Horde_Imap_Client::SEARCH_RESULTS_MIN:
2035: if (isset($status_res['highestmodseq'])) {
2036: $ret['modseq'] = $status_res['highestmodseq'];
2037: }
2038: break;
2039:
2040: case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY:
2041: $ret['relevancy'] = array();
2042: break;
2043: }
2044: }
2045: }
2046:
2047: if ($cache) {
2048: $save = $ret;
2049: if (isset($save['match'])) {
2050: $save['match'] = $this->utils->toSequenceString($ret['match'], array('nosort' => true));
2051: }
2052: $this->_setSearchCache($save, $cache);
2053: }
2054:
2055: return $ret;
2056: }
2057:
2058: /**
2059: * Search a mailbox.
2060: *
2061: * @param object $query The search query.
2062: * @param array $options Additional options. The '_query' key contains
2063: * the value of $query->build().
2064: *
2065: * @return Horde_Imap_Client_Ids An array of IDs.
2066: *
2067: * @throws Horde_Imap_Client_Exception
2068: */
2069: abstract protected function _search($query, $options);
2070:
2071: /**
2072: * Set the comparator to use for searching/sorting (RFC 5255).
2073: *
2074: * @param string $comparator The comparator string (see RFC 4790 [3.1] -
2075: * "collation-id" - for format). The reserved
2076: * string 'default' can be used to select
2077: * the default comparator.
2078: *
2079: * @throws Horde_Imap_Client_Exception
2080: * @throws Horde_Imap_Client_Exception_NoSupportExtension
2081: */
2082: public function setComparator($comparator = null)
2083: {
2084: $comp = is_null($comparator)
2085: ? (empty($this->_params['comparator']) ? null : $this->_params['comparator'])
2086: : $comparator;
2087: if (is_null($comp)) {
2088: return;
2089: }
2090:
2091: $this->login();
2092:
2093: $i18n = $this->queryCapability('I18NLEVEL');
2094: if (empty($i18n) || (max($i18n) < 2)) {
2095: $this->_exception('The IMAP server does not support changing SEARCH/SORT comparators.', 'NO_SUPPORT');
2096: }
2097:
2098: $this->_setComparator($comp);
2099: }
2100:
2101: /**
2102: * Set the comparator to use for searching/sorting (RFC 5255).
2103: *
2104: * @param string $comparator The comparator string (see RFC 4790 [3.1] -
2105: * "collation-id" - for format). The reserved
2106: * string 'default' can be used to select
2107: * the default comparator.
2108: *
2109: * @throws Horde_Imap_Client_Exception
2110: */
2111: abstract protected function _setComparator($comparator);
2112:
2113: /**
2114: * Get the comparator used for searching/sorting (RFC 5255).
2115: *
2116: * @return mixed Null if the default comparator is being used, or an
2117: * array of comparator information (see RFC 5255 [4.8]).
2118: *
2119: * @throws Horde_Imap_Client_Exception
2120: */
2121: public function getComparator()
2122: {
2123: $this->login();
2124:
2125: $i18n = $this->queryCapability('I18NLEVEL');
2126: if (empty($i18n) || (max($i18n) < 2)) {
2127: return null;
2128: }
2129:
2130: return $this->_getComparator();
2131: }
2132:
2133: /**
2134: * Get the comparator used for searching/sorting (RFC 5255).
2135: *
2136: * @return mixed Null if the default comparator is being used, or an
2137: * array of comparator information (see RFC 5255 [4.8]).
2138: *
2139: * @throws Horde_Imap_Client_Exception
2140: */
2141: abstract protected function _getComparator();
2142:
2143: /**
2144: * Thread sort a given list of messages (RFC 5256).
2145: *
2146: * @param mixed $mailbox The mailbox to query. Either a
2147: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
2148: * or a string (UTF-8).
2149: * @param array $options Additional options:
2150: * <ul>
2151: * <li>
2152: * criteria: (mixed) The following thread criteria are available:
2153: * <ul>
2154: * <li>Horde_Imap_Client::THREAD_ORDEREDSUBJECT</li>
2155: * <li>Horde_Imap_Client::THREAD_REFERENCES</li>
2156: * <li>Horde_Imap_Client::THREAD_REFS</li>
2157: * <li>
2158: * Other algorithms can be explicitly specified by passing the IMAP
2159: * thread algorithm in as a string value.
2160: * </li>
2161: * </ul>
2162: * DEFAULT: Horde_Imap_Client::THREAD_ORDEREDSUBJECT
2163: * </li>
2164: * <li>
2165: * search: (Horde_Imap_Client_Search_Query) The search query.
2166: * DEFAULT: All messages in mailbox included in thread sort.
2167: * </li>
2168: * <li>
2169: * sequence: (boolean) If true, each message is stored and referred to
2170: * by its message sequence number.
2171: * DEFAULT: Stored/referred to by UID.
2172: * </li>
2173: * </ul>
2174: *
2175: * @return Horde_Imap_Client_Data_Thread A thread data object.
2176: *
2177: * @throws Horde_Imap_Client_Exception
2178: */
2179: public function thread($mailbox, $options = array())
2180: {
2181: // Open mailbox call will handle the login.
2182: $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2183:
2184: /* Take advantage of search result caching. If CONDSTORE available,
2185: * we can cache all queries and invalidate the cache when the MODSEQ
2186: * changes. If CONDSTORE not available, we can only store queries
2187: * that don't involve flags. See search() for similar caching. */
2188: $cache = null;
2189: if ($this->_initCache(true) &&
2190: (isset($this->_init['enabled']['CONDSTORE']) ||
2191: empty($options['search']) ||
2192: !$options['search']->flagSearch())) {
2193: $cache = $this->_getSearchCache('thread', $this->_selected, $options);
2194: if (is_array($cache)) {
2195: if ($cache['data'] instanceof Horde_Imap_Client_Data_Thread) {
2196: return $cache['data'];
2197: }
2198: $cache = $cache['id'];
2199: }
2200: }
2201:
2202: $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_MESSAGES);
2203:
2204: $ob = new Horde_Imap_Client_Data_Thread($status_res['messages'] ? $this->_thread($options) : array(), empty($options['sequence']) ? 'uid' : 'sequence');
2205:
2206: if ($cache) {
2207: $this->_setSearchCache($ob, $cache);
2208: }
2209:
2210: return $ob;
2211: }
2212:
2213: /**
2214: * Thread sort a given list of messages (RFC 5256).
2215: *
2216: * @param array $options Additional options. See thread().
2217: *
2218: * @return array An array with the following values, one per message,
2219: * with the key being either the UID (default) or the
2220: * message sequence number (if 'sequence' is true). Values
2221: * of each entry:
2222: * - b (base): (integer) [OPTIONAL] The ID of the base message. Is not
2223: * set, this is the only message in the thread.
2224: * DEFAULT: Only message in thread
2225: * - l (level): (integer) [OPTIONAL] The thread level of this
2226: * message (1 = base).
2227: * DEFAULT: 0
2228: * - s (subthread): (boolean) [OPTIONAL] Are there more messages in this
2229: * subthread?
2230: * DEFAULT: No
2231: *
2232: * @throws Horde_Imap_Client_Exception
2233: */
2234: abstract protected function _thread($options);
2235:
2236: /**
2237: * Fetch message data (see RFC 3501 [6.4.5]).
2238: *
2239: * @param mixed $mailbox The mailbox to search.
2240: * Either a
2241: * Horde_Imap_Client_Mailbox
2242: * object (as of 1.2.0) or a
2243: * string (UTF-8).
2244: * @param Horde_Imap_Client_Fetch_Query $query Fetch query object.
2245: * @param array $options Additional options:
2246: * - changedsince: (integer) Only return messages that have a
2247: * mod-sequence larger than this value. This option
2248: * requires the CONDSTORE IMAP extension (if not present,
2249: * this value is ignored). Additionally, the mailbox
2250: * must support mod-sequences or an exception will be
2251: * thrown. If valid, this option implicity adds the
2252: * mod-sequence fetch criteria to the fetch command.
2253: * DEFAULT: Mod-sequence values are ignored.
2254: * - fetch_res: (array) A partial results array to have fetch results
2255: * added to. [DEPRECATED]
2256: * - ids: (Horde_Imap_Client_Ids) A list of messages to fetch data from.
2257: * DEFAULT: All messages in $mailbox will be fetched.
2258: * - vanished: (boolean) Only return messages from the UID set parameter
2259: * that have been expunged and whose associated mod-sequence
2260: * is larger than the specified mod-sequence. This option
2261: * requires the QRESYNC IMAP extension and requires
2262: * 'changedsince' to be set, and requires 'ids' to be UIDs.
2263: * [DEPRECATED]
2264: * DEFAULT: Vanished search ignored.
2265: *
2266: * @return array An array of fetch results. The array consists of
2267: * keys that correspond to 'ids', and values that
2268: * contain Horde_Imap_Query_Data_Fetch objects.
2269: *
2270: * @throws Horde_Imap_Client_Exception
2271: */
2272: public function fetch($mailbox, $query, $options = array())
2273: {
2274: $this->login();
2275:
2276: $query = clone $query;
2277:
2278: $cache_array = $header_cache = $new_query = array();
2279: $qresync = isset($this->_init['enabled']['QRESYNC']);
2280: $res_seq = null;
2281:
2282: if (empty($options['ids'])) {
2283: $options['ids'] = $this->getIdsOb(empty($options['fetch_res']) ? Horde_Imap_Client_Ids::ALL : array_keys($options['fetch_res']));
2284: if ($options['ids']->isEmpty()) {
2285: return array();
2286: }
2287: } elseif ($options['ids']->isEmpty()) {
2288: return array();
2289: } elseif ($options['ids']->search_res &&
2290: !$this->queryCapability('SEARCHRES')) {
2291: /* SEARCHRES requires server support. */
2292: $this->_exception('Server does not support saved searches.', 'NO_SUPPORT');
2293: }
2294:
2295: /* The 'vanished' modifier requires QRESYNC, 'changedsince', and IDs
2296: * that are not sequence numbers. */
2297: if (!empty($options['vanished'])) {
2298: if (!$qresync) {
2299: $this->_exception('Server does not support the QRESYNC extension.', 'NO_SUPPORT');
2300: } elseif ($options['ids']->sequence) {
2301: throw new InvalidArgumentException('The vanished FETCH modifier requires UIDs.');
2302: } elseif (empty($options['changedsince'])) {
2303: throw new InvalidArgumentException('The vanished FETCH modifier requires the changedsince parameter.');
2304: }
2305: }
2306:
2307: $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_AUTO);
2308:
2309: $cf = $this->_initCache(true)
2310: ? $this->_params['cache']['fields']
2311: : array();
2312:
2313: if (!empty($cf)) {
2314: /* We need the UIDVALIDITY for the current mailbox. */
2315: $status_res = $this->status($this->_selected, Horde_Imap_Client::STATUS_HIGHESTMODSEQ | Horde_Imap_Client::STATUS_UIDVALIDITY);
2316:
2317: /* If using cache, we store by UID so we need to return UIDs. */
2318: $query->uid();
2319: }
2320:
2321: if ($query->contains(Horde_Imap_Client::FETCH_MODSEQ) &&
2322: !isset($this->_init['enabled']['CONDSTORE'])) {
2323: unset($query[$k]);
2324: }
2325:
2326: /* Determine if caching is available and if anything in $query is
2327: * cacheable.
2328: * TODO: Re-add base headertext caching. */
2329: foreach ($cf as $k => $v) {
2330: if (isset($query[$k])) {
2331: switch ($k) {
2332: case Horde_Imap_Client::FETCH_ENVELOPE:
2333: case Horde_Imap_Client::FETCH_IMAPDATE:
2334: case Horde_Imap_Client::FETCH_SIZE:
2335: case Horde_Imap_Client::FETCH_STRUCTURE:
2336: $cache_array[$k] = $v;
2337: break;
2338:
2339: case Horde_Imap_Client::FETCH_FLAGS:
2340: /* QRESYNC would have already done syncing on mailbox
2341: * open, so no need to do again. Only can cache if MODSEQ
2342: * is available in the mailbox. */
2343: if (!$qresync && !empty($status_res['highestmodseq'])) {
2344: /* Grab all flags updated since the cached modseq
2345: * val. */
2346: $metadata = $this->cache->getMetaData($this->_selected, $status_res['uidvalidity'], array(self::CACHE_MODSEQ));
2347: if (isset($metadata[self::CACHE_MODSEQ]) &&
2348: ($metadata[self::CACHE_MODSEQ] != $status_res['highestmodseq'])) {
2349: $uids = $this->cache->get($this->_selected, array(), array(), $status_res['uidvalidity']);
2350: if (!empty($uids)) {
2351: $flag_query = new Horde_Imap_Client_Fetch_Query();
2352: $flag_query->flags();
2353:
2354: /* Update flags in cache. */
2355: $this->_fetch($flag_query, array(), array(
2356: 'changedsince' => $metadata[self::CACHE_MODSEQ],
2357: 'ids' => $this->getIdsOb($uids)
2358: ));
2359: }
2360: $this->_updateMetaData($this->_selected, array(self::CACHE_MODSEQ => $status_res['highestmodseq']), $status_res['uidvalidity']);
2361: }
2362: }
2363:
2364: $cache_array[$k] = $v;
2365: break;
2366:
2367: case Horde_Imap_Client::FETCH_HEADERS:
2368: $this->_temp['headers_caching'] = array();
2369:
2370: foreach ($query[$k] as $key => $val) {
2371: /* Only cache if directly requested. Iterate through
2372: * requests to ensure at least one can be cached. */
2373: if (!empty($val['cache']) && !empty($val['peek'])) {
2374: $cache_array[$k] = $v;
2375: ksort($val);
2376: $header_cache[$key] = hash('md5', serialize($val));
2377: }
2378: }
2379: break;
2380: }
2381: }
2382: }
2383:
2384: /* Build the default fetch entries. */
2385: if (empty($options['fetch_res'])) {
2386: $fetch_ob = new $this->_fetchDataClass();
2387: $ret = array();
2388:
2389: $res_seq = $this->_getSeqUidLookup($options['ids']);
2390: $ids = $options['ids']->sequence
2391: ? array_keys($res_seq['lookup'])
2392: : $res_seq['uids'];
2393:
2394: foreach ($ids as $val) {
2395: $ret[$val] = clone $fetch_ob;
2396: }
2397: } else {
2398: $ret = &$options['fetch_res'];
2399: }
2400:
2401: /* If nothing is cacheable, we can do a straight search. */
2402: if (empty($cache_array)) {
2403: $ret = $this->_fetch($query, $ret, $options);
2404: foreach ($ret as $key => $val) {
2405: if ($val->isDefault()) {
2406: unset($ret[$key]);
2407: }
2408: }
2409: return $ret;
2410: }
2411:
2412: /* If doing a changedsince/vanished search, limit the UIDs now. */
2413: if (!empty($options['changedsince'])) {
2414: $changed_query = new Horde_Imap_Client_Fetch_Query();
2415: if (empty($options['vanished']) && $options['ids']->sequence) {
2416: $changed_query->seq();
2417: } else {
2418: $changed_query->uid();
2419: }
2420:
2421: $cs_res = $this->_fetch($changed_query, array(), array(
2422: 'changedsince' => $options['changedsince'],
2423: 'ids' => $options['ids'],
2424: 'vanished' => !empty($options['vanished'])
2425: ));
2426:
2427: if (!empty($options['vanished'])) {
2428: return $cs_res;
2429: }
2430:
2431: $ret = array_intersect_key($ret, $cs_res);
2432: if (empty($ret)) {
2433: return $ret;
2434: }
2435:
2436: $options['ids'] = $this->getIdsOb(array_keys($ret), $options['ids']->sequence);
2437: }
2438:
2439: /* Need Seq -> UID lookup if we haven't already grabbed it. */
2440: if (is_null($res_seq)) {
2441: $res_seq = $this->_getSeqUidLookup($options['ids']);
2442: }
2443:
2444: /* Get the cached values. */
2445: $data = $this->cache->get($this->_selected, $res_seq['uids']->ids, array_values($cache_array), $status_res['uidvalidity']);
2446:
2447: /* Build a list of what we still need. */
2448: foreach (array_keys($ret) as $val) {
2449: $crit = clone $query;
2450:
2451: if ($options['ids']->sequence) {
2452: $uid = $res_seq['lookup'][$val];
2453: unset($crit[Horde_Imap_Client::FETCH_SEQ]);
2454: } else {
2455: $uid = $val;
2456: }
2457:
2458: /* UID will be added into the results object below. */
2459: unset($crit[Horde_Imap_Client::FETCH_UID]);
2460:
2461: foreach ($cache_array as $key => $cid) {
2462: switch ($key) {
2463: case Horde_Imap_Client::FETCH_ENVELOPE:
2464: if (isset($data[$uid][$cid]) &&
2465: ($data[$uid][$cid] instanceof Horde_Imap_Client_Data_Envelope)) {
2466: $ret[$val]->setEnvelope($data[$uid][$cid]);
2467: unset($crit[$key]);
2468: }
2469: break;
2470:
2471: case Horde_Imap_Client::FETCH_FLAGS:
2472: if (isset($data[$uid][$cid]) &&
2473: is_array($data[$uid][$cid])) {
2474: $ret[$val]->setFlags($data[$uid][$cid]);
2475: unset($crit[$key]);
2476: }
2477: break;
2478:
2479: case Horde_Imap_Client::FETCH_HEADERS:
2480: /* HEADERS caching. */
2481: foreach ($header_cache as $hkey => $hval) {
2482: if (isset($data[$uid][$cid][$hval])) {
2483: /* We have found a cached entry with the same MD5
2484: * sum. */
2485: $ret[$val]->setHeaders($hkey, $data[$uid][$cid][$hval]);
2486: $crit->remove($key, $hkey);
2487: } else {
2488: $this->_temp['headers_caching'][$hkey] = $hval;
2489: }
2490: }
2491: break;
2492:
2493: case Horde_Imap_Client::FETCH_IMAPDATE:
2494: if (isset($data[$uid][$cid]) &&
2495: ($data[$uid][$cid] instanceof Horde_Imap_Client_DateTime)) {
2496: $ret[$val]->setImapDate($data[$uid][$cid]);
2497: unset($crit[$key]);
2498: }
2499: break;
2500:
2501: case Horde_Imap_Client::FETCH_SIZE:
2502: if (isset($data[$uid][$cid])) {
2503: $ret[$val]->setSize($data[$uid][$cid]);
2504: unset($crit[$key]);
2505: }
2506: break;
2507:
2508: case Horde_Imap_Client::FETCH_STRUCTURE:
2509: if (isset($data[$uid][$cid]) &&
2510: ($data[$uid][$cid] instanceof Horde_Mime_Part)) {
2511: $ret[$val]->setStructure($data[$uid][$cid]);
2512: unset($crit[$key]);
2513: }
2514: break;
2515: }
2516: }
2517:
2518: if (count($crit)) {
2519: $sig = $crit->hash();
2520: if (isset($new_query[$sig])) {
2521: $new_query[$sig]['i']->add($val);
2522: } else {
2523: $new_query[$sig] = array(
2524: 'c' => $crit,
2525: 'i' => $this->getIdsOb($val, $options['ids']->sequence)
2526: );
2527: }
2528: }
2529: }
2530:
2531: foreach ($new_query as $val) {
2532: $ret = $this->_fetch($val['c'], $ret, array_merge($options, array(
2533: 'ids' => $val['i']
2534: )));
2535: }
2536:
2537: foreach ($ret as $key => $val) {
2538: if ($val->isDefault() && !empty($new_query)) {
2539: /* If $new_query is empty, this means that the fetch requested
2540: * was for UIDs only. Need to add that info below. */
2541: unset($ret[$key]);
2542: } elseif ($options['ids']->sequence) {
2543: $ret[$key]->setSeq($key);
2544: $ret[$key]->setUid($res_seq['lookup'][$key]);
2545: } else {
2546: $ret[$key]->setUid($key);
2547: }
2548: }
2549:
2550: return $ret;
2551: }
2552:
2553: /**
2554: * Fetch message data.
2555: *
2556: * @param Horde_Imap_Client_Fetch_Query $query Fetch query object.
2557: * @param array $results Partial results.
2558: * @param array $options Additional options.
2559: *
2560: * @return array An array of fetch results. The array consists of
2561: * keys that correspond to 'ids', and values that
2562: * contain Horde_Imap_Query_Data_Fetch objects.
2563: *
2564: * @throws Horde_Imap_Client_Exception
2565: */
2566: abstract protected function _fetch($query, $results, $options);
2567:
2568: /**
2569: * Store message flag data (see RFC 3501 [6.4.6]).
2570: *
2571: * @param mixed $mailbox The mailbox containing the messages to modify.
2572: * Either a Horde_Imap_Client_Mailbox object (as of
2573: * 1.2.0) or a string (UTF-8).
2574: * @param array $options Additional options:
2575: * - add: (array) An array of flags to add.
2576: * DEFAULT: No flags added.
2577: * - ids: (Horde_Imap_Client_Ids) The list of messages to modify.
2578: * DEFAULT: All messages in $mailbox will be modified.
2579: * - remove: (array) An array of flags to remove.
2580: * DEFAULT: No flags removed.
2581: * - replace: (array) Replace the current flags with this set
2582: * of flags. Overrides both the 'add' and 'remove' options.
2583: * DEFAULT: No replace is performed.
2584: * - unchangedsince: (integer) Only changes flags if the mod-sequence ID
2585: * of the message is equal or less than this value.
2586: * Requires the CONDSTORE IMAP extension on the server.
2587: * Also requires the mailbox to support mod-sequences.
2588: * Will throw an exception if either condition is not
2589: * met.
2590: * DEFAULT: mod-sequence is ignored when applying
2591: * changes
2592: *
2593: * @return Horde_Imap_Client_Ids A Horde_Imap_Client_Ids object
2594: * containing the list of IDs that failed
2595: * the 'unchangedsince' test.
2596: *
2597: * @throws Horde_Imap_Client_Exception
2598: */
2599: public function store($mailbox, $options = array())
2600: {
2601: // Open mailbox call will handle the login.
2602: $this->openMailbox($mailbox, Horde_Imap_Client::OPEN_READWRITE);
2603:
2604: /* SEARCHRES requires server support. */
2605: if (empty($options['ids'])) {
2606: $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2607: } elseif ($options['ids']->isEmpty()) {
2608: return $this->getIdsOb();
2609: } elseif ($options['ids']->search_res &&
2610: !$this->queryCapability('SEARCHRES')) {
2611: $this->_exception('Server does not support saved searches.', 'NO_SUPPORT');
2612: }
2613:
2614: if (!empty($options['unchangedsince']) &&
2615: !isset($this->_init['enabled']['CONDSTORE'])) {
2616: $this->_exception('Server does not support the CONDSTORE extension.', 'NO_SUPPORT');
2617: }
2618:
2619: return $this->_store($options);
2620: }
2621:
2622: /**
2623: * Store message flag data.
2624: *
2625: * @param array $options Additional options.
2626: *
2627: * @return Horde_Imap_Client_Ids A Horde_Imap_Client_Ids object
2628: * containing the list of IDs that failed
2629: * the 'unchangedsince' test.
2630: *
2631: * @throws Horde_Imap_Client_Exception
2632: */
2633: abstract protected function _store($options);
2634:
2635: /**
2636: * Copy messages to another mailbox.
2637: *
2638: * @param mixed $source The source mailbox. Either a
2639: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
2640: * or a string (UTF-8).
2641: * @param mixed $dest The destination mailbox. Either a
2642: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
2643: * or a string (UTF-8).
2644: * @param array $options Additional options:
2645: * - create: (boolean) Try to create $dest if it does not exist?
2646: * DEFAULT: No.
2647: * - ids: (Horde_Imap_Client_Ids) The list of messages to copy.
2648: * DEFAULT: All messages in $mailbox will be copied.
2649: * - move: (boolean) If true, delete the original messages.
2650: * DEFAULT: Original messages are not deleted.
2651: *
2652: * @return mixed An array mapping old UIDs (keys) to new UIDs (values) on
2653: * success (if the IMAP server and/or driver support the
2654: * UIDPLUS extension) or true.
2655: *
2656: * @throws Horde_Imap_Client_Exception
2657: */
2658: public function copy($source, $dest, $options = array())
2659: {
2660: // Open mailbox call will handle the login.
2661: $this->openMailbox($source, empty($options['move']) ? Horde_Imap_Client::OPEN_AUTO : Horde_Imap_Client::OPEN_READWRITE);
2662:
2663: /* SEARCHRES requires server support. */
2664: if (empty($options['ids'])) {
2665: $options['ids'] = $this->getIdsOb(Horde_Imap_Client_Ids::ALL);
2666: } elseif ($options['ids']->isEmpty()) {
2667: return array();
2668: } elseif ($options['ids']->search_res &&
2669: !$this->queryCapability('SEARCHRES')) {
2670: $this->_exception('Server does not support saved searches.', 'NO_SUPPORT');
2671: }
2672:
2673: return $this->_copy(Horde_Imap_Client_Mailbox::get($dest, null), $options);
2674: }
2675:
2676: /**
2677: * Copy messages to another mailbox.
2678: *
2679: * @param Horde_Imap_Client_Mailbox $dest The destination mailbox.
2680: * @param array $options Additional options.
2681: *
2682: * @return mixed An array mapping old UIDs (keys) to new UIDs (values) on
2683: * success (if the IMAP server and/or driver support the
2684: * UIDPLUS extension) or true.
2685: *
2686: * @throws Horde_Imap_Client_Exception
2687: */
2688: abstract protected function _copy(Horde_Imap_Client_Mailbox $dest,
2689: $options);
2690:
2691: /**
2692: * Set quota limits. The server must support the IMAP QUOTA extension
2693: * (RFC 2087).
2694: *
2695: * @param mixed $root The quota root. Either a
2696: * Horde_Imap_Client_Mailbox object (as of 1.2.0)
2697: * or a string (UTF-8).
2698: * @param array $resources The resource values to set. Keys are the
2699: * resource atom name; value is the resource
2700: * value.
2701: *
2702: * @throws Horde_Imap_Client_Exception
2703: */
2704: public function setQuota($root, array $resources = array())
2705: {
2706: $this->login();
2707:
2708: if (!$this->queryCapability('QUOTA')) {
2709: $this->_exception('Server does not support the QUOTA extension.', 'NO_SUPPORT');
2710: }
2711:
2712: if (!empty($resources)) {
2713: $this->_setQuota(Horde_Imap_Client_Mailbox::get($root, null), $resources);
2714: }
2715: }
2716:
2717: /**
2718: * Set quota limits.
2719: *
2720: * @param Horde_Imap_Client_Mailbox $root The quota root.
2721: * @param array $resources The resource values to set.
2722: *
2723: * @return boolean True on success.
2724: *
2725: * @throws Horde_Imap_Client_Exception
2726: */
2727: abstract protected function _setQuota(Horde_Imap_Client_Mailbox $root,
2728: $resources);
2729:
2730: /**
2731: * Get quota limits. The server must support the IMAP QUOTA extension
2732: * (RFC 2087).
2733: *
2734: * @param mixed $root The quota root. Either a Horde_Imap_Client_Mailbox
2735: * object (as of 1.2.0) or a string (UTF-8).
2736: *
2737: * @return mixed An array with resource keys. Each key holds an array
2738: * with 2 values: 'limit' and 'usage'.
2739: *
2740: * @throws Horde_Imap_Client_Exception
2741: */
2742: public function getQuota($root)
2743: {
2744: $this->login();
2745:
2746: if (!$this->queryCapability('QUOTA')) {
2747: $this->_exception('Server does not support the QUOTA extension.', 'NO_SUPPORT');
2748: }
2749:
2750: return $this->_getQuota(Horde_Imap_Client_Mailbox::get($root, null));
2751: }
2752:
2753: /**
2754: * Get quota limits.
2755: *
2756: * @param Horde_Imap_Client_Mailbox $root The quota root.
2757: *
2758: * @return mixed An array with resource keys. Each key holds an array
2759: * with 2 values: 'limit' and 'usage'.
2760: *
2761: * @throws Horde_Imap_Client_Exception
2762: */
2763: abstract protected function _getQuota(Horde_Imap_Client_Mailbox $root);
2764:
2765: /**
2766: * Get quota limits for a mailbox. The server must support the IMAP QUOTA
2767: * extension (RFC 2087).
2768: *
2769: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
2770: * object (as of 1.2.0) or a string (UTF-8).
2771: *
2772: * @return mixed An array with the keys being the quota roots. Each key
2773: * holds an array with resource keys: each of these keys
2774: * holds an array with 2 values: 'limit' and 'usage'.
2775: *
2776: * @throws Horde_Imap_Client_Exception
2777: */
2778: public function getQuotaRoot($mailbox)
2779: {
2780: $this->login();
2781:
2782: if (!$this->queryCapability('QUOTA')) {
2783: $this->_exception('Server does not support the QUOTA extension.', 'NO_SUPPORT');
2784: }
2785:
2786: return $this->_getQuotaRoot(Horde_Imap_Client_Mailbox::get($mailbox, null));
2787: }
2788:
2789: /**
2790: * Get quota limits for a mailbox.
2791: *
2792: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
2793: *
2794: * @return mixed An array with the keys being the quota roots. Each key
2795: * holds an array with resource keys: each of these keys
2796: * holds an array with 2 values: 'limit' and 'usage'.
2797: *
2798: * @throws Horde_Imap_Client_Exception
2799: */
2800: abstract protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox);
2801:
2802: /**
2803: * Get the ACL rights for a given mailbox. The server must support the
2804: * IMAP ACL extension (RFC 2086/4314).
2805: *
2806: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
2807: * object (as of 1.2.0) or a string (UTF-8).
2808: *
2809: * @return array An array with identifiers as the keys and
2810: * Horde_Imap_Client_Data_Acl objects as the values.
2811: *
2812: * @throws Horde_Imap_Client_Exception
2813: */
2814: public function getACL($mailbox)
2815: {
2816: $this->login();
2817: return $this->_getACL(Horde_Imap_Client_Mailbox::get($mailbox, null));
2818: }
2819:
2820: /**
2821: * Get ACL rights for a given mailbox.
2822: *
2823: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
2824: *
2825: * @return array An array with identifiers as the keys and
2826: * Horde_Imap_Client_Data_Acl objects as the values.
2827: *
2828: * @throws Horde_Imap_Client_Exception
2829: */
2830: abstract protected function _getACL(Horde_Imap_Client_Mailbox $mailbox);
2831:
2832: /**
2833: * Set ACL rights for a given mailbox/identifier.
2834: *
2835: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
2836: * object (as of 1.2.0) or a string (UTF-8).
2837: * @param string $identifier The identifier to alter (UTF-8).
2838: * @param array $options Additional options:
2839: * - remove: (boolean) If true, removes rights for $identifier.
2840: * DEFAULT: false
2841: * - rights: (string) The rights to alter.
2842: * DEFAULT: If 'remove' is true, removes all rights. If
2843: * 'remove' is false, no rights are altered.
2844: *
2845: * @throws Horde_Imap_Client_Exception
2846: */
2847: public function setACL($mailbox, $identifier, $options)
2848: {
2849: $this->login();
2850:
2851: if (!$this->queryCapability('ACL')) {
2852: $this->_exception('Server does not support the ACL extension.', 'NO_SUPPORT');
2853: }
2854:
2855: if (!empty($options['rights'])) {
2856: $acl = ($options['rights'] instanceof Horde_Imap_Client_Data_Acl)
2857: ? $options['rights']
2858: : new Horde_Imap_Client_Data_Acl(strval($options['rights']));
2859:
2860: $options['rights'] =
2861: (empty($options['remove']) ? '+' : '-') .
2862: $acl->getString($this->queryCapability('RIGHTS') ? Horde_Imap_Client_Data_AclCommon::RFC_4314 : Horde_Imap_Client_Data_AclCommon::RFC_2086);
2863: }
2864:
2865: if (empty($options['rights']) && empty($options['remove'])) {
2866: return;
2867: }
2868:
2869: return $this->_setACL(
2870: Horde_Imap_Client_Mailbox::get($mailbox, null),
2871: Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier, true),
2872: $options
2873: );
2874: }
2875:
2876: /**
2877: * Set ACL rights for a given mailbox/identifier.
2878: *
2879: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
2880: * @param string $identifier The identifier to alter
2881: * (UTF7-IMAP).
2882: * @param array $options Additional options. 'rights'
2883: * contains the string of
2884: * rights to set on the server.
2885: *
2886: * @throws Horde_Imap_Client_Exception
2887: */
2888: abstract protected function _setACL(Horde_Imap_Client_Mailbox $mailbox,
2889: $identifier, $options);
2890:
2891: /**
2892: * List the ACL rights for a given mailbox/identifier. The server must
2893: * support the IMAP ACL extension (RFC 2086/4314).
2894: *
2895: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
2896: * object (as of 1.2.0) or a string (UTF-8).
2897: * @param string $identifier The identifier to query (UTF-8).
2898: *
2899: * @return Horde_Imap_Client_Data_AclRights An ACL data rights object.
2900: *
2901: * @throws Horde_Imap_Client_Exception
2902: */
2903: public function listACLRights($mailbox, $identifier)
2904: {
2905: $this->login();
2906:
2907: if (!$this->queryCapability('ACL')) {
2908: $this->_exception('Server does not support the ACL extension.', 'NO_SUPPORT');
2909: }
2910:
2911: return $this->_listACLRights(
2912: Horde_Imap_Client_Mailbox::get($mailbox, null),
2913: Horde_Imap_Client_Utf7imap::Utf8ToUtf7Imap($identifier, true)
2914: );
2915: }
2916:
2917: /**
2918: * Get ACL rights for a given mailbox/identifier.
2919: *
2920: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
2921: * @param string $identifier The identifier to query
2922: * (UTF7-IMAP).
2923: *
2924: * @return Horde_Imap_Client_Data_AclRights An ACL data rights object.
2925: *
2926: * @throws Horde_Imap_Client_Exception
2927: */
2928: abstract protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox,
2929: $identifier);
2930:
2931: /**
2932: * Get the ACL rights for the current user for a given mailbox. The
2933: * server must support the IMAP ACL extension (RFC 2086/4314).
2934: *
2935: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
2936: * object (as of 1.2.0) or a string (UTF-8).
2937: *
2938: * @return Horde_Imap_Client_Data_Acl An ACL data object.
2939: *
2940: * @throws Horde_Imap_Client_Exception
2941: */
2942: public function getMyACLRights($mailbox)
2943: {
2944: $this->login();
2945:
2946: if (!$this->queryCapability('ACL')) {
2947: $this->_exception('Server does not support the ACL extension.', 'NO_SUPPORT');
2948: }
2949:
2950: return $this->_getMyACLRights(Horde_Imap_Client_Mailbox::get($mailbox, null));
2951: }
2952:
2953: /**
2954: * Get the ACL rights for the current user for a given mailbox.
2955: *
2956: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
2957: *
2958: * @return Horde_Imap_Client_Data_Acl An ACL data object.
2959: *
2960: * @throws Horde_Imap_Client_Exception
2961: */
2962: abstract protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox);
2963:
2964: /**
2965: * Return master list of ACL rights available on the server.
2966: *
2967: * @return array A list of ACL rights.
2968: */
2969: public function allAclRights()
2970: {
2971: $this->login();
2972:
2973: $rights = array(
2974: Horde_Imap_Client::ACL_LOOKUP,
2975: Horde_Imap_Client::ACL_READ,
2976: Horde_Imap_Client::ACL_SEEN,
2977: Horde_Imap_Client::ACL_WRITE,
2978: Horde_Imap_Client::ACL_INSERT,
2979: Horde_Imap_Client::ACL_POST,
2980: Horde_Imap_Client::ACL_ADMINISTER
2981: );
2982:
2983: if ($capability = $this->queryCapability('RIGHTS')) {
2984: // Add rights defined in CAPABILITY string (RFC 4314).
2985: return array_merge($rights, str_split(reset($capability)));
2986: }
2987:
2988: // Add RFC 2086 rights (DEPRECATED)
2989: return array_merge($rights, array(
2990: Horde_Imap_Client::ACL_CREATE,
2991: Horde_Imap_Client::ACL_DELETE
2992: ));
2993: }
2994:
2995: /**
2996: * Get metadata for a given mailbox. The server must support either the
2997: * IMAP METADATA extension (RFC 5464) or the ANNOTATEMORE extension
2998: * (http://ietfreport.isoc.org/idref/draft-daboo-imap-annotatemore/).
2999: *
3000: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
3001: * object (as of 1.2.0) or a string (UTF-8).
3002: * @param array $entries The entries to fetch (UTF-8 strings).
3003: * @param array $options Additional options:
3004: * - depth: (string) Either "0", "1" or "infinity". Returns only the
3005: * given value (0), only values one level below the specified
3006: * value (1) or all entries below the specified value
3007: * (infinity).
3008: * - maxsize: (integer) The maximal size the returned values may have.
3009: * DEFAULT: No maximal size.
3010: *
3011: * @return array An array with metadata names as the keys and metadata
3012: * values as the values. If 'maxsize' is set, and entries
3013: * exist on the server larger than this size, the size will
3014: * be returned in the key '*longentries'.
3015: *
3016: * @throws Horde_Imap_Client_Exception
3017: */
3018: public function getMetadata($mailbox, $entries, $options = array())
3019: {
3020: $this->login();
3021:
3022: if (!is_array($entries)) {
3023: $entries = array($entries);
3024: }
3025:
3026: return $this->_getMetadata(Horde_Imap_Client_Mailbox::get($mailbox, null), array_map(array('Horde_Imap_Client_Utf7imap', 'Utf8ToUtf7Imap'), $entries, array_fill(0, count($entries), null)), $options);
3027: }
3028:
3029: /**
3030: * Get metadata for a given mailbox.
3031: *
3032: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
3033: * @param array $entries The entries to fetch
3034: * (UTF7-IMAP strings).
3035: * @param array $options Additional options.
3036: *
3037: * @return array An array with metadata names as the keys and metadata
3038: * values as the values.
3039: *
3040: * @throws Horde_Imap_Client_Exception
3041: */
3042: abstract protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox,
3043: $entries, $options);
3044:
3045: /**
3046: * Set metadata for a given mailbox/identifier.
3047: *
3048: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
3049: * object (as of 1.2.0) or a string (UTF-8). If
3050: * empty, sets a server annotation.
3051: * @param array $data A set of data values. The metadata values
3052: * corresponding to the keys of the array will
3053: * be set to the values in the array.
3054: *
3055: * @throws Horde_Imap_Client_Exception
3056: */
3057: public function setMetadata($mailbox, $data)
3058: {
3059: $this->login();
3060: $this->_setMetadata(Horde_Imap_Client_Mailbox::get($mailbox, null), $data);
3061: }
3062:
3063: /**
3064: * Set metadata for a given mailbox/identifier.
3065: *
3066: * @param Horde_Imap_Client_Mailbox $mailbox A mailbox.
3067: * @param array $data A set of data values. See
3068: * setMetaData() for format.
3069: *
3070: * @throws Horde_Imap_Client_Exception
3071: */
3072: abstract protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox,
3073: $data);
3074:
3075: /* Public utility functions. */
3076:
3077: /**
3078: * Returns a unique identifier for the current mailbox status.
3079: *
3080: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
3081: * object (as of 1.2.0) or a string (UTF-8).
3082: * @param array $addl Additional cache info to add to the cache ID
3083: * string.
3084: *
3085: * @return string The cache ID string, which will change when the
3086: * composition of the mailbox changes. The uidvalidity
3087: * will always be the first element, and will be delimited
3088: * by the '|' character.
3089: *
3090: * @throws Horde_Imap_Client_Exception
3091: */
3092: public function getCacheId($mailbox, $addl = array())
3093: {
3094: $query = Horde_Imap_Client::STATUS_UIDVALIDITY | Horde_Imap_Client::STATUS_MESSAGES | Horde_Imap_Client::STATUS_UIDNEXT;
3095:
3096: /* Use MODSEQ as cache ID if CONDSTORE extension is available. */
3097: if (isset($this->_init['enabled']['CONDSTORE'])) {
3098: $query |= Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
3099: }
3100:
3101: $status = $this->status($mailbox, $query);
3102:
3103: if (!empty($status['highestmodseq'])) {
3104: $parts = array(
3105: 'V' . $status['uidvalidity'],
3106: 'H' . $status['highestmodseq']
3107: );
3108: } else {
3109: if (empty($status['uidnext'])) {
3110: /* UIDNEXT is not strictly required on mailbox open. If it is
3111: * not available, use the last UID + 1 in the mailbox instead
3112: * (or 0 if mailbox is empty). */
3113: if (empty($status['messages'])) {
3114: $status['uidnext'] = 0;
3115: } else {
3116: $this->_temp['nocache'] = true;
3117: $search_res = $this->_getSeqUidLookup($this->getIdsOb($status['messages'], true));
3118: unset($this->_temp['nocache']);
3119: $uids = $search_res['uids']->ids;
3120: $status['uidnext'] = intval(end($uids)) + 1;
3121: }
3122: }
3123:
3124: $parts = array(
3125: 'V' . $status['uidvalidity'],
3126: 'U' . $status['uidnext'],
3127: 'M' . $status['messages']
3128: );
3129: }
3130:
3131: return implode('|', array_merge($parts, $addl));
3132: }
3133:
3134: /**
3135: * Parses a cacheID created by getCacheId().
3136: *
3137: * @param string $id The cache ID.
3138: *
3139: * @return array An array with the following information:
3140: * - highestmodseq: (integer)
3141: * - messages: (integer)
3142: * - uidnext: (integer)
3143: * - uidvalidity: (integer) Always present
3144: */
3145: public function parseCacheId($id)
3146: {
3147: $data = array(
3148: 'H' => 'highestmodseq',
3149: 'M' => 'messages',
3150: 'U' => 'uidnext',
3151: 'V' => 'uidvalidity'
3152: );
3153: $info = array();
3154:
3155: foreach (explode('|', $id) as $part) {
3156: if (isset($data[$part[0]])) {
3157: $info[$data[$part[0]]] = intval(substr($part, 1));
3158: }
3159: }
3160:
3161: return $info;
3162: }
3163:
3164: /**
3165: * Parses a client command array to create a server command string.
3166: *
3167: * @deprecated
3168: * @see Horde_Imap_Client_Utils#parseCommandArray()
3169: */
3170: public function parseCommandArray($query, $callback = null, $out = '')
3171: {
3172: return $this->parseCommandArray($query, $callback, $out);
3173: }
3174:
3175: /**
3176: * Given an IMAP body section string, fetches the corresponding part.
3177: *
3178: * @param mixed $mailbox A mailbox. Either a Horde_Imap_Client_Mailbox
3179: * object (as of 1.2.0) or a string (UTF-8).
3180: * @param integer $uid The IMAP UID.
3181: * @param string $section The IMAP section string.
3182: *
3183: * @return resource The section contents in a stream. Returns null if
3184: * the part could not be found.
3185: *
3186: * @throws Horde_Imap_Client_Exception
3187: */
3188: public function fetchFromSectionString($mailbox, $uid, $section = null)
3189: {
3190: $ids_ob = $this->getIdsOb($uid);
3191: $section = trim($section);
3192:
3193: // BODY[]
3194: if (!strlen($section)) {
3195: $query = new Horde_Imap_Client_Fetch_Query();
3196: $query->fullText(array(
3197: 'peek' => true
3198: ));
3199:
3200: $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3201: return $fetch[$uid]->getFullMsg(true);
3202: }
3203:
3204: // BODY[<#.>HEADER.FIELDS<.NOT>()]
3205: if (($pos = stripos($section, 'HEADER.FIELDS')) !== false) {
3206: $hdr_pos = strpos($section, '(');
3207: $cmd = substr($section, 0, $hdr_pos);
3208:
3209: $query = new Horde_Imap_Client_Fetch_Query();
3210: $query->headers(
3211: 'section',
3212: explode(' ', substr($section, $hdr_pos + 1, strrpos($section, ')') - $hdr_pos)),
3213: array(
3214: 'id' => ($pos ? substr($section, 0, $pos - 1) : 0),
3215: 'notsearch' => (stripos($cmd, '.NOT') !== false),
3216: 'peek' => true
3217: )
3218: );
3219:
3220: $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3221: return $fetch[$uid]->getHeaders('section', Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
3222: }
3223:
3224: // BODY[#]
3225: if (is_numeric(substr($section, -1))) {
3226: $query = new Horde_Imap_Client_Fetch_Query();
3227: $query->bodyPart($section, array(
3228: 'peek' => true
3229: ));
3230:
3231: $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3232: return $fetch[$uid]->getBodyPart($section, true);
3233: }
3234:
3235: // BODY[<#.>HEADER]
3236: if (($pos = stripos($section, 'HEADER')) !== false) {
3237: $id = $pos
3238: ? substr($section, 0, $pos - 1)
3239: : 0;
3240:
3241: $query = new Horde_Imap_Client_Fetch_Query();
3242: $query->headerText(array(
3243: 'id' => $id,
3244: 'peek' => true
3245: ));
3246:
3247: $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3248: return $fetch[$uid]->getHeaderText($id, Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
3249: }
3250:
3251: // BODY[<#.>TEXT]
3252: if (($pos = stripos($section, 'TEXT')) !== false) {
3253: $id = $pos
3254: ? substr($section, 0, $pos - 1)
3255: : 0;
3256:
3257: $query = new Horde_Imap_Client_Fetch_Query();
3258: $query->bodyText(array(
3259: 'id' => $id,
3260: 'peek' => true
3261: ));
3262:
3263: $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3264: return $fetch[$uid]->getBodyText($id, true);
3265: }
3266:
3267: // BODY[<#.>MIMEHEADER]
3268: if (($pos = stripos($section, 'MIME')) !== false) {
3269: $id = $pos
3270: ? substr($section, 0, $pos - 1)
3271: : 0;
3272:
3273: $query = new Horde_Imap_Client_Fetch_Query();
3274: $query->mimeHeader($id, array(
3275: 'peek' => true
3276: ));
3277:
3278: $fetch = $this->fetch($mailbox, $query, array('ids' => $ids_ob));
3279: return $fetch[$uid]->getMimeHeader($id, Horde_Imap_Client_Data_Fetch::HEADER_STREAM);
3280: }
3281:
3282: return null;
3283: }
3284:
3285: /**
3286: * Determines if the given charset is valid for search-related queries.
3287: * This check pertains just to the basic IMAP SEARCH command.
3288: *
3289: * @param string $charset The query charset.
3290: *
3291: * @return boolean True if server supports this charset.
3292: */
3293: public function validSearchCharset($charset)
3294: {
3295: $charset = strtoupper($charset);
3296:
3297: if (!isset($this->_init['s_charset'][$charset])) {
3298: $support = null;
3299:
3300: switch ($charset) {
3301: case 'US-ASCII';
3302: /* US-ASCII is always supported (RFC 3501 [6.4.4]). */
3303: $support = true;
3304: break;
3305: }
3306:
3307: /* Use a dummy search query and search for BADCHARSET
3308: * response. */
3309: if (is_null($support)) {
3310: $query = new Horde_Imap_Client_Search_Query();
3311: $query->charset($charset);
3312: $query->ids($this->getIdsOb(1, true));
3313: $query->text('a');
3314: try {
3315: $this->search('INBOX', $query, array(
3316: 'sequence' => true
3317: ));
3318: $support = true;
3319: } catch (Horde_Imap_Client_Exception $e) {
3320: /* BADCHARSET is only a MAY return - but there is no
3321: * other way of determining charset support. */
3322: $support = ($e->getCode() != Horde_Imap_Client_Exception::BADCHARSET);
3323: }
3324: }
3325:
3326: $s_charset = $this->_init['s_charset'];
3327: $s_charset[$charset] = $support;
3328: $this->_setInit('s_charset', $s_charset);
3329: }
3330:
3331: return $this->_init['s_charset'][$charset];
3332: }
3333:
3334: /**
3335: * Output debug information.
3336: *
3337: * @param string $msg Debug line.
3338: * @param string $type The message type. One of the following:
3339: * - Horde_Imap_Client::DEBUG_RAW: None (output raw message)
3340: * - Horde_Imap_Client::DEBUG_CLIENT: Client command
3341: * - Horde_Imap_Client::DEBUG_INFO: Informational message
3342: * - Horde_Imap_Client::DEBUG_SERVER: Server command
3343: */
3344: public function writeDebug($msg, $type = Horde_Imap_Client::DEBUG_RAW)
3345: {
3346: if (!$this->_debug) {
3347: return;
3348: }
3349:
3350: $pre = '';
3351:
3352: if ($type) {
3353: $new_time = microtime(true);
3354: if (isset($this->_temp['debug_time'])) {
3355: if (($diff = ($new_time - $this->_temp['debug_time'])) > Horde_Imap_Client::SLOW_COMMAND) {
3356: fwrite($this->_debug, '>> Slow IMAP Command: ' . round($diff, 3) . " seconds\n");
3357: }
3358: } else {
3359: fwrite($this->_debug,
3360: str_repeat('-', 30) . "\n" .
3361: '>> Timestamp: ' . date('r') . "\n"
3362: );
3363: }
3364:
3365: $this->_temp['debug_time'] = $new_time;
3366:
3367: switch ($type) {
3368: case Horde_Imap_Client::DEBUG_CLIENT:
3369: $pre .= 'C: ';
3370: break;
3371:
3372: case Horde_Imap_Client::DEBUG_INFO:
3373: $pre .= '>> ';
3374: break;
3375:
3376: case Horde_Imap_Client::DEBUG_SERVER:
3377: $pre .= 'S: ';
3378: break;
3379: }
3380: }
3381:
3382: fwrite($this->_debug, $pre . $msg);
3383: }
3384:
3385: /* Private utility functions. */
3386:
3387: /**
3388: * Returns UIDs for an ALL search, or for a sequence number -> UID lookup.
3389: *
3390: * @param Horde_Imap_Client_Ids $ids The IDs to lookup.
3391: * @param boolean $reverse Perform reverse lookup (UID ->
3392: * Sequence number) if needed.
3393: *
3394: * @return array An array with 2 possible entries:
3395: * - lookup: (array) The mapping of sequence numbers [keys] to UIDs
3396: * [values]. Calculated if $reverse is true or $ids are
3397: * sequence numbers.
3398: * - uids: (Horde_Imap_Client_Ids) The list of UIDs.
3399: */
3400: protected function _getSeqUidLookup(Horde_Imap_Client_Ids $ids,
3401: $reverse = false)
3402: {
3403: $ret = array('lookup' => array());
3404:
3405: if (count($ids) && !$ids->sequence && !$reverse) {
3406: $ret['uids'] = $ids;
3407: return $ret;
3408: }
3409:
3410: if ($ids->all || $ids->search_res) {
3411: $search = null;
3412: } else {
3413: $search = new Horde_Imap_Client_Search_Query();
3414: $search->ids($ids);
3415: }
3416:
3417: $res = $this->search($this->_selected, $search, array(
3418: 'sequence' => !$ids->all && !$ids->sequence,
3419: 'sort' => array(Horde_Imap_Client::SORT_SEQUENCE)
3420: ));
3421:
3422: if ($res['count']) {
3423: $ret['uids'] = ($ids->all || $ids->sequence)
3424: ? $res['match']
3425: : $ids;
3426:
3427: if ($ids->all) {
3428: $seq = range(1, $res['count']);
3429: } else {
3430: $seq = $ids->sequence
3431: ? $ids->ids
3432: : $res['match']->ids;
3433: sort($seq, SORT_NUMERIC);
3434: }
3435: $ret['lookup'] = array_combine($seq, $ret['uids']->ids);
3436: }
3437:
3438: return $ret;
3439: }
3440:
3441: /**
3442: * Store FETCH data in cache.
3443: *
3444: * @param array $data The data array.
3445: * @param array $options Additional options:
3446: * - fields: (array) Only update these cache fields.
3447: * DEFAULT: Update all cache fields.
3448: * - mailbox: (Horde_Imap_Client_Mailbox) The mailbox to update.
3449: * DEFAULT: The selected mailbox.
3450: * - seq: (boolean) Is data stored with sequence numbers?
3451: * DEFAULT: Data stored with UIDs.
3452: * - uidvalid: (integer) The UID Validity number.
3453: * DEFAULT: UIDVALIDITY discovered via a status() call.
3454: *
3455: * @throws Horde_Imap_Client_Exception
3456: */
3457: protected function _updateCache($data, array $options = array())
3458: {
3459: $mailbox = empty($options['mailbox'])
3460: ? $this->_selected
3461: : $options['mailbox'];
3462:
3463: if (!$this->_initCache(empty($options['mailbox']))) {
3464: return;
3465: }
3466:
3467: if (in_array(strval($mailbox), $this->_params['cache']['fetch_ignore'])) {
3468: $this->writeDebug(sprintf("IGNORING cached FETCH data (mailbox: %s)\n", $mailbox), Horde_Imap_Client::DEBUG_INFO);
3469: return;
3470: }
3471:
3472: $seq_res = empty($options['seq'])
3473: ? null
3474: : $this->_getSeqUidLookup($this->getIdsOb(array_keys($data), true));
3475:
3476: $cf = empty($options['fields'])
3477: ? $this->_params['cache']['fields']
3478: : array_intersect_key($this->_params['cache']['fields'], array_flip($options['fields']));
3479: $tocache = array();
3480:
3481: $status_flags = 0;
3482: if (isset($this->_init['enabled']['CONDSTORE'])) {
3483: $status_flags |= Horde_Imap_Client::STATUS_HIGHESTMODSEQ;
3484: }
3485: if (empty($options['uidvalid'])) {
3486: $status_flags |= Horde_Imap_Client::STATUS_UIDVALIDITY;
3487: }
3488:
3489: $status_res = $this->status($mailbox, $status_flags);
3490:
3491: $highestmodseq = empty($status_res['highestmodseq'])
3492: ? null
3493: : array($status_res['highestmodseq']);
3494: $uidvalid = isset($status_res['uidvalidity'])
3495: ? $status_res['uidvalidity']
3496: : $options['uidvalid'];
3497:
3498: reset($data);
3499: while (list($k, $v) = each($data)) {
3500: $tmp = array();
3501:
3502: foreach ($cf as $key => $val) {
3503: if ($v->exists($key)) {
3504: switch ($key) {
3505: case Horde_Imap_Client::FETCH_ENVELOPE:
3506: $tmp[$val] = $v->getEnvelope();
3507: break;
3508:
3509: case Horde_Imap_Client::FETCH_FLAGS:
3510: /* A FLAGS FETCH can only occur if we are in the
3511: * mailbox. So either HIGHESTMODSEQ has already been
3512: * updated or the flag FETCHs will provide the new
3513: * HIGHESTMODSEQ value. In either case, we are
3514: * guaranteed that all cache information is correctly
3515: * updated (in the former case, we reached here via
3516: * a 'changedsince' FETCH and in the latter case, we
3517: * are in EXAMINE/SELECT mode and will catch all flag
3518: * changes).
3519: * Ignore flag caching if MODSEQs not available. */
3520: if ($highestmodseq) {
3521: if ($modseq = $v->getModSeq()) {
3522: $highestmodseq[] = $modseq;
3523: }
3524: $tmp[$val] = $v->getFlags();
3525: }
3526: break;
3527:
3528: case Horde_Imap_Client::FETCH_HEADERS:
3529: foreach ($this->_temp['headers_caching'] as $label => $hash) {
3530: if ($hdr = $v->getHeaders($label)) {
3531: $tmp[$val][$hash] = $hdr;
3532: }
3533: }
3534: break;
3535:
3536: case Horde_Imap_Client::FETCH_IMAPDATE:
3537: $tmp[$val] = $v->getImapDate();
3538: break;
3539:
3540: case Horde_Imap_Client::FETCH_SIZE:
3541: $tmp[$val] = $v->getSize();
3542: break;
3543:
3544: case Horde_Imap_Client::FETCH_STRUCTURE:
3545: $tmp[$val] = clone $v->getStructure();
3546: break;
3547: }
3548: }
3549: }
3550:
3551: if (!empty($tmp)) {
3552: $tocache[is_null($seq_res) ? $k : $seq_res['lookup'][$k]] = $tmp;
3553: }
3554: }
3555:
3556: $this->cache->set($mailbox, $tocache, $uidvalid);
3557:
3558: if (!empty($highestmodseq)) {
3559: $modseq = max($highestmodseq);
3560: $metadata = $this->cache->getMetaData($mailbox, $uidvalid, array(self::CACHE_MODSEQ));
3561: if (!isset($metadata[self::CACHE_MODSEQ]) ||
3562: ($metadata[self::CACHE_MODSEQ] != $modseq)) {
3563: $this->_temp['lastmodseq'][strval($mailbox)] = isset($metadata[self::CACHE_MODSEQ])
3564: ? $metadata[self::CACHE_MODSEQ]
3565: : 0;
3566: if (count($tocache)) {
3567: $this->_temp['lastmodsequids'][strval($mailbox)] = $this->utils->toSequenceString(array_keys($tocache), array('nosort' => true));
3568: }
3569: $this->_updateMetaData($mailbox, array(self::CACHE_MODSEQ => $modseq), $uidvalid);
3570: }
3571: }
3572: }
3573:
3574: /**
3575: * Moves cache entries from one mailbox to another.
3576: *
3577: * @param string $from The source mailbox (UTF7-IMAP).
3578: * @param string $to The destination mailbox (UTF7-IMAP).
3579: * @param array $map Mapping of source UIDs (keys) to destination
3580: * UIDs (values).
3581: * @param string $uidvalid UIDVALIDITY of destination mailbox.
3582: *
3583: * @throws Horde_Imap_Client_Exception
3584: */
3585: protected function _moveCache($from, $to, $map, $uidvalid = null)
3586: {
3587: if (!$this->_initCache()) {
3588: return;
3589: }
3590:
3591: if (in_array($to, $this->_params['cache']['fetch_ignore'])) {
3592: $this->writeDebug(sprintf("IGNORING moving cached FETCH data (%s => %s)\n", $from, $to), Horde_Imap_Client::DEBUG_INFO);
3593: return;
3594: }
3595:
3596: if (is_null($uidvalid)) {
3597: $status_res = $this->status($to, Horde_Imap_Client::STATUS_UIDVALIDITY);
3598: $uidvalid = $status_res['uidvalidity'];
3599: }
3600:
3601: $old_data = $this->cache->get($from, array_keys($map), null);
3602: $new_data = array();
3603:
3604: foreach ($map as $key => $val) {
3605: if (!empty($old_data[$key])) {
3606: $new_data[$val] = $old_data[$key];
3607: }
3608: }
3609:
3610: if (!empty($new_data)) {
3611: $this->cache->set($to, $new_data, $uidvalid);
3612: }
3613: }
3614:
3615: /**
3616: * Delete messages in the cache.
3617: *
3618: * @param string $mailbox An IMAP mailbox string.
3619: * @param array $uids The list of message UIDs to delete.
3620: *
3621: * @throws Horde_Imap_Client_Exception
3622: */
3623: protected function _deleteMsgs($mailbox, $uids)
3624: {
3625: $this->cache->deleteMsgs($mailbox, $uids);
3626: }
3627:
3628: /**
3629: * Retrieve data from the search cache.
3630: *
3631: * @param string $type The cache type ('search' or 'thread').
3632: * @param string $mailbox The mailbox to update.
3633: * @param array $options The options array of the calling function.
3634: *
3635: * @return mixed If retrieved, array is returned with data in key 'data'
3636: * and the search ID in key 'id'.
3637: * If caching is available, returns cacheid string.
3638: * Returns null if caching is not available.
3639: */
3640: protected function _getSearchCache($type, $mailbox, $options)
3641: {
3642: ksort($options);
3643: $cache = hash('md5', $type . serialize($options));
3644:
3645: $search_id = $mailbox . $cache;
3646: $status = $this->status($mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
3647: $metadata = $this->cache->getMetaData($mailbox, $status['uidvalidity'], array(self::CACHE_SEARCH));
3648:
3649: $cacheid = $this->getCacheId($mailbox);
3650: if (isset($metadata[self::CACHE_SEARCH]['cacheid']) &&
3651: ($metadata[self::CACHE_SEARCH]['cacheid'] != $cacheid)) {
3652: $metadata[self::CACHE_SEARCH] = array();
3653: if ($this->_debug &&
3654: !isset($this->_temp['searchcacheexpire'][strval($mailbox)])) {
3655: $this->writeDebug(sprintf("Expired search results from cache (mailbox: %s)\n", $mailbox), Horde_Imap_Client::DEBUG_INFO);
3656: $this->_temp['searchcacheexpire'][strval($mailbox)] = true;
3657: }
3658: } elseif (isset($metadata[self::CACHE_SEARCH][$cache])) {
3659: $this->writeDebug(sprintf("Retrieved %s results from cache (mailbox: %s; id: %s)\n", $type, $mailbox, $cache), Horde_Imap_Client::DEBUG_INFO);
3660:
3661: return array(
3662: 'data' => unserialize($metadata[self::CACHE_SEARCH][$cache]),
3663: 'id' => $search_id
3664: );
3665: }
3666:
3667: $metadata[self::CACHE_SEARCH]['cacheid'] = $cacheid;
3668:
3669: $this->_temp['searchcache'][$search_id] = array(
3670: 'id' => $cache,
3671: 'mailbox' => $mailbox,
3672: 'metadata' => $metadata,
3673: 'type' => $type
3674: );
3675:
3676: return $search_id;
3677: }
3678:
3679: /**
3680: * Set data in the search cache.
3681: *
3682: * @param mixed $data The cache data to store.
3683: * @param string $sid The search ID returned from _getSearchCache().
3684: */
3685: protected function _setSearchCache($data, $sid)
3686: {
3687: $cache = &$this->_temp['searchcache'][$sid];
3688: $cache['metadata'][self::CACHE_SEARCH][$cache['id']] = serialize($data);
3689:
3690: $this->_updateMetaData($cache['mailbox'], $cache['metadata']);
3691:
3692: if ($this->_debug) {
3693: $this->writeDebug(sprintf("Saved %s results to cache (mailbox: %s; id: %s)\n", $cache['type'], $cache['mailbox'], $cache['id']), Horde_Imap_Client::DEBUG_INFO);
3694: unset($this->_temp['searchcacheexpire'][strval($cache['mailbox'])]);
3695: }
3696: }
3697:
3698: /**
3699: * Updates metadata for a mailbox.
3700: *
3701: * @param string $mailbox Mailbox to update.
3702: * @param string $data The data to update.
3703: * @param integer $uidvalid The uidvalidity of the mailbox. If not set,
3704: * do a status call to grab it.
3705: */
3706: protected function _updateMetaData($mailbox, $data, $uidvalid = null)
3707: {
3708: if (is_null($uidvalid)) {
3709: $status = $this->status($mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY);
3710: $uidvalid = $status['uidvalidity'];
3711: }
3712: $this->cache->setMetaData($mailbox, $uidvalid, $data);
3713: }
3714:
3715: /**
3716: * Prepares append message data for insertion into the IMAP command
3717: * string.
3718: *
3719: * @param mixed $data Either a resource or a string.
3720: * @param resource $stream The stream to append to. If not given, will
3721: * append to new stream.
3722: *
3723: * @param resource A stream containing the message data.
3724: */
3725: protected function _prepareAppendData($data = null, $stream = null)
3726: {
3727: if (is_null($stream)) {
3728: $stream = fopen('php://temp', 'w+');
3729: stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
3730: stream_filter_append($stream, 'horde_eol', STREAM_FILTER_WRITE);
3731: }
3732:
3733: if (!is_null($data)) {
3734: if (is_resource($data)) {
3735: rewind($data);
3736: stream_copy_to_stream($data, $stream);
3737: } else {
3738: fwrite($stream, $data);
3739: }
3740: }
3741:
3742: return $stream;
3743: }
3744:
3745: /**
3746: * Builds a stream from CATENATE input to append().
3747: *
3748: * @param array $data See append() - array input for the 'data' key to
3749: * the $data parameter.
3750: *
3751: * @return resource The data combined into a single stream.
3752: * @throws Horde_Imap_Client_Exception
3753: */
3754: protected function _buildCatenateData($data)
3755: {
3756: $stream = $this->_prepareAppendData();
3757:
3758: foreach (array_keys($data) as $key2) {
3759: switch ($data[$key2]['t']) {
3760: case 'text':
3761: $this->_prepareAppendData($data[$key2]['v'], $stream);
3762: break;
3763:
3764: case 'url':
3765: $part = $exception = null;
3766: $url = $this->utils->parseUrl($data[$key2]['v']);
3767:
3768: if (isset($url['mailbox']) &&
3769: isset($url['uid'])) {
3770: try {
3771: $status_res = isset($url['uidvalidity'])
3772: ? $this->status($url['mailbox'], Horde_Imap_Client::STATUS_UIDVALIDITY)
3773: : null;
3774:
3775: if (is_null($status_res) ||
3776: ($status_res['uidvalidity'] == $url['uidvalidity'])) {
3777: $part = $this->fetchFromSectionString($url['mailbox'], $url['uid'], isset($url['section']) ? $url['section'] : null);
3778: }
3779: } catch (Horde_Imap_Client_Exception $exception) {
3780: }
3781: }
3782:
3783: if (is_null($part)) {
3784: $message = 'Bad IMAP URL given in CATENATE data: ' . json_encode($url);
3785: if ($exception) {
3786: $message .= ' ' . $exception->getMessage();
3787: }
3788: throw new InvalidArgumentException($message);
3789: } else {
3790: $this->_prepareAppendData($part, $stream);
3791: }
3792: break;
3793: }
3794: }
3795:
3796: return $stream;
3797: }
3798:
3799: /**
3800: * Parses human-readable response text for response codes.
3801: *
3802: * @param string $text The response text.
3803: *
3804: * @return object An object with the following properties:
3805: * - code: (string) The response code, if it exists.
3806: * - data: (string) The response code data, if it exists.
3807: * - text: (string) The human-readable response text.
3808: */
3809: protected function _parseResponseText($text)
3810: {
3811: $ret = new stdClass;
3812:
3813: $text = trim($text);
3814: if ($text[0] == '[') {
3815: $pos = strpos($text, ' ', 2);
3816: $end_pos = strpos($text, ']', 2);
3817: if ($pos > $end_pos) {
3818: $ret->code = strtoupper(substr($text, 1, $end_pos - 1));
3819: } else {
3820: $ret->code = strtoupper(substr($text, 1, $pos - 1));
3821: $ret->data = substr($text, $pos + 1, $end_pos - $pos - 1);
3822: }
3823: $ret->text = trim(substr($text, $end_pos + 1));
3824: } else {
3825: $ret->text = $text;
3826: }
3827:
3828: return $ret;
3829: }
3830:
3831: }
3832: