[ Index ] |
PHP Cross Reference of Unnamed Project |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Originally based on code from: 4 * - auth.php (1.49) 5 * - imap_general.php (1.212) 6 * - imap_messages.php (revision 13038) 7 * - strings.php (1.184.2.35) 8 * from the Squirrelmail project. 9 * Copyright (c) 1999-2007 The SquirrelMail Project Team 10 * 11 * Copyright 2005-2014 Horde LLC (http://www.horde.org/) 12 * 13 * See the enclosed file COPYING for license information (LGPL). If you 14 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 15 * 16 * @category Horde 17 * @copyright 1999-2007 The SquirrelMail Project Team 18 * @copyright 2005-2014 Horde LLC 19 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 20 * @package Imap_Client 21 */ 22 23 /** 24 * An interface to an IMAP4rev1 server (RFC 3501) using built-in PHP features. 25 * 26 * Implements the following IMAP-related RFCs (see 27 * http://www.iana.org/assignments/imap4-capabilities): 28 * <pre> 29 * - RFC 2086/4314: ACL 30 * - RFC 2087: QUOTA 31 * - RFC 2088: LITERAL+ 32 * - RFC 2195: AUTH=CRAM-MD5 33 * - RFC 2221: LOGIN-REFERRALS 34 * - RFC 2342: NAMESPACE 35 * - RFC 2595/4616: TLS & AUTH=PLAIN 36 * - RFC 2831: DIGEST-MD5 authentication mechanism (obsoleted by RFC 6331) 37 * - RFC 2971: ID 38 * - RFC 3348: CHILDREN 39 * - RFC 3501: IMAP4rev1 specification 40 * - RFC 3502: MULTIAPPEND 41 * - RFC 3516: BINARY 42 * - RFC 3691: UNSELECT 43 * - RFC 4315: UIDPLUS 44 * - RFC 4422: SASL Authentication (for DIGEST-MD5) 45 * - RFC 4466: Collected extensions (updates RFCs 2088, 3501, 3502, 3516) 46 * - RFC 4469/5550: CATENATE 47 * - RFC 4731: ESEARCH 48 * - RFC 4959: SASL-IR 49 * - RFC 5032: WITHIN 50 * - RFC 5161: ENABLE 51 * - RFC 5182: SEARCHRES 52 * - RFC 5255: LANGUAGE/I18NLEVEL 53 * - RFC 5256: THREAD/SORT 54 * - RFC 5258: LIST-EXTENDED 55 * - RFC 5267: ESORT; PARTIAL search return option 56 * - RFC 5464: METADATA 57 * - RFC 5530: IMAP Response Codes 58 * - RFC 5819: LIST-STATUS 59 * - RFC 5957: SORT=DISPLAY 60 * - RFC 6154: SPECIAL-USE/CREATE-SPECIAL-USE 61 * - RFC 6203: SEARCH=FUZZY 62 * - RFC 6851: MOVE 63 * - RFC 6858: DOWNGRADED response code 64 * - RFC 7162: CONDSTORE/QRESYNC 65 * </pre> 66 * 67 * Implements the following non-RFC extensions: 68 * <pre> 69 * - draft-ietf-morg-inthread-01: THREAD=REFS 70 * - draft-daboo-imap-annotatemore-07: ANNOTATEMORE 71 * - draft-daboo-imap-annotatemore-08: ANNOTATEMORE2 72 * - XIMAPPROXY 73 * Requires imapproxy v1.2.7-rc1 or later 74 * See https://squirrelmail.svn.sourceforge.net/svnroot/squirrelmail/trunk/imap_proxy/README 75 * - AUTH=XOAUTH2 76 * https://developers.google.com/gmail/xoauth2_protocol 77 * </pre> 78 * 79 * TODO (or not necessary?): 80 * <pre> 81 * - RFC 2177: IDLE 82 * Probably not necessary due to the limited connection time of each 83 * HTTP/PHP request 84 * - RFC 2193: MAILBOX-REFERRALS 85 * - RFC 4467/5092/5524/5550/5593: URLAUTH, URLAUTH=BINARY, URL-PARTIAL 86 * - RFC 4978: COMPRESS=DEFLATE 87 * See: http://bugs.php.net/bug.php?id=48725 88 * - RFC 5257: ANNOTATE (Experimental) 89 * - RFC 5259: CONVERT 90 * - RFC 5267: CONTEXT=SEARCH; CONTEXT=SORT 91 * - RFC 5465: NOTIFY 92 * - RFC 5466: FILTERS 93 * - RFC 6237: MULTISEARCH (Experimental) 94 * - RFC 6855: UTF8 95 * </pre> 96 * 97 * @author Michael Slusarz <slusarz@horde.org> 98 * @category Horde 99 * @copyright 1999-2007 The SquirrelMail Project Team 100 * @copyright 2005-2014 Horde LLC 101 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 102 * @package Imap_Client 103 */ 104 class Horde_Imap_Client_Socket extends Horde_Imap_Client_Base 105 { 106 /** 107 * Cache names used exclusively within this class. 108 */ 109 const CACHE_FLAGS = 'HICflags'; 110 111 /** 112 * Queued commands to send to the server. 113 * 114 * @var array 115 */ 116 protected $_cmdQueue = array(); 117 118 /** 119 * The default ports to use for a connection. 120 * 121 * @var array 122 */ 123 protected $_defaultPorts = array(143, 993); 124 125 /** 126 * Mapping of status fields to IMAP names. 127 * 128 * @var array 129 */ 130 protected $_statusFields = array( 131 'messages' => Horde_Imap_Client::STATUS_MESSAGES, 132 'recent' => Horde_Imap_Client::STATUS_RECENT, 133 'uidnext' => Horde_Imap_Client::STATUS_UIDNEXT, 134 'uidvalidity' => Horde_Imap_Client::STATUS_UIDVALIDITY, 135 'unseen' => Horde_Imap_Client::STATUS_UNSEEN, 136 'firstunseen' => Horde_Imap_Client::STATUS_FIRSTUNSEEN, 137 'flags' => Horde_Imap_Client::STATUS_FLAGS, 138 'permflags' => Horde_Imap_Client::STATUS_PERMFLAGS, 139 'uidnotsticky' => Horde_Imap_Client::STATUS_UIDNOTSTICKY, 140 'highestmodseq' => Horde_Imap_Client::STATUS_HIGHESTMODSEQ 141 ); 142 143 /** 144 * The unique tag to use when making an IMAP query. 145 * 146 * @var integer 147 */ 148 protected $_tag = 0; 149 150 /** 151 * @param array $params A hash containing configuration parameters. 152 * Additional parameters to base driver: 153 * - debug_literal: (boolean) If true, will output the raw text of 154 * literal responses to the debug stream. Otherwise, 155 * outputs a summary of the literal response. 156 * - envelope_addrs: (integer) The maximum number of address entries to 157 * read for FETCH ENVELOPE address fields. 158 * DEFAULT: 1000 159 * - envelope_string: (integer) The maximum length of string fields 160 * returned by the FETCH ENVELOPE command. 161 * DEFAULT: 2048 162 * - xoauth2_token: (mixed) If set, will authenticate via the XOAUTH2 163 * mechanism (if available) with this token. Either a 164 * string (since 2.13.0) or a 165 * Horde_Imap_Client_Base_Password object (since 166 * 2.14.0). 167 */ 168 public function __construct(array $params = array()) 169 { 170 parent::__construct(array_merge(array( 171 'debug_literal' => false, 172 'envelope_addrs' => 1000, 173 'envelope_string' => 2048 174 ), $params)); 175 } 176 177 /** 178 */ 179 public function getParam($key) 180 { 181 switch ($key) { 182 case 'xoauth2_token': 183 if (isset($this->_params[$key]) && 184 ($this->_params[$key] instanceof Horde_Imap_Client_Base_Password)) { 185 return $this->_params[$key]->getPassword(); 186 } 187 break; 188 } 189 190 return parent::getParam($key); 191 } 192 193 /** 194 */ 195 protected function _capability() 196 { 197 // Need to use connect call here or else we run into loop issues 198 // because _connect() can call capability() internally. 199 $this->_connect(); 200 201 // It is possible the server provided capability information on 202 // connect, so check for it now. 203 if (!isset($this->_init['capability'])) { 204 $this->_sendCmd($this->_command('CAPABILITY')); 205 } 206 } 207 208 /** 209 * Parse a CAPABILITY Response (RFC 3501 [7.2.1]). 210 * 211 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 212 * object. 213 * @param array $data An array of CAPABILITY strings. 214 */ 215 protected function _parseCapability( 216 Horde_Imap_Client_Interaction_Pipeline $pipeline, 217 $data 218 ) 219 { 220 if (!empty($this->_temp['no_cap'])) { 221 return; 222 } 223 224 $pipeline->data['capability_set'] = true; 225 226 $c = array(); 227 228 foreach ($data as $val) { 229 $cap_list = explode('=', $val); 230 $cap_list[0] = strtoupper($cap_list[0]); 231 if (isset($cap_list[1])) { 232 if (!isset($c[$cap_list[0]]) || !is_array($c[$cap_list[0]])) { 233 $c[$cap_list[0]] = array(); 234 } 235 $c[$cap_list[0]][] = $cap_list[1]; 236 } elseif (!isset($c[$cap_list[0]])) { 237 $c[$cap_list[0]] = true; 238 } 239 } 240 241 $this->_setInit('capability', $c); 242 } 243 244 /** 245 * Unsets a capability. 246 * 247 * @param string $cap Capability to unset. 248 */ 249 protected function _unsetCapability($cap) 250 { 251 $cap_list = $this->capability(); 252 unset($cap_list[$cap]); 253 $this->_setInit('capability', $cap_list); 254 } 255 256 /** 257 */ 258 protected function _noop() 259 { 260 // NOOP doesn't return any specific response 261 $this->_sendCmd($this->_command('NOOP')); 262 } 263 264 /** 265 */ 266 protected function _getNamespaces() 267 { 268 $data = $this->queryCapability('NAMESPACE') 269 ? $this->_sendCmd($this->_command('NAMESPACE'))->data 270 : array(); 271 272 return isset($data['namespace']) 273 ? $data['namespace'] 274 : array(); 275 } 276 277 /** 278 * Parse a NAMESPACE response (RFC 2342 [5] & RFC 5255 [3.4]). 279 * 280 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 281 * object. 282 * @param Horde_Imap_Client_Tokenize $data The NAMESPACE data. 283 */ 284 protected function _parseNamespace( 285 Horde_Imap_Client_Interaction_Pipeline $pipeline, 286 Horde_Imap_Client_Tokenize $data 287 ) 288 { 289 $namespace_array = array( 290 Horde_Imap_Client_Data_Namespace::NS_PERSONAL, 291 Horde_Imap_Client_Data_Namespace::NS_OTHER, 292 Horde_Imap_Client_Data_Namespace::NS_SHARED 293 ); 294 295 $c = array(); 296 297 // Per RFC 2342, response from NAMESPACE command is: 298 // (PERSONAL NAMESPACES) (OTHER_USERS NAMESPACE) (SHARED NAMESPACES) 299 foreach ($namespace_array as $val) { 300 $entry = $data->next(); 301 302 if (is_null($entry)) { 303 continue; 304 } 305 306 while ($data->next() !== false) { 307 $ob = Horde_Imap_Client_Mailbox::get($data->next(), true); 308 309 $ns = new Horde_Imap_Client_Data_Namespace(); 310 $ns->delimiter = $data->next(); 311 $ns->name = strval($ob); 312 $ns->type = $val; 313 $c[strval($ob)] = $ns; 314 315 // RFC 4466: NAMESPACE extensions 316 while (($ext = $data->next()) !== false) { 317 switch (strtoupper($ext)) { 318 case 'TRANSLATION': 319 // RFC 5255 [3.4] - TRANSLATION extension 320 $data->next(); 321 $ns->translation = $data->next(); 322 $data->next(); 323 break; 324 } 325 } 326 } 327 } 328 329 $pipeline->data['namespace'] = new Horde_Imap_Client_Namespace_List($c); 330 } 331 332 /** 333 */ 334 public function alerts() 335 { 336 $alerts = empty($this->_temp['alerts']) 337 ? array() 338 : $this->_temp['alerts']; 339 $this->_temp['alerts'] = array(); 340 return $alerts; 341 } 342 343 /** 344 */ 345 protected function _login() 346 { 347 $secure = $this->getParam('secure'); 348 349 if (!empty($this->_temp['preauth'])) { 350 unset($this->_temp['preauth']); 351 352 /* Don't allow PREAUTH if we are requring secure access, since 353 * PREAUTH cannot provide secure access. */ 354 if (!$this->isSecureConnection() && ($secure !== false)) { 355 $this->logout(); 356 throw new Horde_Imap_Client_Exception( 357 Horde_Imap_Client_Translation::r("Could not open secure TLS connection to the IMAP server."), 358 Horde_Imap_Client_Exception::LOGIN_TLSFAILURE 359 ); 360 } 361 362 return $this->_loginTasks(); 363 } 364 365 /* Blank passwords are not allowed, so no need to even try 366 * authentication to determine this. */ 367 if (is_null($this->getParam('password'))) { 368 throw new Horde_Imap_Client_Exception( 369 Horde_Imap_Client_Translation::r("No password provided."), 370 Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED 371 ); 372 } 373 374 $this->_connect(); 375 376 $first_login = empty($this->_init['authmethod']); 377 378 // Switch to secure channel if using TLS. 379 if (!$this->isSecureConnection() && 380 (($secure === 'tls') || 381 (($secure === true) && $this->queryCapability('LOGINDISABLED')))) { 382 if ($first_login && !$this->queryCapability('STARTTLS')) { 383 /* We should never hit this - STARTTLS is required pursuant to 384 * RFC 3501 [6.2.1]. */ 385 throw new Horde_Imap_Client_Exception( 386 Horde_Imap_Client_Translation::r("Server does not support TLS connections."), 387 Horde_Imap_Client_Exception::LOGIN_TLSFAILURE 388 ); 389 } 390 391 // Switch over to a TLS connection. 392 // STARTTLS returns no untagged response. 393 $this->_sendCmd($this->_command('STARTTLS')); 394 395 if (!$this->_connection->startTls()) { 396 $this->logout(); 397 throw new Horde_Imap_Client_Exception( 398 Horde_Imap_Client_Translation::r("Could not open secure TLS connection to the IMAP server."), 399 Horde_Imap_Client_Exception::LOGIN_TLSFAILURE 400 ); 401 } 402 403 $this->_debug->info('Successfully completed TLS negotiation.'); 404 405 $this->setParam('secure', 'tls'); 406 $secure = 'tls'; 407 408 if ($first_login) { 409 // Expire cached CAPABILITY information (RFC 3501 [6.2.1]) 410 $this->_setInit('capability'); 411 412 // Reset language (RFC 5255 [3.1]) 413 $this->_setInit('lang'); 414 } 415 416 // Set language if using imapproxy 417 if (!empty($this->_init['imapproxy'])) { 418 $this->setLanguage(); 419 } 420 } 421 422 /* If we reached this point and don't have a secure connection, then 423 * a secure connections is not available. */ 424 if (($secure === true) && !$this->isSecureConnection()) { 425 $this->setParam('secure', false); 426 $secure = false; 427 } 428 429 if ($first_login) { 430 // Add authentication methods. 431 $auth_mech = array(); 432 433 $auth = ($auth = $this->queryCapability('AUTH')) 434 ? array_flip($auth) 435 : array(); 436 437 // XOAUTH2 438 if (isset($auth['XOAUTH2']) && $this->getParam('xoauth2_token')) { 439 $auth_mech[] = 'XOAUTH2'; 440 } 441 unset($auth['XOAUTH2']); 442 443 /* 'PLAIN' authentication always exists if under TLS (RFC 444 * 3501 [7.2.1]; RFC 2595). Use it over all other authentication 445 * methods, although we need to do sanity checking since broken 446 * IMAP servers may not support as required - fallback to 447 * LOGIN instead. */ 448 if ($secure) { 449 if (isset($auth['PLAIN'])) { 450 $auth_mech[] = 'PLAIN'; 451 unset($auth['PLAIN']); 452 } else { 453 $auth_mech[] = 'LOGIN'; 454 } 455 } 456 457 // Prefer CRAM-MD5 over DIGEST-MD5, as the latter has been 458 // obsoleted (RFC 6331). 459 if (isset($auth['CRAM-MD5'])) { 460 $auth_mech[] = 'CRAM-MD5'; 461 } elseif (isset($auth['DIGEST-MD5'])) { 462 $auth_mech[] = 'DIGEST-MD5'; 463 } 464 unset($auth['CRAM-MD5'], $auth['DIGEST-MD5']); 465 466 // Fall back to 'LOGIN' if available. 467 $auth_mech = array_merge($auth_mech, array_keys($auth)); 468 if (!$secure && !$this->queryCapability('LOGINDISABLED')) { 469 $auth_mech[] = 'LOGIN'; 470 } 471 472 if (empty($auth_mech)) { 473 throw new Horde_Imap_Client_Exception( 474 Horde_Imap_Client_Translation::r("No supported IMAP authentication method could be found."), 475 Horde_Imap_Client_Exception::LOGIN_NOAUTHMETHOD 476 ); 477 } 478 } else { 479 $auth_mech = array($this->_init['authmethod']); 480 } 481 482 $login_err = null; 483 484 foreach ($auth_mech as $method) { 485 try { 486 $resp = $this->_tryLogin($method); 487 $data = $resp->data; 488 $this->_setInit('authmethod', $method); 489 unset($this->_temp['referralcount']); 490 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 491 $data = $e->resp_data; 492 if (isset($data['loginerr'])) { 493 $login_err = $data['loginerr']; 494 } 495 $resp = false; 496 } catch (Horde_Imap_Client_Exception $e) { 497 $resp = false; 498 } 499 500 // Check for login referral (RFC 2221) response - can happen for 501 // an OK, NO, or BYE response. 502 if (isset($data['referral'])) { 503 foreach (array('hostspec', 'port', 'username') as $val) { 504 if (!is_null($data['referral']->$val)) { 505 $this->setParam($val, $data['referral']->$val); 506 } 507 } 508 509 if (!is_null($data['referral']->auth)) { 510 $this->_setInit('authmethod', $data['referral']->auth); 511 } 512 513 if (!isset($this->_temp['referralcount'])) { 514 $this->_temp['referralcount'] = 0; 515 } 516 517 // RFC 2221 [3] - Don't follow more than 10 levels of referral 518 // without consulting the user. 519 if (++$this->_temp['referralcount'] < 10) { 520 $this->logout(); 521 $this->_setInit('capability'); 522 $this->_setInit('namespace', array()); 523 return $this->login(); 524 } 525 526 unset($this->_temp['referralcount']); 527 } 528 529 if ($resp) { 530 return $this->_loginTasks($first_login, $resp->data); 531 } 532 } 533 534 /* Try again from scratch if authentication failed in an established, 535 * previously-authenticated object. */ 536 if (!empty($this->_init['authmethod'])) { 537 $this->_setInit(); 538 unset($this->_temp['no_cap']); 539 try { 540 return $this->_login(); 541 } catch (Horde_Imap_Client_Exception $e) {} 542 } 543 544 /* Default to AUTHENTICATIONFAILED error (see RFC 5530[3]). */ 545 if (is_null($login_err)) { 546 throw new Horde_Imap_Client_Exception( 547 Horde_Imap_Client_Translation::r("Mail server denied authentication."), 548 Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED 549 ); 550 } 551 552 throw $login_err; 553 } 554 555 /** 556 * Connects to the IMAP server. 557 * 558 * @throws Horde_Imap_Client_Exception 559 */ 560 protected function _connect() 561 { 562 if (!is_null($this->_connection)) { 563 return; 564 } 565 566 try { 567 $this->_connection = new Horde_Imap_Client_Socket_Connection_Socket( 568 $this->getParam('hostspec'), 569 $this->getParam('port'), 570 $this->getParam('timeout'), 571 $this->getParam('secure'), 572 array( 573 'debug' => $this->_debug, 574 'debugliteral' => $this->getParam('debug_literal') 575 ) 576 ); 577 } catch (Horde\Socket\Client\Exception $e) { 578 $e2 = new Horde_Imap_Client_Exception( 579 Horde_Imap_Client_Translation::r("Error connecting to mail server."), 580 Horde_Imap_Client_Exception::SERVER_CONNECT 581 ); 582 $e2->details = $e->details; 583 throw $e2; 584 } 585 586 // If we already have capability information, don't re-set with 587 // (possibly) limited information sent in the initial banner. 588 if (isset($this->_init['capability'])) { 589 $this->_temp['no_cap'] = true; 590 } 591 592 /* Get greeting information (untagged response). */ 593 try { 594 $this->_getLine($this->_pipeline()); 595 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 596 if ($e->status === Horde_Imap_Client_Interaction_Server::BYE) { 597 /* Server is explicitly rejecting our connection (RFC 3501 598 * [7.1.5]). */ 599 $e->setMessage(Horde_Imap_Client_Translation::r("Server rejected connection.")); 600 $e->setCode(Horde_Imap_Client_Exception::SERVER_CONNECT); 601 } 602 throw $e; 603 } 604 605 // Check for IMAP4rev1 support 606 if (!$this->queryCapability('IMAP4REV1')) { 607 throw new Horde_Imap_Client_Exception( 608 Horde_Imap_Client_Translation::r("The mail server does not support IMAP4rev1 (RFC 3501)."), 609 Horde_Imap_Client_Exception::SERVER_CONNECT 610 ); 611 } 612 613 // Set language if NOT using imapproxy 614 if (empty($this->_init['imapproxy'])) { 615 if ($this->queryCapability('XIMAPPROXY')) { 616 $this->_setInit('imapproxy', true); 617 } else { 618 $this->setLanguage(); 619 } 620 } 621 622 // If pre-authenticated, we need to do all login tasks now. 623 if (!empty($this->_temp['preauth'])) { 624 $this->login(); 625 } 626 } 627 628 /** 629 * Authenticate to the IMAP server. 630 * 631 * @param string $method IMAP login method. 632 * 633 * @return Horde_Imap_Client_Interaction_Pipeline Pipeline object. 634 * 635 * @throws Horde_Imap_Client_Exception 636 */ 637 protected function _tryLogin($method) 638 { 639 $username = $this->getParam('username'); 640 $password = $this->getParam('password'); 641 642 $authenticate_cmd = false; 643 644 switch ($method) { 645 case 'CRAM-MD5': 646 case 'CRAM-SHA1': 647 case 'CRAM-SHA256': 648 // RFC 2195: CRAM-MD5 649 // CRAM-SHA1 & CRAM-SHA256 supported by Courier SASL library 650 651 // Need $args because PHP 5.3 doesn't allow access to $this in 652 // anonymous functions. 653 $args = array( 654 $username, 655 strtolower(substr($method, 5)), 656 $password 657 ); 658 659 $cmd = $this->_command('AUTHENTICATE')->add(array( 660 $method, 661 new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($args) { 662 return new Horde_Imap_Client_Data_Format_List( 663 base64_encode($args[0] . ' ' . hash_hmac($args[1], base64_decode($ob->token->current()), $args[2], false)) 664 ); 665 }) 666 )); 667 $cmd->debug = sprintf('[AUTHENTICATE %s Command - username: %s]', $method, $username); 668 break; 669 670 case 'DIGEST-MD5': 671 // RFC 2831/4422; obsoleted by RFC 6331 672 673 // Need $args because PHP 5.3 doesn't allow access to $this in 674 // anonymous functions. 675 $args = array( 676 $username, 677 $password, 678 $this->getParam('hostspec') 679 ); 680 681 $cmd = $this->_command('AUTHENTICATE')->add(array( 682 $method, 683 new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($args) { 684 return new Horde_Imap_Client_Data_Format_List( 685 base64_encode(new Horde_Imap_Client_Auth_DigestMD5( 686 $args[0], 687 $args[1], 688 base64_decode($ob->token->current()), 689 $args[2], 690 'imap' 691 )) 692 ); 693 }), 694 new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) { 695 if (strpos(base64_decode($ob->token->current()), 'rspauth=') === false) { 696 throw new Horde_Imap_Client_Exception( 697 Horde_Imap_Client_Translation::r("Unexpected response from server when authenticating."), 698 Horde_Imap_Client_Exception::SERVER_CONNECT 699 ); 700 } 701 702 return new Horde_Imap_Client_Data_Format_List(); 703 }) 704 )); 705 $cmd->debug = sprintf('[AUTHENTICATE DIGEST-MD5 Command - username: %s]', $username); 706 break; 707 708 case 'LOGIN': 709 $cmd = $this->_command('LOGIN')->add(array( 710 new Horde_Imap_Client_Data_Format_Astring($username), 711 new Horde_Imap_Client_Data_Format_Astring($password) 712 )); 713 $cmd->debug = sprintf('[LOGIN Command - username: %s]', $username); 714 break; 715 716 case 'PLAIN': 717 // RFC 2595/4616 - PLAIN SASL mechanism 718 $auth = base64_encode(implode("\0", array( 719 $username, 720 $username, 721 $password 722 ))); 723 $authenticate_cmd = true; 724 break; 725 726 case 'XOAUTH2': 727 // Google XOAUTH2 728 $auth = $this->getParam('xoauth2_token'); 729 $authenticate_cmd = true; 730 break; 731 732 default: 733 throw new Horde_Imap_Client_Exception( 734 sprintf(Horde_Imap_Client_Translation::r("Unknown authentication method: %s"), $method), 735 Horde_Imap_Client_Exception::SERVER_CONNECT 736 ); 737 } 738 739 if ($authenticate_cmd) { 740 $cmd = $this->_command('AUTHENTICATE')->add($method); 741 742 if ($this->queryCapability('SASL-IR')) { 743 // IMAP Extension for SASL Initial Client Response (RFC 4959) 744 $cmd->add($auth); 745 $cmd->debug = sprintf('[SASL-IR AUTHENTICATE Command - method: %s, username: %s]', $method, $username); 746 } else { 747 $cmd->add(new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) use ($auth) { 748 return new Horde_Imap_Client_Data_Format_List($auth); 749 })); 750 $cmd->debug = sprintf('[AUTHENTICATE Command - method: %s, username: %s]', $method, $username); 751 } 752 753 /* This is an optional command continuation. E.g. XOAUTH2 will 754 * return error information in continuation response. */ 755 $error_continuation = new Horde_Imap_Client_Interaction_Command_Continuation(function($ob) { 756 return new Horde_Imap_Client_Data_Format_List(); 757 }); 758 $error_continuation->optional = true; 759 $cmd->add($error_continuation); 760 } 761 762 return $this->_sendCmd($this->_pipeline($cmd)); 763 } 764 765 /** 766 * Perform login tasks. 767 * 768 * @param boolean $firstlogin Is this the first login? 769 * @param array $resp The data response from the login command. 770 * May include: 771 * - capability_set: (boolean) True if CAPABILITY was set after login. 772 * - proxyreuse: (boolean) True if re-used connection via imapproxy. 773 * 774 * @return boolean True if global login tasks should be performed. 775 */ 776 protected function _loginTasks($firstlogin = true, array $resp = array()) 777 { 778 /* If reusing an imapproxy connection, no need to do any of these 779 * login tasks again. */ 780 if (!$firstlogin && !empty($resp['proxyreuse'])) { 781 if (isset($this->_init['enabled'])) { 782 $this->_temp['enabled'] = $this->_init['enabled']; 783 } 784 785 // If we have not yet set the language, set it now. 786 if (!isset($this->_init['lang'])) { 787 $this->_temp['lang_queue'] = true; 788 $this->setLanguage(); 789 unset($this->_temp['lang_queue']); 790 } 791 return false; 792 } 793 794 /* If we logged in for first time, and server did not return 795 * capability information, we need to mark for retrieval. */ 796 if ($firstlogin && empty($resp['capability_set'])) { 797 $this->_setInit('capability'); 798 } 799 800 $this->_temp['lang_queue'] = true; 801 $this->setLanguage(); 802 unset($this->_temp['lang_queue']); 803 804 /* Only active QRESYNC/CONDSTORE if caching is enabled. */ 805 if ($this->_initCache()) { 806 if ($this->queryCapability('QRESYNC')) { 807 $this->_enable(array('QRESYNC')); 808 } elseif ($this->queryCapability('CONDSTORE')) { 809 $this->_enable(array('CONDSTORE')); 810 } 811 } 812 813 return true; 814 } 815 816 /** 817 */ 818 protected function _logout() 819 { 820 if (empty($this->_temp['logout'])) { 821 /* If using imapproxy, force sending these commands, since they 822 * may not be sent again if they are (likely) initialization 823 * commands. */ 824 if (!empty($this->_cmdQueue) && 825 !empty($this->_init['imapproxy'])) { 826 $this->_sendCmd($this->_pipeline()); 827 } 828 829 $this->_temp['logout'] = true; 830 try { 831 $this->_sendCmd($this->_command('LOGOUT')); 832 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 833 // Ignore server errors 834 } 835 unset($this->_temp['logout']); 836 } 837 } 838 839 /** 840 */ 841 protected function _sendID($info) 842 { 843 $cmd = $this->_command('ID'); 844 845 if (empty($info)) { 846 $cmd->add(new Horde_Imap_Client_Data_Format_Nil()); 847 } else { 848 $tmp = new Horde_Imap_Client_Data_Format_List(); 849 foreach ($info as $key => $val) { 850 $tmp->add(array( 851 new Horde_Imap_Client_Data_Format_String(strtolower($key)), 852 new Horde_Imap_Client_Data_Format_Nstring($val) 853 )); 854 } 855 $cmd->add($tmp); 856 } 857 858 $this->_temp['id'] = $this->_sendCmd($cmd)->data['id']; 859 } 860 861 /** 862 * Parse an ID response (RFC 2971 [3.2]). 863 * 864 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 865 * object. 866 * @param Horde_Imap_Client_Tokenize $data The server response. 867 */ 868 protected function _parseID( 869 Horde_Imap_Client_Interaction_Pipeline $pipeline, 870 Horde_Imap_Client_Tokenize $data 871 ) 872 { 873 $ids = array(); 874 875 if (!is_null($data->next())) { 876 while (($curr = $data->next()) !== false) { 877 if (!is_null($id = $data->next())) { 878 $ids[$curr] = $id; 879 } 880 } 881 } 882 883 $pipeline->data['id'] = $ids; 884 } 885 886 /** 887 */ 888 protected function _getID() 889 { 890 if (!isset($this->_temp['id'])) { 891 $this->sendID(); 892 } 893 894 return $this->_temp['id']; 895 } 896 897 /** 898 */ 899 protected function _setLanguage($langs) 900 { 901 $cmd = $this->_command('LANGUAGE'); 902 foreach ($langs as $lang) { 903 $cmd->add(new Horde_Imap_Client_Data_Format_Astring($lang)); 904 } 905 906 if (!empty($this->_temp['lang_queue'])) { 907 $this->_cmdQueue[] = $cmd; 908 return array(); 909 } 910 911 try { 912 $this->_sendCmd($cmd); 913 } catch (Horde_Imap_Client_Exception $e) { 914 $this->_setInit('lang', false); 915 return null; 916 } 917 918 return $this->_init['lang']; 919 } 920 921 /** 922 */ 923 protected function _getLanguage($list) 924 { 925 if (!$list) { 926 return empty($this->_init['lang']) 927 ? null 928 : $this->_init['lang']; 929 } 930 931 if (!isset($this->_init['langavail'])) { 932 try { 933 $this->_sendCmd($this->_command('LANGUAGE')); 934 } catch (Horde_Imap_Client_Exception $e) { 935 $this->_setInit('langavail', array()); 936 } 937 } 938 939 return $this->_init['langavail']; 940 } 941 942 /** 943 * Parse a LANGUAGE response (RFC 5255 [3.3]). 944 * 945 * @param Horde_Imap_Client_Tokenize $data The server response. 946 */ 947 protected function _parseLanguage(Horde_Imap_Client_Tokenize $data) 948 { 949 $lang_list = $data->flushIterator(); 950 951 if (count($lang_list) === 1) { 952 // This is the language that was set. 953 $this->_setInit('lang', reset($lang_list)); 954 } else { 955 // These are the languages that are available. 956 $this->_setInit('langavail', $lang_list); 957 } 958 } 959 960 /** 961 * Enable an IMAP extension (see RFC 5161). 962 * 963 * @param array $exts The extensions to enable. 964 * 965 * @throws Horde_Imap_Client_Exception 966 */ 967 protected function _enable($exts) 968 { 969 if ($this->queryCapability('ENABLE')) { 970 // Only enable non-enabled extensions. 971 $exts = array_diff($exts, array_keys($this->_temp['enabled'])); 972 if (!empty($exts)) { 973 $this->_cmdQueue[] = $this->_command('ENABLE')->add($exts); 974 $this->_enabled($exts, 1); 975 } 976 } 977 } 978 979 /** 980 * Parse an ENABLED response (RFC 5161 [3.2]). 981 * 982 * @param Horde_Imap_Client_Tokenize $data The server response. 983 */ 984 protected function _parseEnabled(Horde_Imap_Client_Tokenize $data) 985 { 986 $this->_enabled($data->flushIterator(), 2); 987 } 988 989 /** 990 */ 991 protected function _enabled($exts, $status) 992 { 993 parent::_enabled($exts, $status); 994 995 if (($status == 2) && !empty($this->_init['imapproxy'])) { 996 $this->_setInit('enabled', $this->_temp['enabled']); 997 } 998 } 999 1000 /** 1001 */ 1002 protected function _openMailbox(Horde_Imap_Client_Mailbox $mailbox, $mode) 1003 { 1004 $qresync = isset($this->_temp['enabled']['QRESYNC']); 1005 1006 $cmd = $this->_command( 1007 ($mode == Horde_Imap_Client::OPEN_READONLY) ? 'EXAMINE' : 'SELECT' 1008 )->add( 1009 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 1010 ); 1011 $pipeline = $this->_pipeline($cmd); 1012 1013 /* If QRESYNC is available, synchronize the mailbox. */ 1014 if ($qresync) { 1015 $this->_initCache(); 1016 $md = $this->_cache->getMetaData($mailbox, null, array(self::CACHE_MODSEQ, 'uidvalid')); 1017 1018 if (isset($md[self::CACHE_MODSEQ])) { 1019 if ($uids = $this->_cache->get($mailbox)) { 1020 $uids = $this->getIdsOb($uids); 1021 1022 /* Check for extra long UID string. Assume that any 1023 * server that can handle QRESYNC can also handle long 1024 * input strings (at least 8 KB), so 7 KB is as good as 1025 * any guess as to an upper limit. If this occurs, provide 1026 * a range string (min -> max) instead. */ 1027 if (strlen($uid_str = $uids->tostring_sort) > 7000) { 1028 $uid_str = $uids->range_string; 1029 } 1030 } else { 1031 $uid_str = null; 1032 } 1033 1034 /* Several things can happen with a QRESYNC: 1035 * 1. UIDVALIDITY may have changed. If so, we need to expire 1036 * the cache immediately (done below). 1037 * 2. NOMODSEQ may have been returned. We can keep current 1038 * message cache data but won't be able to do flag caching. 1039 * 3. VANISHED/FETCH information was returned. These responses 1040 * will have already been handled by those response handlers. 1041 * 4. We are already synced with the local server in which 1042 * case it acts like a normal EXAMINE/SELECT. */ 1043 $cmd->add(new Horde_Imap_Client_Data_Format_List(array( 1044 'QRESYNC', 1045 new Horde_Imap_Client_Data_Format_List(array_filter(array( 1046 $md['uidvalid'], 1047 $md[self::CACHE_MODSEQ], 1048 $uid_str 1049 ))) 1050 ))); 1051 } 1052 1053 /* Let the 'CLOSED' response code handle mailbox switching if 1054 * QRESYNC is active. */ 1055 if ($this->_selected) { 1056 $pipeline->data['qresyncmbox'] = array($mailbox, $mode); 1057 } else { 1058 $this->_changeSelected($mailbox, $mode); 1059 } 1060 } else { 1061 if (!isset($this->_temp['enabled']['CONDSTORE']) && 1062 $this->_initCache() && 1063 $this->queryCapability('CONDSTORE')) { 1064 /* Activate CONDSTORE now if ENABLE is not available. */ 1065 $cmd->add(new Horde_Imap_Client_Data_Format_List('CONDSTORE')); 1066 $this->_enabled(array('CONDSTORE'), 2); 1067 } 1068 1069 $this->_changeSelected($mailbox, $mode); 1070 } 1071 1072 try { 1073 $this->_sendCmd($pipeline); 1074 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 1075 // An EXAMINE/SELECT failure with a return of 'NO' will cause the 1076 // current mailbox to be unselected. 1077 if ($e->status === Horde_Imap_Client_Interaction_Server::NO) { 1078 $this->_changeSelected(null); 1079 $this->_mode = 0; 1080 if (!$e->getCode()) { 1081 throw new Horde_Imap_Client_Exception( 1082 sprintf(Horde_Imap_Client_Translation::r("Could not open mailbox \"%s\"."), $mailbox), 1083 Horde_Imap_Client_Exception::MAILBOX_NOOPEN 1084 ); 1085 } 1086 } 1087 throw $e; 1088 } 1089 1090 if ($qresync) { 1091 /* Mailbox is fully sync'd. */ 1092 $this->_mailboxOb()->sync = true; 1093 } 1094 } 1095 1096 /** 1097 */ 1098 protected function _createMailbox(Horde_Imap_Client_Mailbox $mailbox, $opts) 1099 { 1100 $cmd = $this->_command('CREATE')->add( 1101 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 1102 ); 1103 1104 if (!empty($opts['special_use'])) { 1105 $cmd->add(array( 1106 'USE', 1107 new Horde_Imap_Client_Data_Format_List($opts['special_use']) 1108 )); 1109 } 1110 1111 // CREATE returns no untagged information (RFC 3501 [6.3.3]) 1112 $this->_sendCmd($cmd); 1113 } 1114 1115 /** 1116 */ 1117 protected function _deleteMailbox(Horde_Imap_Client_Mailbox $mailbox) 1118 { 1119 // Some IMAP servers will not allow a delete of a currently open 1120 // mailbox. 1121 if ($mailbox->equals($this->_selected)) { 1122 $this->close(); 1123 } 1124 1125 $cmd = $this->_command('DELETE')->add( 1126 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 1127 ); 1128 1129 try { 1130 // DELETE returns no untagged information (RFC 3501 [6.3.4]) 1131 $this->_sendCmd($cmd); 1132 } catch (Horde_Imap_Client_Exception $e) { 1133 // Some IMAP servers won't allow a mailbox delete unless all 1134 // messages in that mailbox are deleted. 1135 $this->expunge($mailbox, array( 1136 'delete' => true 1137 )); 1138 $this->_sendCmd($cmd); 1139 } 1140 } 1141 1142 /** 1143 */ 1144 protected function _renameMailbox(Horde_Imap_Client_Mailbox $old, 1145 Horde_Imap_Client_Mailbox $new) 1146 { 1147 // Some IMAP servers will not allow a rename of a currently open 1148 // mailbox. 1149 if ($old->equals($this->_selected)) { 1150 $this->close(); 1151 } 1152 1153 // RENAME returns no untagged information (RFC 3501 [6.3.5]) 1154 $this->_sendCmd( 1155 $this->_command('RENAME')->add(array( 1156 new Horde_Imap_Client_Data_Format_Mailbox($old), 1157 new Horde_Imap_Client_Data_Format_Mailbox($new) 1158 )) 1159 ); 1160 } 1161 1162 /** 1163 */ 1164 protected function _subscribeMailbox(Horde_Imap_Client_Mailbox $mailbox, 1165 $subscribe) 1166 { 1167 // SUBSCRIBE/UNSUBSCRIBE returns no untagged information (RFC 3501 1168 // [6.3.6 & 6.3.7]) 1169 $this->_sendCmd( 1170 $this->_command( 1171 $subscribe ? 'SUBSCRIBE' : 'UNSUBSCRIBE' 1172 )->add( 1173 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 1174 ) 1175 ); 1176 } 1177 1178 /** 1179 */ 1180 protected function _listMailboxes($pattern, $mode, $options) 1181 { 1182 // RFC 5258 [3.1]: Use LSUB for MBOX_SUBSCRIBED if no other server 1183 // return options are specified. 1184 if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) && 1185 empty($options['attributes']) && 1186 empty($options['children']) && 1187 empty($options['recursivematch']) && 1188 empty($options['remote']) && 1189 empty($options['special_use']) && 1190 empty($options['status'])) { 1191 return $this->_getMailboxList( 1192 $pattern, 1193 Horde_Imap_Client::MBOX_SUBSCRIBED, 1194 array( 1195 'delimiter' => !empty($options['delimiter']), 1196 'flat' => !empty($options['flat']), 1197 'no_listext' => true 1198 ) 1199 ); 1200 } 1201 1202 // Get the list of subscribed/unsubscribed mailboxes. Since LSUB is 1203 // not guaranteed to have correct attributes, we must use LIST to 1204 // ensure we receive the correct information. 1205 if (($mode != Horde_Imap_Client::MBOX_ALL) && 1206 !$this->queryCapability('LIST-EXTENDED')) { 1207 $subscribed = $this->_getMailboxList($pattern, Horde_Imap_Client::MBOX_SUBSCRIBED, array('flat' => true)); 1208 1209 // If mode is subscribed, and 'flat' option is true, we can 1210 // return now. 1211 if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) && 1212 !empty($options['flat'])) { 1213 return $subscribed; 1214 } 1215 } else { 1216 $subscribed = null; 1217 } 1218 1219 return $this->_getMailboxList($pattern, $mode, $options, $subscribed); 1220 } 1221 1222 /** 1223 * Obtain a list of mailboxes. 1224 * 1225 * @param array $pattern The mailbox search pattern(s). 1226 * @param integer $mode Which mailboxes to return. 1227 * @param array $options Additional options. 'no_listext' will skip 1228 * using the LIST-EXTENDED capability. 1229 * @param array $subscribed A list of subscribed mailboxes. 1230 * 1231 * @return array See listMailboxes((). 1232 * 1233 * @throws Horde_Imap_Client_Exception 1234 */ 1235 protected function _getMailboxList($pattern, $mode, $options, 1236 $subscribed = null) 1237 { 1238 $check = (($mode != Horde_Imap_Client::MBOX_ALL) && !is_null($subscribed)); 1239 1240 // Setup entry for use in _parseList(). 1241 $pipeline = $this->_pipeline(); 1242 $pipeline->data['mailboxlist'] = array( 1243 'check' => $check, 1244 'ext' => false, 1245 'options' => $options, 1246 'subexist' => ($mode == Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS), 1247 /* Can't use array_merge here because it will destroy any mailbox 1248 * name (key) that is "numeric". */ 1249 'subscribed' => ($check ? (array_flip(array_map('strval', $subscribed)) + array('INBOX' => true)) : null) 1250 ); 1251 $pipeline->data['listresponse'] = array(); 1252 1253 $cmds = array(); 1254 $return_opts = new Horde_Imap_Client_Data_Format_List(); 1255 1256 if ($this->queryCapability('LIST-EXTENDED') && 1257 empty($options['no_listext'])) { 1258 $cmd = $this->_command('LIST'); 1259 $pipeline->data['mailboxlist']['ext'] = true; 1260 1261 $select_opts = new Horde_Imap_Client_Data_Format_List(); 1262 $subscribed = false; 1263 1264 if (($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) || 1265 ($mode == Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS)) { 1266 $select_opts->add('SUBSCRIBED'); 1267 $return_opts->add('SUBSCRIBED'); 1268 $subscribed = true; 1269 } 1270 1271 if (!empty($options['remote'])) { 1272 $select_opts->add('REMOTE'); 1273 } 1274 1275 if (!empty($options['recursivematch'])) { 1276 $select_opts->add('RECURSIVEMATCH'); 1277 } 1278 1279 $cmd->add(array( 1280 $select_opts, 1281 '' 1282 )); 1283 1284 $tmp = new Horde_Imap_Client_Data_Format_List(); 1285 foreach ($pattern as $val) { 1286 if ($subscribed && (strcasecmp($val, 'INBOX') === 0)) { 1287 $cmds[] = $this->_command('LIST')->add(array( 1288 '', 1289 'INBOX' 1290 )); 1291 } else { 1292 $tmp->add(new Horde_Imap_Client_Data_Format_ListMailbox($val)); 1293 } 1294 } 1295 1296 if (count($tmp)) { 1297 $cmd->add($tmp); 1298 $cmds[] = $cmd; 1299 } 1300 1301 if (!empty($options['children'])) { 1302 $return_opts->add('CHILDREN'); 1303 } 1304 1305 if (!empty($options['special_use'])) { 1306 $return_opts->add('SPECIAL-USE'); 1307 } 1308 } else { 1309 foreach ($pattern as $val) { 1310 $cmds[] = $this->_command( 1311 ($mode == Horde_Imap_Client::MBOX_SUBSCRIBED) ? 'LSUB' : 'LIST' 1312 )->add(array( 1313 '', 1314 new Horde_Imap_Client_Data_Format_ListMailbox($val) 1315 )); 1316 } 1317 } 1318 1319 /* LIST-STATUS does NOT depend on LIST-EXTENDED. */ 1320 if (!empty($options['status']) && 1321 $this->queryCapability('LIST-STATUS')) { 1322 $available_status = array( 1323 Horde_Imap_Client::STATUS_MESSAGES, 1324 Horde_Imap_Client::STATUS_RECENT, 1325 Horde_Imap_Client::STATUS_UIDNEXT, 1326 Horde_Imap_Client::STATUS_UIDVALIDITY, 1327 Horde_Imap_Client::STATUS_UNSEEN, 1328 Horde_Imap_Client::STATUS_HIGHESTMODSEQ 1329 ); 1330 1331 $status_opts = array(); 1332 foreach (array_intersect($this->_statusFields, $available_status) as $key => $val) { 1333 if ($options['status'] & $val) { 1334 $status_opts[] = $key; 1335 } 1336 } 1337 1338 if (count($status_opts)) { 1339 $return_opts->add(array( 1340 'STATUS', 1341 new Horde_Imap_Client_Data_Format_List( 1342 array_map('strtoupper', $status_opts) 1343 ) 1344 )); 1345 } 1346 } 1347 1348 foreach ($cmds as $val) { 1349 if (count($return_opts)) { 1350 $val->add(array( 1351 'RETURN', 1352 $return_opts 1353 )); 1354 } 1355 1356 $pipeline->add($val); 1357 } 1358 1359 try { 1360 $lr = $this->_sendCmd($pipeline)->data['listresponse']; 1361 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 1362 /* Archiveopteryx 3.1.3 can't process empty list-select-opts list. 1363 * Retry using base IMAP4rev1 functionality. */ 1364 if (($e->status === Horde_Imap_Client_Interaction_Server::BAD) && 1365 $this->queryCapability('LIST-EXTENDED')) { 1366 $this->_unsetCapability('LIST-EXTENDED'); 1367 return $this->_listMailboxes($pattern, $mode, $options); 1368 } 1369 1370 throw $e; 1371 } 1372 1373 if (!empty($options['flat'])) { 1374 return array_values($lr); 1375 } 1376 1377 /* Add in STATUS return, if needed. */ 1378 if (!empty($options['status'])) { 1379 foreach ($pattern as $val) { 1380 $val_utf8 = Horde_Imap_Client_Utf7imap::Utf7ImapToUtf8($val); 1381 if (isset($lr[$val_utf8])) { 1382 $lr[$val_utf8]['status'] = $this->_prepareStatusResponse($status_opts, $val_utf8); 1383 } 1384 } 1385 } 1386 1387 return $lr; 1388 } 1389 1390 /** 1391 * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]). 1392 * 1393 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 1394 * object. 1395 * @param Horde_Imap_Client_Tokenize $data The server response (includes 1396 * type as first token). 1397 * 1398 * @throws Horde_Imap_Client_Exception 1399 */ 1400 protected function _parseList( 1401 Horde_Imap_Client_Interaction_Pipeline $pipeline, 1402 Horde_Imap_Client_Tokenize $data 1403 ) 1404 { 1405 $data->next(); 1406 $attr = $data->flushIterator(); 1407 $delimiter = $data->next(); 1408 $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true); 1409 $ml = $pipeline->data['mailboxlist']; 1410 1411 if ($ml['check'] && 1412 $ml['subexist'] && 1413 // Subscribed list is in UTF-8. 1414 !isset($ml['subscribed'][strval($mbox)])) { 1415 return; 1416 } elseif ((!$ml['check'] && $ml['subexist']) || 1417 (empty($ml['options']['flat']) && 1418 !empty($ml['options']['attributes']))) { 1419 $attr = array_flip(array_map('strtolower', $attr)); 1420 if ($ml['subexist'] && 1421 !$ml['check'] && 1422 isset($attr['\\nonexistent'])) { 1423 return; 1424 } 1425 } 1426 1427 if (empty($ml['options']['flat'])) { 1428 $tmp = array( 1429 'mailbox' => $mbox 1430 ); 1431 1432 if (!empty($ml['options']['attributes'])) { 1433 /* RFC 5258 [3.4]: inferred attributes. */ 1434 if ($ml['ext']) { 1435 if (isset($attr['\\noinferiors'])) { 1436 $attr['\\hasnochildren'] = 1; 1437 } 1438 if (isset($attr['\\nonexistent'])) { 1439 $attr['\\noselect'] = 1; 1440 } 1441 } 1442 $tmp['attributes'] = array_keys($attr); 1443 } 1444 if (!empty($ml['options']['delimiter'])) { 1445 $tmp['delimiter'] = $delimiter; 1446 } 1447 if ($data->next() !== false) { 1448 $tmp['extended'] = $data->flushIterator(); 1449 } 1450 $pipeline->data['listresponse'][strval($mbox)] = $tmp; 1451 } else { 1452 $pipeline->data['listresponse'][] = $mbox; 1453 } 1454 } 1455 1456 /** 1457 */ 1458 protected function _status($mboxes, $flags) 1459 { 1460 $out = $to_process = array(); 1461 $pipeline = $this->_pipeline(); 1462 $unseen_flags = array( 1463 Horde_Imap_Client::STATUS_FIRSTUNSEEN, 1464 Horde_Imap_Client::STATUS_UNSEEN 1465 ); 1466 1467 foreach ($mboxes as $mailbox) { 1468 /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must 1469 * do a SELECT/EXAMINE to get this information (data will be 1470 * caught in the code below). */ 1471 if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) || 1472 ($flags & Horde_Imap_Client::STATUS_FLAGS) || 1473 ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) || 1474 ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY)) { 1475 $this->openMailbox($mailbox); 1476 } 1477 1478 $mbox_ob = $this->_mailboxOb($mailbox); 1479 $data = $query = array(); 1480 1481 foreach ($this->_statusFields as $key => $val) { 1482 if (!($val & $flags)) { 1483 continue; 1484 } 1485 1486 if ($val == Horde_Imap_Client::STATUS_HIGHESTMODSEQ) { 1487 /* Don't include modseq returns if server does not support 1488 * it. */ 1489 if (!$this->queryCapability('CONDSTORE')) { 1490 continue; 1491 } 1492 1493 /* Even though CONDSTORE is available, it may not yet have 1494 * been enabled. */ 1495 if (!isset($this->_temp['enabled']['CONDSTORE'])) { 1496 $this->_enabled(array('CONDSTORE'), 2); 1497 } 1498 } 1499 1500 if ($mailbox->equals($this->_selected)) { 1501 if (!is_null($tmp = $mbox_ob->getStatus($val))) { 1502 $data[$key] = $tmp; 1503 } elseif (($val == Horde_Imap_Client::STATUS_UIDNEXT) && 1504 ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE)) { 1505 /* UIDNEXT is not mandatory. */ 1506 if ($mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) == 0) { 1507 $data[$key] = 0; 1508 } else { 1509 $fquery = new Horde_Imap_Client_Fetch_Query(); 1510 $fquery->uid(); 1511 $fetch_res = $this->fetch($this->_selected, $fquery, array( 1512 'ids' => $this->getIdsOb(Horde_Imap_Client_Ids::LARGEST) 1513 )); 1514 $data[$key] = $fetch_res->first()->getUid() + 1; 1515 } 1516 } elseif (in_array($val, $unseen_flags)) { 1517 /* RFC 3501 [6.3.1] - FIRSTUNSEEN information is not 1518 * mandatory. If missing in EXAMINE/SELECT results, we 1519 * need to do a search. An UNSEEN count also requires 1520 * a search. */ 1521 $squery = new Horde_Imap_Client_Search_Query(); 1522 $squery->flag(Horde_Imap_Client::FLAG_SEEN, false); 1523 $search = $this->search($mailbox, $squery, array( 1524 'results' => array( 1525 Horde_Imap_Client::SEARCH_RESULTS_MIN, 1526 Horde_Imap_Client::SEARCH_RESULTS_COUNT 1527 ), 1528 'sequence' => true 1529 )); 1530 1531 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $search['min']); 1532 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UNSEEN, $search['count']); 1533 1534 $data[$key] = $mbox_ob->getStatus($val); 1535 } 1536 } else { 1537 $query[] = $key; 1538 } 1539 } 1540 1541 $out[strval($mailbox)] = $data; 1542 1543 if (count($query)) { 1544 $pipeline->add( 1545 $this->_command('STATUS')->add(array( 1546 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 1547 new Horde_Imap_Client_Data_Format_List( 1548 array_map('strtoupper', $query) 1549 ) 1550 )) 1551 ); 1552 $to_process[] = array($query, $mailbox); 1553 } 1554 } 1555 1556 if (count($pipeline)) { 1557 $this->_sendCmd($pipeline); 1558 1559 foreach ($to_process as $val) { 1560 $out[strval($val[1])] += $this->_prepareStatusResponse($val[0], $val[1]); 1561 } 1562 } 1563 1564 return $out; 1565 } 1566 1567 /** 1568 * Parse a STATUS response (RFC 3501 [7.2.4]). 1569 * 1570 * @param Horde_Imap_Client_Tokenize $data Token data 1571 */ 1572 protected function _parseStatus(Horde_Imap_Client_Tokenize $data) 1573 { 1574 // Mailbox name is in UTF7-IMAP 1575 $mbox_ob = $this->_mailboxOb( 1576 Horde_Imap_Client_Mailbox::get($data->next(), true) 1577 ); 1578 1579 $data->next(); 1580 1581 while (($k = $data->next()) !== false) { 1582 $mbox_ob->setStatus( 1583 $this->_statusFields[strtolower($k)], 1584 $data->next() 1585 ); 1586 } 1587 } 1588 1589 /** 1590 * Prepares a status response for a mailbox. 1591 * 1592 * @param array $request The status keys to return. 1593 * @param string $mailbox The mailbox to query. 1594 */ 1595 protected function _prepareStatusResponse($request, $mailbox) 1596 { 1597 $mbox_ob = $this->_mailboxOb($mailbox); 1598 $out = array(); 1599 1600 foreach ($request as $val) { 1601 $out[$val] = $mbox_ob->getStatus($this->_statusFields[$val]); 1602 } 1603 1604 return $out; 1605 } 1606 1607 /** 1608 */ 1609 protected function _append(Horde_Imap_Client_Mailbox $mailbox, $data, 1610 $options) 1611 { 1612 // Check for MULTIAPPEND extension (RFC 3502) 1613 if ((count($data) > 1) && !$this->queryCapability('MULTIAPPEND')) { 1614 $result = $this->getIdsOb(); 1615 foreach (array_keys($data) as $key) { 1616 $res = $this->_append($mailbox, array($data[$key]), $options); 1617 if (($res === true) || ($result === true)) { 1618 $result = true; 1619 } else { 1620 $result->add($res); 1621 } 1622 } 1623 return $result; 1624 } 1625 1626 // Check for CATENATE extension (RFC 4469) 1627 $catenate = $this->queryCapability('CATENATE'); 1628 1629 $asize = 0; 1630 1631 $cmd = $this->_command('APPEND')->add( 1632 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 1633 ); 1634 $cmd->literal8 = true; 1635 1636 foreach (array_keys($data) as $key) { 1637 if (!empty($data[$key]['flags'])) { 1638 $tmp = new Horde_Imap_Client_Data_Format_List(); 1639 foreach ($data[$key]['flags'] as $val) { 1640 /* Ignore recent flag. RFC 3501 [9]: flag definition */ 1641 if (strcasecmp($val, Horde_Imap_Client::FLAG_RECENT) !== 0) { 1642 $tmp->add($val); 1643 } 1644 } 1645 $cmd->add($tmp); 1646 } 1647 1648 if (!empty($data[$key]['internaldate'])) { 1649 $cmd->add(new Horde_Imap_Client_Data_Format_DateTime($data[$key]['internaldate'])); 1650 } 1651 1652 if (is_array($data[$key]['data'])) { 1653 if ($catenate) { 1654 $cmd->add('CATENATE'); 1655 $tmp = new Horde_Imap_Client_Data_Format_List(); 1656 } else { 1657 $data_stream = new Horde_Stream_Temp(); 1658 } 1659 1660 reset($data[$key]['data']); 1661 while (list(,$v) = each($data[$key]['data'])) { 1662 switch ($v['t']) { 1663 case 'text': 1664 if ($catenate) { 1665 $tmp->add(array( 1666 'TEXT', 1667 $this->_appendData($v['v'], $asize) 1668 )); 1669 } else { 1670 if (is_resource($v['v'])) { 1671 rewind($v['v']); 1672 } 1673 $data_stream->add($v['v']); 1674 } 1675 break; 1676 1677 case 'url': 1678 if ($catenate) { 1679 $tmp->add(array( 1680 'URL', 1681 new Horde_Imap_Client_Data_Format_Astring($v['v']) 1682 )); 1683 } else { 1684 $data_stream->add($this->_convertCatenateUrl($v['v'])); 1685 } 1686 break; 1687 } 1688 } 1689 1690 if ($catenate) { 1691 $cmd->add($tmp); 1692 } else { 1693 $cmd->add($this->_appendData($data_stream->stream, $asize)); 1694 } 1695 } else { 1696 $cmd->add($this->_appendData($data[$key]['data'], $asize)); 1697 } 1698 } 1699 1700 /* Although it is normally more efficient to use LITERAL+, disable if 1701 * payload is over 0.5 MB because it allows the server to throw error 1702 * before we potentially push a lot of data to server that would 1703 * otherwise be ignored (see RFC 4549 [4.2.2.3]). 1704 * Additionally, if using BINARY, since so many IMAP servers have 1705 * issues with APPEND + BINARY, don't use LITERAL+ since servers may 1706 * send BAD after initial command. */ 1707 $cmd->literalplus = (($asize < 524288) && !$this->queryCapability('BINARY')); 1708 1709 // If the mailbox is currently selected read-only, we need to close 1710 // because some IMAP implementations won't allow an append. And some 1711 // implementations don't support append on ANY open mailbox. Be safe 1712 // and always make sure we are in a non-selected state. 1713 $this->close(); 1714 1715 try { 1716 $resp = $this->_sendCmd($cmd); 1717 } catch (Horde_Imap_Client_Exception $e) { 1718 switch ($e->getCode()) { 1719 case $e::CATENATE_BADURL: 1720 case $e::CATENATE_TOOBIG: 1721 /* Cyrus 2.4 (at least as of .14) has a broken CATENATE (see 1722 * Bug #11111). Regardless, if CATENATE is broken, we can try 1723 * to fallback to APPEND. */ 1724 $this->_unsetCapability('CATENATE'); 1725 return $this->_append($mailbox, $data, $options); 1726 1727 case $e::DISCONNECT: 1728 /* Workaround broken literal8 on Cyrus. */ 1729 if ($this->queryCapability('BINARY')) { 1730 // Need to re-login first before removing capability. 1731 $this->login(); 1732 $this->_unsetCapability('BINARY'); 1733 return $this->_append($mailbox, $data, $options); 1734 } 1735 break; 1736 } 1737 1738 if (!empty($options['create']) && 1739 !empty($e->resp_data['trycreate'])) { 1740 $this->createMailbox($mailbox); 1741 unset($options['create']); 1742 return $this->_append($mailbox, $data, $options); 1743 } 1744 1745 /* RFC 3516/4466 says we should be able to append binary data 1746 * using literal8 "~{#} format", but it doesn't seem to work on 1747 * all servers tried (UW-IMAP/Cyrus). Do a last-ditch check for 1748 * broken BINARY and attempt to fix here. */ 1749 if ($this->queryCapability('BINARY') && 1750 ($e instanceof Horde_Imap_Client_Exception_ServerResponse)) { 1751 switch ($e->status) { 1752 case Horde_Imap_Client_Interaction_Server::BAD: 1753 case Horde_Imap_Client_Interaction_Server::NO: 1754 $this->_unsetCapability('BINARY'); 1755 return $this->_append($mailbox, $data, $options); 1756 } 1757 } 1758 1759 throw $e; 1760 } 1761 1762 /* If we reach this point and have data in 'appenduid', UIDPLUS (RFC 1763 * 4315) has done the dirty work for us. */ 1764 return isset($resp->data['appenduid']) 1765 ? $resp->data['appenduid'] 1766 : true; 1767 } 1768 1769 /** 1770 * Prepares append message data for insertion into the IMAP command 1771 * string. 1772 * 1773 * @param mixed $data Either a resource or a string. 1774 * @param integer &$asize Total append size. 1775 * 1776 * @return Horde_Imap_Client_Data_Format_String The data object. 1777 */ 1778 protected function _appendData($data, &$asize) 1779 { 1780 if (is_resource($data)) { 1781 rewind($data); 1782 } 1783 1784 $ob = new Horde_Imap_Client_Data_Format_String($data, array( 1785 'eol' => true, 1786 'skipscan' => true 1787 )); 1788 1789 // APPEND data MUST be sent in a literal (RFC 3501 [6.3.11]). 1790 $ob->forceLiteral(); 1791 1792 $asize += $ob->length(); 1793 1794 return $ob; 1795 } 1796 1797 /** 1798 * Converts a CATENATE URL to stream data. 1799 * 1800 * @param string $url The CATENATE URL. 1801 * 1802 * @return resource A stream containing the data. 1803 */ 1804 protected function _convertCatenateUrl($url) 1805 { 1806 $e = $part = null; 1807 $url = new Horde_Imap_Client_Url($url); 1808 1809 if (!is_null($url->mailbox) && !is_null($url->uid)) { 1810 try { 1811 $status_res = is_null($url->uidvalidity) 1812 ? null 1813 : $this->status($url->mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY); 1814 1815 if (is_null($status_res) || 1816 ($status_res['uidvalidity'] == $url->uidvalidity)) { 1817 if (!isset($this->_temp['catenate_ob'])) { 1818 $this->_temp['catenate_ob'] = new Horde_Imap_Client_Socket_Catenate($this); 1819 } 1820 $part = $this->_temp['catenate_ob']->fetchFromUrl($url); 1821 } 1822 } catch (Horde_Imap_Client_Exception $e) {} 1823 } 1824 1825 if (is_null($part)) { 1826 $message = 'Bad IMAP URL given in CATENATE data: ' . strval($url); 1827 if ($e) { 1828 $message .= ' ' . $e->getMessage(); 1829 } 1830 1831 throw new InvalidArgumentException($message); 1832 } 1833 1834 return $part; 1835 } 1836 1837 /** 1838 */ 1839 protected function _check() 1840 { 1841 // CHECK returns no untagged information (RFC 3501 [6.4.1]) 1842 $this->_sendCmd($this->_command('CHECK')); 1843 } 1844 1845 /** 1846 */ 1847 protected function _close($options) 1848 { 1849 if (empty($options['expunge'])) { 1850 if ($this->queryCapability('UNSELECT')) { 1851 // RFC 3691 defines 'UNSELECT' for precisely this purpose 1852 $this->_sendCmd($this->_command('UNSELECT')); 1853 } else { 1854 /* RFC 3501 [6.4.2]: to close a mailbox without expunge, 1855 * select a non-existent mailbox. */ 1856 try { 1857 $this->_sendCmd($this->_command('EXAMINE')->add( 1858 new Horde_Imap_Client_Data_Format_Mailbox("\24nonexist\24") 1859 )); 1860 1861 /* Not pipelining, since the odds that this CLOSE is even 1862 * needed is tiny; and it returns BAD, which should be 1863 * avoided, if possible. */ 1864 $this->_sendCmd($this->_command('CLOSE')); 1865 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 1866 // Ignore error; it is expected. 1867 } 1868 } 1869 } else { 1870 // If caching, we need to know the UIDs being deleted, so call 1871 // expunge() before calling close(). 1872 if ($this->_initCache(true)) { 1873 $this->expunge($this->_selected); 1874 } 1875 1876 // CLOSE returns no untagged information (RFC 3501 [6.4.2]) 1877 $this->_sendCmd($this->_command('CLOSE')); 1878 } 1879 } 1880 1881 /** 1882 */ 1883 protected function _expunge($options) 1884 { 1885 $expunged_ob = $modseq = null; 1886 $ids = $options['ids']; 1887 $list_msgs = !empty($options['list']); 1888 $uidplus = $this->queryCapability('UIDPLUS'); 1889 $unflag = array(); 1890 $use_cache = $this->_initCache(true); 1891 1892 if ($ids->all) { 1893 if (!$uidplus || $list_msgs || $use_cache) { 1894 $ids = $this->resolveIds($this->_selected, $ids, 2); 1895 } 1896 } elseif ($uidplus) { 1897 /* If QRESYNC is not available, and we are returning the list of 1898 * expunged messages (or we are caching), we have to make sure we 1899 * have a mapping of Sequence -> UIDs. If we have QRESYNC, the 1900 * server SHOULD return a VANISHED response with UIDs. However, 1901 * even if the server returns EXPUNGEs instead, we can use 1902 * vanished() to grab the list. */ 1903 unset($this->_temp['search_save']); 1904 if (isset($this->_temp['enabled']['QRESYNC'])) { 1905 $ids = $this->resolveIds($this->_selected, $ids, 1); 1906 if ($list_msgs) { 1907 $modseq = $this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ); 1908 } 1909 } else { 1910 $ids = $this->resolveIds($this->_selected, $ids, ($list_msgs || $use_cache) ? 2 : 1); 1911 } 1912 if (!empty($this->_temp['search_save'])) { 1913 $ids = $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES); 1914 } 1915 } else { 1916 /* Without UIDPLUS, need to temporarily unflag all messages marked 1917 * as deleted but not a part of requested IDs to delete. Use NOT 1918 * searches to accomplish this goal. */ 1919 $squery = new Horde_Imap_Client_Search_Query(); 1920 $squery->flag(Horde_Imap_Client::FLAG_DELETED, true); 1921 $squery->ids($ids, true); 1922 1923 $s_res = $this->search($this->_selected, $squery, array( 1924 'results' => array( 1925 Horde_Imap_Client::SEARCH_RESULTS_MATCH, 1926 Horde_Imap_Client::SEARCH_RESULTS_SAVE 1927 ) 1928 )); 1929 1930 $this->store($this->_selected, array( 1931 'ids' => empty($s_res['save']) ? $s_res['match'] : $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES), 1932 'remove' => array(Horde_Imap_Client::FLAG_DELETED) 1933 )); 1934 1935 $unflag = $s_res['match']; 1936 } 1937 1938 if ($list_msgs) { 1939 $expunged_ob = $this->getIdsOb(); 1940 $this->_temp['expunged'] = $expunged_ob; 1941 } 1942 1943 /* Always use UID EXPUNGE if available. */ 1944 if ($uidplus) { 1945 /* We can only pipeline STORE w/ EXPUNGE if using UIDs and UIDPLUS 1946 * is available. */ 1947 if (empty($options['delete'])) { 1948 $pipeline = $this->_pipeline(); 1949 } else { 1950 $pipeline = $this->_storeCmd(array( 1951 'add' => array( 1952 Horde_Imap_Client::FLAG_DELETED 1953 ), 1954 'ids' => $ids 1955 )); 1956 } 1957 1958 foreach ($ids->split(2000) as $val) { 1959 $pipeline->add( 1960 $this->_command('UID EXPUNGE')->add($val) 1961 ); 1962 } 1963 1964 $resp = $this->_sendCmd($pipeline); 1965 } else { 1966 if (!empty($options['delete'])) { 1967 $this->store($this->_selected, array( 1968 'add' => array(Horde_Imap_Client::FLAG_DELETED), 1969 'ids' => $ids 1970 )); 1971 } 1972 1973 if ($use_cache || $list_msgs) { 1974 $this->_sendCmd($this->_command('EXPUNGE')); 1975 } else { 1976 /* This is faster than an EXPUNGE because the server will not 1977 * return untagged EXPUNGE responses. We can only do this if 1978 * we are not updating cache information. */ 1979 $this->close(array('expunge' => true)); 1980 } 1981 } 1982 1983 unset($this->_temp['expunged']); 1984 1985 if (!empty($unflag)) { 1986 $this->store($this->_selected, array( 1987 'add' => array(Horde_Imap_Client::FLAG_DELETED), 1988 'ids' => $unflag 1989 )); 1990 } 1991 1992 if (!is_null($modseq) && !empty($resp->data['expunge_seen'])) { 1993 /* There's a chance we actually did a full map of sequence -> UID, 1994 * but this code should never be reached in the first place so 1995 * be ultra-safe and just do a full VANISHED search. */ 1996 $expunged_ob = $this->vanished($this->_selected, $modseq, array( 1997 'ids' => $ids 1998 )); 1999 $this->_deleteMsgs($this->_selected, $expunged_ob, array( 2000 'pipeline' => $resp 2001 )); 2002 } 2003 2004 return $expunged_ob; 2005 } 2006 2007 /** 2008 * Parse a VANISHED response (RFC 7162 [3.2.10]). 2009 * 2010 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2011 * object. 2012 * @param Horde_Imap_Client_Tokenize $data The response data. 2013 */ 2014 protected function _parseVanished( 2015 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2016 Horde_Imap_Client_Tokenize $data 2017 ) 2018 { 2019 /* There are two forms of VANISHED. VANISHED (EARLIER) will be sent 2020 * in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call. 2021 * If this is the case, we can go ahead and update the cache 2022 * immediately (we know we are caching or else QRESYNC would not be 2023 * enabled). HIGHESTMODSEQ information will be updated via the tagged 2024 * response. */ 2025 if (($curr = $data->next()) === true) { 2026 if (strtoupper($data->next()) === 'EARLIER') { 2027 /* Caching is guaranteed to be active if we are using 2028 * QRESYNC. */ 2029 $data->next(); 2030 $vanished = $this->getIdsOb($data->next()); 2031 if (isset($pipeline->data['vanished'])) { 2032 $pipeline->data['vanished']->add($vanished); 2033 } else { 2034 $this->_deleteMsgs($this->_selected, $vanished, array( 2035 'pipeline' => $pipeline 2036 )); 2037 } 2038 } 2039 } else { 2040 /* The second form is just VANISHED. This is analogous to EXPUNGE 2041 * and requires the message count to decrement. */ 2042 $this->_deleteMsgs($this->_selected, $this->getIdsOb($curr), array( 2043 'decrement' => true, 2044 'pipeline' => $pipeline 2045 )); 2046 } 2047 } 2048 2049 /** 2050 * Search a mailbox. This driver supports all IMAP4rev1 search criteria 2051 * as defined in RFC 3501. 2052 */ 2053 protected function _search($query, $options) 2054 { 2055 $sort_criteria = array( 2056 Horde_Imap_Client::SORT_ARRIVAL => 'ARRIVAL', 2057 Horde_Imap_Client::SORT_CC => 'CC', 2058 Horde_Imap_Client::SORT_DATE => 'DATE', 2059 Horde_Imap_Client::SORT_DISPLAYFROM => 'DISPLAYFROM', 2060 Horde_Imap_Client::SORT_DISPLAYTO => 'DISPLAYTO', 2061 Horde_Imap_Client::SORT_FROM => 'FROM', 2062 Horde_Imap_Client::SORT_REVERSE => 'REVERSE', 2063 Horde_Imap_Client::SORT_RELEVANCY => 'RELEVANCY', 2064 // This is a bogus entry to allow the sort options check to 2065 // correctly work below. 2066 Horde_Imap_Client::SORT_SEQUENCE => 'SEQUENCE', 2067 Horde_Imap_Client::SORT_SIZE => 'SIZE', 2068 Horde_Imap_Client::SORT_SUBJECT => 'SUBJECT', 2069 Horde_Imap_Client::SORT_TO => 'TO' 2070 ); 2071 2072 $results_criteria = array( 2073 Horde_Imap_Client::SEARCH_RESULTS_COUNT => 'COUNT', 2074 Horde_Imap_Client::SEARCH_RESULTS_MATCH => 'ALL', 2075 Horde_Imap_Client::SEARCH_RESULTS_MAX => 'MAX', 2076 Horde_Imap_Client::SEARCH_RESULTS_MIN => 'MIN', 2077 Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY => 'RELEVANCY', 2078 Horde_Imap_Client::SEARCH_RESULTS_SAVE => 'SAVE' 2079 ); 2080 2081 // Check if the server supports sorting (RFC 5256). 2082 $esearch = $return_sort = $server_seq_sort = $server_sort = false; 2083 if (!empty($options['sort'])) { 2084 /* Make sure sort options are correct. If not, default to no 2085 * sort. */ 2086 if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) { 2087 unset($options['sort']); 2088 } else { 2089 $return_sort = true; 2090 2091 if ($server_sort = $this->queryCapability('SORT')) { 2092 /* Make sure server supports DISPLAYFROM & DISPLAYTO. */ 2093 $server_sort = 2094 !array_intersect($options['sort'], array(Horde_Imap_Client::SORT_DISPLAYFROM, Horde_Imap_Client::SORT_DISPLAYTO)) || 2095 (is_array($server_sort) && 2096 in_array('DISPLAY', $server_sort)); 2097 } 2098 2099 /* If doing a sequence sort, need to do this on the client 2100 * side. */ 2101 if ($server_sort && 2102 in_array(Horde_Imap_Client::SORT_SEQUENCE, $options['sort'])) { 2103 $server_sort = false; 2104 2105 /* Optimization: If doing only a sequence sort, just do a 2106 * simple search and sort UIDs/sequences on client side. */ 2107 switch (count($options['sort'])) { 2108 case 1: 2109 $server_seq_sort = true; 2110 break; 2111 2112 case 2: 2113 $server_seq_sort = (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE); 2114 break; 2115 } 2116 } 2117 } 2118 } 2119 2120 $charset = is_null($options['_query']['charset']) 2121 ? 'US-ASCII' 2122 : $options['_query']['charset']; 2123 $partial = false; 2124 2125 if ($server_sort) { 2126 $cmd = $this->_command( 2127 empty($options['sequence']) ? 'UID SORT' : 'SORT' 2128 ); 2129 $results = array(); 2130 2131 // Use ESEARCH (RFC 4466) response if server supports. 2132 $esearch = false; 2133 2134 // Check for ESORT capability (RFC 5267) 2135 if ($this->queryCapability('ESORT')) { 2136 foreach ($options['results'] as $val) { 2137 if (isset($results_criteria[$val]) && 2138 ($val != Horde_Imap_Client::SEARCH_RESULTS_SAVE)) { 2139 $results[] = $results_criteria[$val]; 2140 } 2141 } 2142 $esearch = true; 2143 } 2144 2145 // Add PARTIAL limiting (RFC 5267 [4.4]) 2146 if ((!$esearch || !empty($options['partial'])) && 2147 ($cap = $this->queryCapability('CONTEXT')) && 2148 in_array('SORT', $cap)) { 2149 /* RFC 5267 indicates RFC 4466 ESEARCH-like support, 2150 * notwithstanding "real" RFC 4731 support. */ 2151 $esearch = true; 2152 2153 if (!empty($options['partial'])) { 2154 /* Can't have both ALL and PARTIAL returns. */ 2155 $results = array_diff($results, array('ALL')); 2156 2157 $results[] = 'PARTIAL'; 2158 $results[] = $options['partial']; 2159 $partial = true; 2160 } 2161 } 2162 2163 if ($esearch && empty($this->_init['noesearch'])) { 2164 $cmd->add(array( 2165 'RETURN', 2166 new Horde_Imap_Client_Data_Format_List($results) 2167 )); 2168 } 2169 2170 $tmp = new Horde_Imap_Client_Data_Format_List(); 2171 foreach ($options['sort'] as $val) { 2172 if (isset($sort_criteria[$val])) { 2173 $tmp->add($sort_criteria[$val]); 2174 } 2175 } 2176 $cmd->add($tmp); 2177 2178 // Charset is mandatory for SORT (RFC 5256 [3]). 2179 $cmd->add($charset); 2180 } else { 2181 $cmd = $this->_command( 2182 empty($options['sequence']) ? 'UID SEARCH' : 'SEARCH' 2183 ); 2184 $esearch = false; 2185 $results = array(); 2186 2187 // Check if the server supports ESEARCH (RFC 4731). 2188 if ($this->queryCapability('ESEARCH')) { 2189 foreach ($options['results'] as $val) { 2190 if (isset($results_criteria[$val])) { 2191 $results[] = $results_criteria[$val]; 2192 } 2193 } 2194 $esearch = true; 2195 } 2196 2197 // Add PARTIAL limiting (RFC 5267 [4.4]). 2198 if ((!$esearch || !empty($options['partial'])) && 2199 ($cap = $this->queryCapability('CONTEXT')) && 2200 in_array('SEARCH', $cap)) { 2201 /* RFC 5267 indicates RFC 4466 ESEARCH-like support, 2202 * notwithstanding "real" RFC 4731 support. */ 2203 $esearch = true; 2204 2205 if (!empty($options['partial'])) { 2206 // Can't have both ALL and PARTIAL returns. 2207 $results = array_diff($results, array('ALL')); 2208 2209 $results[] = 'PARTIAL'; 2210 $results[] = $options['partial']; 2211 $partial = true; 2212 } 2213 } 2214 2215 if ($esearch && empty($this->_init['noesearch'])) { 2216 // Always use ESEARCH if available because it returns results 2217 // in a more compact sequence-set list 2218 $cmd->add(array( 2219 'RETURN', 2220 new Horde_Imap_Client_Data_Format_List($results) 2221 )); 2222 } 2223 2224 // Charset is optional for SEARCH (RFC 3501 [6.4.4]). 2225 if ($charset != 'US-ASCII') { 2226 $cmd->add(array( 2227 'CHARSET', 2228 $options['_query']['charset'] 2229 )); 2230 } 2231 } 2232 2233 $cmd->add($options['_query']['query'], true); 2234 2235 $pipeline = $this->_pipeline($cmd); 2236 $pipeline->data['esearchresp'] = array(); 2237 $er = &$pipeline->data['esearchresp']; 2238 $pipeline->data['searchresp'] = $this->getIdsOb(array(), !empty($options['sequence'])); 2239 $sr = &$pipeline->data['searchresp']; 2240 2241 try { 2242 $resp = $this->_sendCmd($pipeline); 2243 } catch (Horde_Imap_Client_Exception $e) { 2244 if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) && 2245 ($e->status === Horde_Imap_Client_Interaction_Server::NO) && 2246 ($charset != 'US-ASCII')) { 2247 /* RFC 3501 [6.4.4]: BADCHARSET response code is only a 2248 * SHOULD return. If it doesn't exist, need to check for 2249 * command status of 'NO'. List of supported charsets in 2250 * the BADCHARSET response has already been parsed and stored 2251 * at this point. */ 2252 $s_charset = $this->_init['s_charset']; 2253 $s_charset[$charset] = false; 2254 $this->_setInit('s_charset', $s_charset); 2255 $e->setCode(Horde_Imap_Client_Exception::BADCHARSET); 2256 } 2257 2258 if (empty($this->_temp['search_retry'])) { 2259 $this->_temp['search_retry'] = true; 2260 2261 /* Bug #9842: Workaround broken Cyrus servers (as of 2262 * 2.4.7). */ 2263 if ($esearch && ($charset != 'US-ASCII')) { 2264 $this->_unsetCapability('ESEARCH'); 2265 $this->_setInit('noesearch', true); 2266 2267 try { 2268 return $this->_search($query, $options); 2269 } catch (Horde_Imap_Client_Exception $e) {} 2270 } 2271 2272 /* Try to convert charset. */ 2273 if (($e->getCode() === Horde_Imap_Client_Exception::BADCHARSET) && 2274 ($charset != 'US-ASCII')) { 2275 foreach (array_merge(array_keys(array_filter($this->_init['s_charset'])), array('US-ASCII')) as $val) { 2276 $this->_temp['search_retry'] = 1; 2277 $new_query = clone($query); 2278 try { 2279 $new_query->charset($val); 2280 $options['_query'] = $new_query->build($this->capability()); 2281 return $this->_search($new_query, $options); 2282 } catch (Horde_Imap_Client_Exception $e) {} 2283 } 2284 } 2285 2286 unset($this->_temp['search_retry']); 2287 } 2288 2289 throw $e; 2290 } 2291 2292 if ($return_sort && !$server_sort) { 2293 if ($server_seq_sort) { 2294 $sr->sort(); 2295 if (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE) { 2296 $sr->reverse(); 2297 } 2298 } else { 2299 if (!isset($this->_temp['clientsort'])) { 2300 $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this); 2301 } 2302 $sr = $this->getIdsOb($this->_temp['clientsort']->clientSort($sr, $options), !empty($options['sequence'])); 2303 } 2304 } 2305 2306 if (!$partial && !empty($options['partial'])) { 2307 $partial = $this->getIdsOb($options['partial'], true); 2308 $min = $partial->min - 1; 2309 2310 $sr->sort(); 2311 $sr = $this->getIdsOb( 2312 array_slice($sr->ids(), $min, $partial->max - $min), 2313 !empty($options['sequence']) 2314 ); 2315 } 2316 2317 $ret = array(); 2318 foreach ($options['results'] as $val) { 2319 switch ($val) { 2320 case Horde_Imap_Client::SEARCH_RESULTS_COUNT: 2321 $ret['count'] = ($esearch && !$partial) 2322 ? $er['count'] 2323 : count($sr); 2324 break; 2325 2326 case Horde_Imap_Client::SEARCH_RESULTS_MATCH: 2327 $ret['match'] = $sr; 2328 break; 2329 2330 case Horde_Imap_Client::SEARCH_RESULTS_MAX: 2331 $ret['max'] = $esearch 2332 ? (!$partial && isset($er['max']) ? $er['max'] : null) 2333 : (count($sr) ? max($sr->ids) : null); 2334 break; 2335 2336 case Horde_Imap_Client::SEARCH_RESULTS_MIN: 2337 $ret['min'] = $esearch 2338 ? (!$partial && isset($er['min']) ? $er['min'] : null) 2339 : (count($sr) ? min($sr->ids) : null); 2340 break; 2341 2342 case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY: 2343 $ret['relevancy'] = ($esearch && isset($er['relevancy'])) ? $er['relevancy'] : array(); 2344 break; 2345 2346 case Horde_Imap_Client::SEARCH_RESULTS_SAVE: 2347 $this->_temp['search_save'] = $ret['save'] = $esearch ? empty($resp->data['searchnotsaved']) : false; 2348 break; 2349 } 2350 } 2351 2352 // Add modseq data, if needed. 2353 if (!empty($er['modseq'])) { 2354 $ret['modseq'] = $er['modseq']; 2355 } 2356 2357 unset($this->_temp['search_retry']); 2358 2359 /* Check for EXPUNGEISSUED (RFC 2180 [4.3]/RFC 5530 [3]). */ 2360 if (!empty($resp->data['expungeissued'])) { 2361 $this->noop(); 2362 } 2363 2364 return $ret; 2365 } 2366 2367 /** 2368 * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3]; 2369 * RFC 5256 [4]; RFC 5267 [3]). 2370 * 2371 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2372 * object. 2373 * @param array $data A list of IDs (message sequence numbers or UIDs). 2374 */ 2375 protected function _parseSearch( 2376 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2377 $data 2378 ) 2379 { 2380 /* More than one search response may be sent. */ 2381 $pipeline->data['searchresp']->add($data); 2382 } 2383 2384 /** 2385 * Parse an ESEARCH response (RFC 4466 [2.6.2]) 2386 * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28 2387 * 2388 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2389 * object. 2390 * @param Horde_Imap_Client_Tokenize $data The server response. 2391 */ 2392 protected function _parseEsearch( 2393 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2394 Horde_Imap_Client_Tokenize $data 2395 ) 2396 { 2397 // Ignore search correlator information 2398 if ($data->next() === true) { 2399 $data->flushIterator(false); 2400 } 2401 2402 // Ignore UID tag 2403 $current = $data->next(); 2404 if (strtoupper($current) === 'UID') { 2405 $current = $data->next(); 2406 } 2407 2408 do { 2409 $val = $data->next(); 2410 $tag = strtoupper($current); 2411 2412 switch ($tag) { 2413 case 'ALL': 2414 $this->_parseSearch($pipeline, $val); 2415 break; 2416 2417 case 'COUNT': 2418 case 'MAX': 2419 case 'MIN': 2420 case 'MODSEQ': 2421 case 'RELEVANCY': 2422 $pipeline->data['esearchresp'][strtolower($tag)] = $val; 2423 break; 2424 2425 case 'PARTIAL': 2426 // RFC 5267 [4.4] 2427 $partial = $val->flushIterator(); 2428 $this->_parseSearch($pipeline, end($partial)); 2429 break; 2430 } 2431 } while (($current = $data->next()) !== false); 2432 } 2433 2434 /** 2435 */ 2436 protected function _setComparator($comparator) 2437 { 2438 $cmd = $this->_command('COMPARATOR'); 2439 foreach ($comparator as $val) { 2440 $cmd->add(new Horde_Imap_Client_Data_Format_Astring($val)); 2441 } 2442 $this->_sendCmd($cmd); 2443 } 2444 2445 /** 2446 */ 2447 protected function _getComparator() 2448 { 2449 $resp = $this->_sendCmd($this->_command('COMPARATOR')); 2450 2451 return isset($resp->data['comparator']) 2452 ? $resp->data['comparator'] 2453 : null; 2454 } 2455 2456 /** 2457 * Parse a COMPARATOR response (RFC 5255 [4.8]) 2458 * 2459 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2460 * object. 2461 * @param Horde_Imap_Client_Tokenize $data The server response. 2462 */ 2463 protected function _parseComparator( 2464 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2465 $data 2466 ) 2467 { 2468 $pipeline->data['comparator'] = $data->next(); 2469 // Ignore optional matching comparator list 2470 } 2471 2472 /** 2473 * @throws Horde_Imap_Client_Exception_NoSupportExtension 2474 */ 2475 protected function _thread($options) 2476 { 2477 $thread_criteria = array( 2478 Horde_Imap_Client::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT', 2479 Horde_Imap_Client::THREAD_REFERENCES => 'REFERENCES', 2480 Horde_Imap_Client::THREAD_REFS => 'REFS' 2481 ); 2482 2483 $tsort = (isset($options['criteria'])) 2484 ? (is_string($options['criteria']) ? strtoupper($options['criteria']) : $thread_criteria[$options['criteria']]) 2485 : 'ORDEREDSUBJECT'; 2486 2487 $cap = $this->queryCapability('THREAD'); 2488 if (!$cap || !in_array($tsort, $cap)) { 2489 switch ($tsort) { 2490 case 'ORDEREDSUBJECT': 2491 if (empty($options['search'])) { 2492 $ids = $this->getIdsOb(Horde_Imap_Client_Ids::ALL, !empty($options['sequence'])); 2493 } else { 2494 $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence']))); 2495 $ids = $search_res['match']; 2496 } 2497 2498 /* Do client-side ORDEREDSUBJECT threading. */ 2499 $query = new Horde_Imap_Client_Fetch_Query(); 2500 $query->envelope(); 2501 $query->imapDate(); 2502 2503 $fetch_res = $this->fetch($this->_selected, $query, array( 2504 'ids' => $ids 2505 )); 2506 2507 if (!isset($this->_temp['clientsort'])) { 2508 $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this); 2509 } 2510 return $this->_temp['clientsort']->threadOrderedSubject($fetch_res, empty($options['sequence'])); 2511 2512 case 'REFERENCES': 2513 case 'REFS': 2514 throw new Horde_Imap_Client_Exception_NoSupportExtension( 2515 'THREAD', 2516 sprintf('Server does not support "%s" thread sort.', $tsort) 2517 ); 2518 } 2519 } 2520 2521 $cmd = $this->_command( 2522 empty($options['sequence']) ? 'UID THREAD' : 'THREAD' 2523 )->add($tsort); 2524 2525 if (empty($options['search'])) { 2526 $cmd->add(array( 2527 'US-ASCII', 2528 'ALL' 2529 )); 2530 } else { 2531 $search_query = $options['search']->build(); 2532 $cmd->add(is_null($search_query['charset']) ? 'US-ASCII' : $search_query['charset']); 2533 $cmd->add($search_query['query'], true); 2534 } 2535 2536 return new Horde_Imap_Client_Data_Thread( 2537 $this->_sendCmd($cmd)->data['threadparse'], 2538 empty($options['sequence']) ? 'uid' : 'sequence' 2539 ); 2540 } 2541 2542 /** 2543 * Parse a THREAD response (RFC 5256 [4]). 2544 * 2545 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2546 * object. 2547 * @param Horde_Imap_Client_Tokenize $data Thread data. 2548 */ 2549 protected function _parseThread( 2550 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2551 Horde_Imap_Client_Tokenize $data 2552 ) 2553 { 2554 $out = array(); 2555 2556 while ($data->next() !== false) { 2557 $thread = array(); 2558 $this->_parseThreadLevel($thread, $data); 2559 $out[] = $thread; 2560 } 2561 2562 $pipeline->data['threadparse'] = $out; 2563 } 2564 2565 /** 2566 * Parse a level of a THREAD response (RFC 5256 [4]). 2567 * 2568 * @param array $thread Results. 2569 * @param Horde_Imap_Client_Tokenize $data Thread data. 2570 * @param integer $level The current tree level. 2571 */ 2572 protected function _parseThreadLevel(&$thread, 2573 Horde_Imap_Client_Tokenize $data, 2574 $level = 0) 2575 { 2576 while (($curr = $data->next()) !== false) { 2577 if ($curr === true) { 2578 $this->_parseThreadLevel($thread, $data, $level); 2579 } elseif (!is_bool($curr)) { 2580 $thread[$curr] = $level++; 2581 } 2582 } 2583 } 2584 2585 /** 2586 */ 2587 protected function _fetch(Horde_Imap_Client_Fetch_Results $results, 2588 $queries) 2589 { 2590 $pipeline = $this->_pipeline(); 2591 $pipeline->data['fetch_lookup'] = array(); 2592 $pipeline->data['fetch_followup'] = array(); 2593 2594 foreach ($queries as $options) { 2595 $this->_fetchCmd($pipeline, $options); 2596 $sequence = $options['ids']->sequence; 2597 } 2598 2599 try { 2600 $resp = $this->_sendCmd($pipeline); 2601 2602 /* Check for EXPUNGEISSUED (RFC 2180 [4.1]/RFC 5530 [3]). */ 2603 if (!empty($resp->data['expungeissued'])) { 2604 $this->noop(); 2605 } 2606 2607 foreach ($resp->fetch as $k => $v) { 2608 $results->get($sequence ? $k : $v->getUid())->merge($v); 2609 } 2610 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 2611 if ($e->status === Horde_Imap_Client_Interaction_Server::NO) { 2612 if ($e->getCode() === $e::UNKNOWNCTE) { 2613 /* UNKNOWN-CTE error. Redo the query without the BINARY 2614 * elements. */ 2615 $bq = $pipeline->data['binaryquery']; 2616 2617 foreach ($queries as $val) { 2618 foreach ($bq as $key2 => $val2) { 2619 unset($val2['decode']); 2620 $val['_query']->bodyPart($key2, $val2); 2621 $val['_query']->remove(Horde_Imap_Client::FETCH_BODYPARTSIZE, $key2); 2622 } 2623 $pipeline->data['fetch_followup'][] = $val; 2624 } 2625 } elseif ($sequence) { 2626 /* A NO response, when coupled with a sequence FETCH, most 2627 * likely means that messages were expunged. (RFC 2180 2628 * [4.1]) */ 2629 $this->noop(); 2630 } 2631 } 2632 } catch (Exception $e) { 2633 // For any other error, ignore the Exception - fetch() is nice in 2634 // that the return value explicitly handles missing data for any 2635 // given message. 2636 } 2637 2638 if (!empty($pipeline->data['fetch_followup'])) { 2639 $this->_fetch($results, $pipeline->data['fetch_followup']); 2640 } 2641 } 2642 2643 /** 2644 * Add a FETCH command to the given pipeline. 2645 * 2646 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2647 * object. 2648 * @param array $options Fetch query 2649 * options 2650 */ 2651 protected function _fetchCmd( 2652 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2653 $options 2654 ) 2655 { 2656 $fetch = new Horde_Imap_Client_Data_Format_List(); 2657 $sequence = $options['ids']->sequence; 2658 2659 /* Build an IMAP4rev1 compliant FETCH query. We handle the following 2660 * criteria: 2661 * BINARY[.PEEK][<section #>]<<partial>> (RFC 3516) 2662 * see BODY[] response 2663 * BINARY.SIZE[<section #>] (RFC 3516) 2664 * BODY[.PEEK][<section>]<<partial>> 2665 * <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, 2666 * TEXT, empty 2667 * <<partial>> = 0.# (# of bytes) 2668 * BODYSTRUCTURE 2669 * ENVELOPE 2670 * FLAGS 2671 * INTERNALDATE 2672 * MODSEQ (RFC 7162) 2673 * RFC822.SIZE 2674 * UID 2675 * 2676 * No need to support these (can be built from other queries): 2677 * =========================================================== 2678 * ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE) 2679 * BODY => Use BODYSTRUCTURE instead 2680 * FAST macro => (FLAGS INTERNALDATE RFC822.SIZE) 2681 * FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) 2682 * RFC822 => BODY[] 2683 * RFC822.HEADER => BODY[HEADER] 2684 * RFC822.TEXT => BODY[TEXT] 2685 */ 2686 2687 foreach ($options['_query'] as $type => $c_val) { 2688 switch ($type) { 2689 case Horde_Imap_Client::FETCH_STRUCTURE: 2690 $fetch->add('BODYSTRUCTURE'); 2691 break; 2692 2693 case Horde_Imap_Client::FETCH_FULLMSG: 2694 if (empty($c_val['peek'])) { 2695 $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE); 2696 } 2697 $fetch->add( 2698 'BODY' . 2699 (!empty($c_val['peek']) ? '.PEEK' : '') . 2700 '[]' . 2701 $this->_partialAtom($c_val) 2702 ); 2703 break; 2704 2705 case Horde_Imap_Client::FETCH_HEADERTEXT: 2706 case Horde_Imap_Client::FETCH_BODYTEXT: 2707 case Horde_Imap_Client::FETCH_MIMEHEADER: 2708 case Horde_Imap_Client::FETCH_BODYPART: 2709 case Horde_Imap_Client::FETCH_HEADERS: 2710 foreach ($c_val as $key => $val) { 2711 $cmd = ($key == 0) 2712 ? '' 2713 : $key . '.'; 2714 $main_cmd = 'BODY'; 2715 2716 switch ($type) { 2717 case Horde_Imap_Client::FETCH_HEADERTEXT: 2718 $cmd .= 'HEADER'; 2719 break; 2720 2721 case Horde_Imap_Client::FETCH_BODYTEXT: 2722 $cmd .= 'TEXT'; 2723 break; 2724 2725 case Horde_Imap_Client::FETCH_MIMEHEADER: 2726 $cmd .= 'MIME'; 2727 break; 2728 2729 case Horde_Imap_Client::FETCH_BODYPART: 2730 // Remove the last dot from the string. 2731 $cmd = substr($cmd, 0, -1); 2732 2733 if (!empty($val['decode']) && 2734 $this->queryCapability('BINARY')) { 2735 $main_cmd = 'BINARY'; 2736 $pipeline->data['binaryquery'][$key] = $val; 2737 } 2738 break; 2739 2740 case Horde_Imap_Client::FETCH_HEADERS: 2741 $cmd .= 'HEADER.FIELDS'; 2742 if (!empty($val['notsearch'])) { 2743 $cmd .= '.NOT'; 2744 } 2745 $cmd .= ' (' . implode(' ', array_map('strtoupper', $val['headers'])) . ')'; 2746 2747 // Maintain a command -> label lookup so we can put 2748 // the results in the proper location. 2749 $pipeline->data['fetch_lookup'][$cmd] = $key; 2750 } 2751 2752 if (empty($val['peek'])) { 2753 $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE); 2754 } 2755 2756 $fetch->add( 2757 $main_cmd . 2758 (!empty($val['peek']) ? '.PEEK' : '') . 2759 '[' . $cmd . ']' . 2760 $this->_partialAtom($val) 2761 ); 2762 } 2763 break; 2764 2765 case Horde_Imap_Client::FETCH_BODYPARTSIZE: 2766 if ($this->queryCapability('BINARY')) { 2767 foreach ($c_val as $val) { 2768 $fetch->add('BINARY.SIZE[' . $val . ']'); 2769 } 2770 } 2771 break; 2772 2773 case Horde_Imap_Client::FETCH_ENVELOPE: 2774 $fetch->add('ENVELOPE'); 2775 break; 2776 2777 case Horde_Imap_Client::FETCH_FLAGS: 2778 $fetch->add('FLAGS'); 2779 break; 2780 2781 case Horde_Imap_Client::FETCH_IMAPDATE: 2782 $fetch->add('INTERNALDATE'); 2783 break; 2784 2785 case Horde_Imap_Client::FETCH_SIZE: 2786 $fetch->add('RFC822.SIZE'); 2787 break; 2788 2789 case Horde_Imap_Client::FETCH_UID: 2790 /* A UID FETCH will always return UID information (RFC 3501 2791 * [6.4.8]). Don't add to query as it just creates a longer 2792 * FETCH command. */ 2793 if ($sequence || (count($options['_query']) === 1)) { 2794 $fetch->add('UID'); 2795 } 2796 break; 2797 2798 case Horde_Imap_Client::FETCH_SEQ: 2799 // Nothing we need to add to fetch request unless sequence is 2800 // the only criteria. 2801 if (count($options['_query']) === 1) { 2802 $fetch->add('UID'); 2803 } 2804 break; 2805 2806 case Horde_Imap_Client::FETCH_MODSEQ: 2807 /* The 'changedsince' modifier implicitly adds the MODSEQ 2808 * FETCH item (RFC 7162 [3.1.4.1]). Don't add to query as it 2809 * just creates a longer FETCH command. */ 2810 if (empty($options['changedsince'])) { 2811 $fetch->add('MODSEQ'); 2812 } 2813 break; 2814 } 2815 } 2816 2817 /* Add changedsince parameters. */ 2818 if (empty($options['changedsince'])) { 2819 $fetch_cmd = $fetch; 2820 } else { 2821 /* We might just want the list of UIDs changed since a given 2822 * modseq. In that case, we don't have any other FETCH attributes, 2823 * but RFC 3501 requires at least one specified attribute. */ 2824 $fetch_cmd = array( 2825 count($fetch) 2826 ? $fetch 2827 : new Horde_Imap_Client_Data_Format_List('UID'), 2828 new Horde_Imap_Client_Data_Format_List(array( 2829 'CHANGEDSINCE', 2830 new Horde_Imap_Client_Data_Format_Number($options['changedsince']) 2831 )) 2832 ); 2833 } 2834 2835 /* The FETCH command should be the only command issued by this library 2836 * that should ever approach this limit. 2837 * @todo Move this check to a more centralized location (_command()?). 2838 * For simplification, assume that the UID list is the limiting factor 2839 * and split this list at a sequence comma delimiter if it exceeds 2840 * the character limit. */ 2841 foreach ($options['ids']->split($this->_init['cmdlength']) as $val) { 2842 $cmd = $this->_command( 2843 $sequence ? 'FETCH' : 'UID FETCH' 2844 )->add(array( 2845 $val, 2846 $fetch_cmd 2847 )); 2848 $pipeline->add($cmd); 2849 } 2850 } 2851 2852 /** 2853 * Add a partial atom to an IMAP command based on the criteria options. 2854 * 2855 * @param array $opts Criteria options. 2856 * 2857 * @return string The partial atom. 2858 */ 2859 protected function _partialAtom($opts) 2860 { 2861 if (!empty($opts['length'])) { 2862 return '<' . (empty($opts['start']) ? 0 : intval($opts['start'])) . '.' . intval($opts['length']) . '>'; 2863 } 2864 2865 return empty($opts['start']) 2866 ? '' 2867 : ('<' . intval($opts['start']) . '>'); 2868 } 2869 2870 /** 2871 * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur 2872 * due to a FETCH command, or due to a change in a message's state (i.e. 2873 * the flags change). 2874 * 2875 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2876 * object. 2877 * @param integer $id The message sequence number. 2878 * @param Horde_Imap_Client_Tokenize $data The server response. 2879 */ 2880 protected function _parseFetch( 2881 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2882 $id, 2883 Horde_Imap_Client_Tokenize $data 2884 ) 2885 { 2886 if ($data->next() !== true) { 2887 return; 2888 } 2889 2890 $ob = $pipeline->fetch->get($id); 2891 $ob->setSeq($id); 2892 2893 $flags = $modseq = $uid = false; 2894 2895 while (($tag = $data->next()) !== false) { 2896 $tag = strtoupper($tag); 2897 2898 switch ($tag) { 2899 case 'BODYSTRUCTURE': 2900 $data->next(); 2901 $structure = $this->_parseBodystructure($data); 2902 $structure->buildMimeIds(); 2903 $ob->setStructure($structure); 2904 break; 2905 2906 case 'ENVELOPE': 2907 $data->next(); 2908 $ob->setEnvelope($this->_parseEnvelope($data)); 2909 break; 2910 2911 case 'FLAGS': 2912 $data->next(); 2913 $ob->setFlags($data->flushIterator()); 2914 $flags = true; 2915 break; 2916 2917 case 'INTERNALDATE': 2918 $ob->setImapDate($data->next()); 2919 break; 2920 2921 case 'RFC822.SIZE': 2922 $ob->setSize($data->next()); 2923 break; 2924 2925 case 'UID': 2926 $ob->setUid($data->next()); 2927 $uid = true; 2928 break; 2929 2930 case 'MODSEQ': 2931 $data->next(); 2932 $modseq = $data->next(); 2933 $data->next(); 2934 2935 /* MODSEQ must be greater than 0, so do sanity checking. */ 2936 if ($modseq > 0) { 2937 $ob->setModSeq($modseq); 2938 2939 /* Store MODSEQ value. It may be used as the highestmodseq 2940 * once a tagged response is received (RFC 7162 [6]). */ 2941 $pipeline->data['modseqs'][] = $modseq; 2942 } 2943 break; 2944 2945 default: 2946 // Catch BODY[*]<#> responses 2947 if (strpos($tag, 'BODY[') === 0) { 2948 // Remove the beginning 'BODY[' 2949 $tag = substr($tag, 5); 2950 2951 // BODY[HEADER.FIELDS] request 2952 if (!empty($pipeline->data['fetch_lookup']) && 2953 (strpos($tag, 'HEADER.FIELDS') !== false)) { 2954 $data->next(); 2955 $sig = $tag . ' (' . implode(' ', array_map('strtoupper', $data->flushIterator())) . ')'; 2956 2957 // Ignore the trailing bracket 2958 $data->next(); 2959 2960 $ob->setHeaders($pipeline->data['fetch_lookup'][$sig], $data->next()); 2961 } else { 2962 // Remove trailing bracket and octet start info 2963 $tag = substr($tag, 0, strrpos($tag, ']')); 2964 2965 if (!strlen($tag)) { 2966 // BODY[] request 2967 if (!is_null($tmp = $data->next())) { 2968 $ob->setFullMsg($tmp); 2969 } 2970 } elseif (is_numeric(substr($tag, -1))) { 2971 // BODY[MIMEID] request 2972 if (!is_null($tmp = $data->next())) { 2973 $ob->setBodyPart($tag, $tmp); 2974 } 2975 } else { 2976 // BODY[HEADER|TEXT|MIME] request 2977 if (($last_dot = strrpos($tag, '.')) === false) { 2978 $mime_id = 0; 2979 } else { 2980 $mime_id = substr($tag, 0, $last_dot); 2981 $tag = substr($tag, $last_dot + 1); 2982 } 2983 2984 if (!is_null($tmp = $data->next())) { 2985 switch ($tag) { 2986 case 'HEADER': 2987 $ob->setHeaderText($mime_id, $tmp); 2988 break; 2989 2990 case 'TEXT': 2991 $ob->setBodyText($mime_id, $tmp); 2992 break; 2993 2994 case 'MIME': 2995 $ob->setMimeHeader($mime_id, $tmp); 2996 break; 2997 } 2998 } 2999 } 3000 } 3001 } elseif (strpos($tag, 'BINARY[') === 0) { 3002 // Catch BINARY[*]<#> responses 3003 // Remove the beginning 'BINARY[' and the trailing bracket 3004 // and octet start info 3005 $tag = substr($tag, 7, strrpos($tag, ']') - 7); 3006 $body = $data->next(); 3007 3008 if (is_null($body)) { 3009 /* Dovecot bug (as of 2.2.12): binary fetch of body 3010 * part may fail with NIL return if decoding failed on 3011 * server. Try again with non-decoded body. */ 3012 $bq = $pipeline->data['binaryquery'][$tag]; 3013 unset($bq['decode']); 3014 3015 $query = new Horde_Imap_Client_Fetch_Query(); 3016 $query->bodyPart($tag, $bq); 3017 3018 $qids = ($quid = $ob->getUid()) 3019 ? new Horde_Imap_Client_Ids($quid) 3020 : new Horde_Imap_Client_Ids($id, true); 3021 3022 $pipeline->data['fetch_followup'][] = array( 3023 '_query' => $query, 3024 'ids' => $qids 3025 ); 3026 } else { 3027 $ob->setBodyPart( 3028 $tag, 3029 $body, 3030 empty($this->_temp['literal8']) ? '8bit' : 'binary' 3031 ); 3032 } 3033 } elseif (strpos($tag, 'BINARY.SIZE[') === 0) { 3034 // Catch BINARY.SIZE[*] responses 3035 // Remove the beginning 'BINARY.SIZE[' and the trailing 3036 // bracket and octet start info 3037 $tag = substr($tag, 12, strrpos($tag, ']') - 12); 3038 $ob->setBodyPartSize($tag, $data->next()); 3039 } 3040 break; 3041 } 3042 } 3043 3044 /* MODSEQ issue: Oh joy. Per RFC 5162 (see Errata #1807), FETCH FLAGS 3045 * responses are NOT required to provide UID information, even if 3046 * QRESYNC is explicitly enabled. Caveat: the FLAGS information 3047 * returned during a SELECT/EXAMINE MUST contain UIDs so we are OK 3048 * there. 3049 * The good news: all decent IMAP servers (Cyrus, Dovecot) will always 3050 * provide UID information, so this is not normally an issue. 3051 * The bad news: spec-wise, this behavior cannot be 100% guaranteed. 3052 * Compromise: We will watch for a FLAGS response with a MODSEQ and 3053 * check if a UID exists also. If not, put the sequence number in a 3054 * queue - it is possible the UID information may appear later in an 3055 * untagged response. When the command is over, double check to make 3056 * sure there are none of these MODSEQ/FLAGS that are still UID-less. 3057 * In the (rare) event that there is, don't cache anything and 3058 * immediately close the mailbox: flags will be correctly sync'd next 3059 * mailbox open so we only lose a bit of caching efficiency. 3060 * Otherwise, we could end up with an inconsistent cached state. 3061 * This Errata has been fixed in 7162 [3.2.4]. */ 3062 if ($flags && $modseq && !$uid) { 3063 $pipeline->data['modseqs_nouid'][] = $id; 3064 } 3065 } 3066 3067 /** 3068 * Recursively parse BODYSTRUCTURE data from a FETCH return (see 3069 * RFC 3501 [7.4.2]). 3070 * 3071 * @param Horde_Imap_Client_Tokenize $data Data returned from the server. 3072 * 3073 * @return Horde_Mime_Part Mime part object. 3074 */ 3075 protected function _parseBodystructure(Horde_Imap_Client_Tokenize $data) 3076 { 3077 $ob = new Horde_Mime_Part(); 3078 3079 // If index 0 is an array, this is a multipart part. 3080 if (($entry = $data->next()) === true) { 3081 do { 3082 $ob->addPart($this->_parseBodystructure($data)); 3083 } while (($entry = $data->next()) === true); 3084 3085 // The subpart type. 3086 $ob->setType('multipart/' . $entry); 3087 3088 // After the subtype is further extension information. This 3089 // information MAY appear for BODYSTRUCTURE requests. 3090 3091 // This is parameter information. 3092 if (($tmp = $data->next()) === false) { 3093 return $ob; 3094 } elseif ($tmp === true) { 3095 foreach ($this->_parseStructureParams($data, 'content-type') as $key => $val) { 3096 $ob->setContentTypeParameter($key, $val); 3097 } 3098 } 3099 } else { 3100 $ob->setType($entry . '/' . $data->next()); 3101 3102 if ($data->next() === true) { 3103 foreach ($this->_parseStructureParams($data, 'content-type') as $key => $val) { 3104 $ob->setContentTypeParameter($key, $val); 3105 } 3106 } 3107 3108 if (!is_null($tmp = $data->next())) { 3109 $ob->setContentId($tmp); 3110 } 3111 3112 if (!is_null($tmp = $data->next())) { 3113 $ob->setDescription(Horde_Mime::decode($tmp)); 3114 } 3115 3116 if (!is_null($tmp = $data->next())) { 3117 $ob->setTransferEncoding($tmp); 3118 } 3119 3120 $ob->setBytes($data->next()); 3121 3122 // If the type is 'message/rfc822' or 'text/*', several extra 3123 // fields are included 3124 switch ($ob->getPrimaryType()) { 3125 case 'message': 3126 if ($ob->getSubType() == 'rfc822') { 3127 if ($data->next() === true) { 3128 // Ignore: envelope 3129 $data->flushIterator(false); 3130 } 3131 if ($data->next() === true) { 3132 $ob->addPart($this->_parseBodystructure($data)); 3133 } 3134 $data->next(); // Ignore: lines 3135 } 3136 break; 3137 3138 case 'text': 3139 $data->next(); // Ignore: lines 3140 break; 3141 } 3142 3143 // After the subtype is further extension information. This 3144 // information MAY appear for BODYSTRUCTURE requests. 3145 3146 // Ignore: MD5 3147 if ($data->next() === false) { 3148 return $ob; 3149 } 3150 } 3151 3152 // This is disposition information 3153 if (($tmp = $data->next()) === false) { 3154 return $ob; 3155 } elseif ($tmp === true) { 3156 $ob->setDisposition($data->next()); 3157 3158 if ($data->next() === true) { 3159 foreach ($this->_parseStructureParams($data, 'content-disposition') as $key => $val) { 3160 $ob->setDispositionParameter($key, $val); 3161 } 3162 } 3163 $data->next(); 3164 } 3165 3166 // This is language information. It is either a single value or a list 3167 // of values. 3168 if (($tmp = $data->next()) === false) { 3169 return $ob; 3170 } elseif (!is_null($tmp)) { 3171 $ob->setLanguage(($tmp === true) ? $data->flushIterator() : $tmp); 3172 } 3173 3174 // Ignore location (RFC 2557) and consume closing paren. 3175 $data->flushIterator(false); 3176 3177 return $ob; 3178 } 3179 3180 /** 3181 * Helper function to parse a parameters-like tokenized array. 3182 * 3183 * @param mixed $data Message data. Either a Horde_Imap_Client_Tokenize 3184 * object or null. 3185 * @param string $type The header name. 3186 * 3187 * @return array The parameter array. 3188 */ 3189 protected function _parseStructureParams($data, $type) 3190 { 3191 $params = array(); 3192 3193 if (is_null($data)) { 3194 return $params; 3195 } 3196 3197 while (($name = $data->next()) !== false) { 3198 $params[strtolower($name)] = $data->next(); 3199 } 3200 3201 $ret = Horde_Mime::decodeParam($type, $params); 3202 3203 return $ret['params']; 3204 } 3205 3206 /** 3207 * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]). 3208 * 3209 * @param Horde_Imap_Client_Tokenize $data Data returned from the server. 3210 * 3211 * @return Horde_Imap_Client_Data_Envelope An envelope object. 3212 */ 3213 protected function _parseEnvelope(Horde_Imap_Client_Tokenize $data) 3214 { 3215 // 'route', the 2nd element, is deprecated by RFC 2822. 3216 $addr_structure = array( 3217 0 => 'personal', 3218 2 => 'mailbox', 3219 3 => 'host' 3220 ); 3221 $env_data = array( 3222 0 => 'date', 3223 1 => 'subject', 3224 2 => 'from', 3225 3 => 'sender', 3226 4 => 'reply_to', 3227 5 => 'to', 3228 6 => 'cc', 3229 7 => 'bcc', 3230 8 => 'in_reply_to', 3231 9 => 'message_id' 3232 ); 3233 3234 $addr_ob = new Horde_Mail_Rfc822_Address(); 3235 $env_addrs = $this->getParam('envelope_addrs'); 3236 $env_str = $this->getParam('envelope_string'); 3237 $key = 0; 3238 $ret = new Horde_Imap_Client_Data_Envelope(); 3239 3240 while (($val = $data->next()) !== false) { 3241 if (!isset($env_data[$key]) || is_null($val)) { 3242 ++$key; 3243 continue; 3244 } 3245 3246 if (is_string($val)) { 3247 // These entries are text fields. 3248 $ret->{$env_data[$key]} = substr($val, 0, $env_str); 3249 } else { 3250 // These entries are address structures. 3251 $group = null; 3252 $key2 = 0; 3253 $tmp = new Horde_Mail_Rfc822_List(); 3254 3255 while ($data->next() !== false) { 3256 $a_val = $data->flushIterator(); 3257 3258 // RFC 3501 [7.4.2]: Group entry when host is NIL. 3259 // Group end when mailbox is NIL; otherwise, this is 3260 // mailbox name. 3261 if (is_null($a_val[3])) { 3262 if (is_null($a_val[2])) { 3263 $group = null; 3264 } else { 3265 $group = new Horde_Mail_Rfc822_Group($a_val[2]); 3266 $tmp->add($group); 3267 } 3268 } else { 3269 $addr = clone $addr_ob; 3270 3271 foreach ($addr_structure as $add_key => $add_val) { 3272 if (!is_null($a_val[$add_key])) { 3273 $addr->$add_val = $a_val[$add_key]; 3274 } 3275 } 3276 3277 if ($group) { 3278 $group->addresses->add($addr); 3279 } else { 3280 $tmp->add($addr); 3281 } 3282 } 3283 3284 if (++$key2 >= $env_addrs) { 3285 $data->flushIterator(false); 3286 break; 3287 } 3288 } 3289 3290 $ret->{$env_data[$key]} = $tmp; 3291 } 3292 3293 ++$key; 3294 } 3295 3296 return $ret; 3297 } 3298 3299 /** 3300 */ 3301 protected function _vanished($modseq, Horde_Imap_Client_Ids $ids) 3302 { 3303 $pipeline = $this->_pipeline( 3304 $this->_command('UID FETCH')->add(array( 3305 strval($ids), 3306 'UID', 3307 new Horde_Imap_Client_Data_Format_List(array( 3308 'VANISHED', 3309 'CHANGEDSINCE', 3310 new Horde_Imap_Client_Data_Format_Number($modseq) 3311 )) 3312 )) 3313 ); 3314 $pipeline->data['vanished'] = $this->getIdsOb(); 3315 3316 return $this->_sendCmd($pipeline)->data['vanished']; 3317 } 3318 3319 /** 3320 */ 3321 protected function _store($options) 3322 { 3323 $pipeline = $this->_storeCmd($options); 3324 $pipeline->data['modified'] = $this->getIdsOb(); 3325 3326 try { 3327 $resp = $this->_sendCmd($pipeline); 3328 3329 /* Check for EXPUNGEISSUED (RFC 2180 [4.2]/RFC 5530 [3]). */ 3330 if (!empty($resp->data['expungeissued'])) { 3331 $this->noop(); 3332 } 3333 3334 return $resp->data['modified']; 3335 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 3336 /* A NO response, when coupled with a sequence STORE and 3337 * non-SILENT behavior, most likely means that messages were 3338 * expunged. RFC 2180 [4.2] */ 3339 if (empty($pipeline->data['store_silent']) && 3340 !empty($options['sequence']) && 3341 ($e->status === Horde_Imap_Client_Interaction_Server::NO)) { 3342 $this->noop(); 3343 } 3344 3345 return $pipeline->data['modified']; 3346 } 3347 } 3348 3349 /** 3350 * Create a store command. 3351 * 3352 * @param array $options See Horde_Imap_Client_Base#_store(). 3353 * 3354 * @return Horde_Imap_Client_Interaction_Pipeline Pipeline object. 3355 */ 3356 protected function _storeCmd($options) 3357 { 3358 $cmds = array(); 3359 $silent = empty($options['unchangedsince']) 3360 ? !($this->_debug->debug || $this->_initCache(true)) 3361 : false; 3362 3363 if (!empty($options['replace'])) { 3364 $cmds[] = array( 3365 'FLAGS' . ($silent ? '.SILENT' : ''), 3366 $options['replace'] 3367 ); 3368 } else { 3369 foreach (array('add' => '+', 'remove' => '-') as $k => $v) { 3370 if (!empty($options[$k])) { 3371 $cmds[] = array( 3372 $v . 'FLAGS' . ($silent ? '.SILENT' : ''), 3373 $options[$k] 3374 ); 3375 } 3376 } 3377 } 3378 3379 $pipeline = $this->_pipeline(); 3380 $pipeline->data['store_silent'] = $silent; 3381 3382 foreach ($cmds as $val) { 3383 $cmd = $this->_command( 3384 empty($options['sequence']) ? 'UID STORE' : 'STORE' 3385 )->add(strval($options['ids'])); 3386 if (!empty($options['unchangedsince'])) { 3387 $cmd->add(new Horde_Imap_Client_Data_Format_List(array( 3388 'UNCHANGEDSINCE', 3389 new Horde_Imap_Client_Data_Format_Number(intval($options['unchangedsince'])) 3390 ))); 3391 } 3392 $cmd->add($val); 3393 3394 $pipeline->add($cmd); 3395 } 3396 3397 return $pipeline; 3398 } 3399 3400 /** 3401 */ 3402 protected function _copy(Horde_Imap_Client_Mailbox $dest, $options) 3403 { 3404 /* Check for MOVE command (RFC 6851). */ 3405 $move_cmd = (!empty($options['move']) && 3406 $this->queryCapability('MOVE')); 3407 3408 $cmd = $this->_pipeline( 3409 $this->_command( 3410 ($options['ids']->sequence ? '' : 'UID ') . ($move_cmd ? 'MOVE' : 'COPY') 3411 )->add(array( 3412 strval($options['ids']), 3413 new Horde_Imap_Client_Data_Format_Mailbox($dest) 3414 )) 3415 ); 3416 $cmd->data['copydest'] = $dest; 3417 3418 // COPY returns no untagged information (RFC 3501 [6.4.7]) 3419 try { 3420 $resp = $this->_sendCmd($cmd); 3421 } catch (Horde_Imap_Client_Exception $e) { 3422 if (!empty($options['create']) && 3423 !empty($e->resp_data['trycreate'])) { 3424 $this->createMailbox($dest); 3425 unset($options['create']); 3426 return $this->_copy($dest, $options); 3427 } 3428 throw $e; 3429 } 3430 3431 // If moving, delete the old messages now. Short-circuit if nothing 3432 // was moved. 3433 if (!$move_cmd && 3434 !empty($options['move']) && 3435 (isset($resp->data['copyuid']) || 3436 !$this->queryCapability('UIDPLUS'))) { 3437 $this->expunge($this->_selected, array( 3438 'delete' => true, 3439 'ids' => $options['ids'] 3440 )); 3441 } 3442 3443 return isset($resp->data['copyuid']) 3444 ? $resp->data['copyuid'] 3445 : true; 3446 } 3447 3448 /** 3449 */ 3450 protected function _setQuota(Horde_Imap_Client_Mailbox $root, $resources) 3451 { 3452 $limits = new Horde_Imap_Client_Data_Format_List(); 3453 3454 foreach ($resources as $key => $val) { 3455 $limits->add(array( 3456 strtoupper($key), 3457 new Horde_Imap_Client_Data_Format_Number($val) 3458 )); 3459 } 3460 3461 $this->_sendCmd( 3462 $this->_command('SETQUOTA')->add(array( 3463 new Horde_Imap_Client_Data_Format_Mailbox($root), 3464 $limits 3465 )) 3466 ); 3467 } 3468 3469 /** 3470 */ 3471 protected function _getQuota(Horde_Imap_Client_Mailbox $root) 3472 { 3473 $pipeline = $this->_pipeline( 3474 $this->_command('GETQUOTA')->add( 3475 new Horde_Imap_Client_Data_Format_Mailbox($root) 3476 ) 3477 ); 3478 $pipeline->data['quotaresp'] = array(); 3479 3480 return reset($this->_sendCmd($pipeline)->data['quotaresp']); 3481 } 3482 3483 /** 3484 * Parse a QUOTA response (RFC 2087 [5.1]). 3485 * 3486 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3487 * object. 3488 * @param Horde_Imap_Client_Tokenize $data The server response. 3489 */ 3490 protected function _parseQuota( 3491 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3492 Horde_Imap_Client_Tokenize $data 3493 ) 3494 { 3495 $c = &$pipeline->data['quotaresp']; 3496 3497 $root = $data->next(); 3498 $c[$root] = array(); 3499 3500 $data->next(); 3501 3502 while (($curr = $data->next()) !== false) { 3503 $c[$root][strtolower($curr)] = array( 3504 'usage' => $data->next(), 3505 'limit' => $data->next() 3506 ); 3507 } 3508 } 3509 3510 /** 3511 */ 3512 protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox) 3513 { 3514 $pipeline = $this->_pipeline( 3515 $this->_command('GETQUOTAROOT')->add( 3516 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 3517 ) 3518 ); 3519 $pipeline->data['quotaresp'] = array(); 3520 3521 return $this->_sendCmd($pipeline)->data['quotaresp']; 3522 } 3523 3524 /** 3525 */ 3526 protected function _setACL(Horde_Imap_Client_Mailbox $mailbox, $identifier, 3527 $options) 3528 { 3529 // SETACL returns no untagged information (RFC 4314 [3.1]). 3530 $this->_sendCmd( 3531 $this->_command('SETACL')->add(array( 3532 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 3533 new Horde_Imap_Client_Data_Format_Astring($identifier), 3534 new Horde_Imap_Client_Data_Format_Astring($options['rights']) 3535 )) 3536 ); 3537 } 3538 3539 /** 3540 */ 3541 protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox, $identifier) 3542 { 3543 // DELETEACL returns no untagged information (RFC 4314 [3.2]). 3544 $this->_sendCmd( 3545 $this->_command('DELETEACL')->add(array( 3546 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 3547 new Horde_Imap_Client_Data_Format_Astring($identifier) 3548 )) 3549 ); 3550 } 3551 3552 /** 3553 */ 3554 protected function _getACL(Horde_Imap_Client_Mailbox $mailbox) 3555 { 3556 return $this->_sendCmd( 3557 $this->_command('GETACL')->add( 3558 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 3559 ) 3560 )->data['getacl']; 3561 } 3562 3563 /** 3564 * Parse an ACL response (RFC 4314 [3.6]). 3565 * 3566 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3567 * object. 3568 * @param Horde_Imap_Client_Tokenize $data The server response. 3569 */ 3570 protected function _parseACL( 3571 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3572 Horde_Imap_Client_Tokenize $data 3573 ) 3574 { 3575 $acl = array(); 3576 3577 // Ignore mailbox argument -> index 1 3578 $data->next(); 3579 3580 while (($curr = $data->next()) !== false) { 3581 $acl[$curr] = ($curr[0] === '-') 3582 ? new Horde_Imap_Client_Data_AclNegative($data->next()) 3583 : new Horde_Imap_Client_Data_Acl($data->next()); 3584 } 3585 3586 $pipeline->data['getacl'] = $acl; 3587 } 3588 3589 /** 3590 */ 3591 protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox, 3592 $identifier) 3593 { 3594 $resp = $this->_sendCmd( 3595 $this->_command('LISTRIGHTS')->add(array( 3596 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 3597 new Horde_Imap_Client_Data_Format_Astring($identifier) 3598 )) 3599 ); 3600 3601 return isset($resp->data['listaclrights']) 3602 ? $resp->data['listaclrights'] 3603 : new Horde_Imap_Client_Data_AclRights(); 3604 } 3605 3606 /** 3607 * Parse a LISTRIGHTS response (RFC 4314 [3.7]). 3608 * 3609 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3610 * object. 3611 * @param Horde_Imap_Client_Tokenize $data The server response. 3612 */ 3613 protected function _parseListRights( 3614 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3615 Horde_Imap_Client_Tokenize $data 3616 ) 3617 { 3618 // Ignore mailbox and identifier arguments 3619 $data->next(); 3620 $data->next(); 3621 3622 $pipeline->data['listaclrights'] = new Horde_Imap_Client_Data_AclRights( 3623 str_split($data->next()), 3624 $data->flushIterator() 3625 ); 3626 } 3627 3628 /** 3629 */ 3630 protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox) 3631 { 3632 $resp = $this->_sendCmd( 3633 $this->_command('MYRIGHTS')->add( 3634 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 3635 ) 3636 ); 3637 3638 return isset($resp->data['myrights']) 3639 ? $resp->data['myrights'] 3640 : new Horde_Imap_Client_Data_Acl(); 3641 } 3642 3643 /** 3644 * Parse a MYRIGHTS response (RFC 4314 [3.8]). 3645 * 3646 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3647 * object. 3648 * @param Horde_Imap_Client_Tokenize $data The server response. 3649 */ 3650 protected function _parseMyRights( 3651 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3652 Horde_Imap_Client_Tokenize $data 3653 ) 3654 { 3655 // Ignore 1st token (mailbox name) 3656 $data->next(); 3657 3658 $pipeline->data['myrights'] = new Horde_Imap_Client_Data_Acl($data->next()); 3659 } 3660 3661 /** 3662 */ 3663 protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox, 3664 $entries, $options) 3665 { 3666 $pipeline = $this->_pipeline(); 3667 $pipeline->data['metadata'] = array(); 3668 3669 if ($this->queryCapability('METADATA') || 3670 (strlen($mailbox) && $this->queryCapability('METADATA-SERVER'))) { 3671 $cmd_options = new Horde_Imap_Client_Data_Format_List(); 3672 3673 if (!empty($options['maxsize'])) { 3674 $cmd_options->add(array( 3675 'MAXSIZE', 3676 new Horde_Imap_Client_Data_Format_Number($options['maxsize']) 3677 )); 3678 } 3679 if (!empty($options['depth'])) { 3680 $cmd_options->add(array( 3681 'DEPTH', 3682 new Horde_Imap_Client_Data_Format_Number($options['depth']) 3683 )); 3684 } 3685 3686 $queries = new Horde_Imap_Client_Data_Format_List(); 3687 foreach ($entries as $md_entry) { 3688 $queries->add(new Horde_Imap_Client_Data_Format_Astring($md_entry)); 3689 } 3690 3691 $cmd = $this->_command('GETMETADATA')->add( 3692 new Horde_Imap_Client_Data_Format_Mailbox($mailbox) 3693 ); 3694 if (count($cmd_options)) { 3695 $cmd->add($cmd_options); 3696 } 3697 $cmd->add($queries); 3698 3699 $pipeline->add($cmd); 3700 } else { 3701 if (!$this->queryCapability('ANNOTATEMORE') && 3702 !$this->queryCapability('ANNOTATEMORE2')) { 3703 throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA'); 3704 } 3705 3706 $queries = array(); 3707 foreach ($entries as $md_entry) { 3708 list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry); 3709 3710 if (!isset($queries[$type])) { 3711 $queries[$type] = new Horde_Imap_Client_Data_Format_List(); 3712 } 3713 $queries[$type]->add(new Horde_Imap_Client_Data_Format_String($entry)); 3714 } 3715 3716 foreach ($queries as $key => $val) { 3717 // TODO: Honor maxsize and depth options. 3718 $pipeline->add( 3719 $this->_command('GETANNOTATION')->add(array( 3720 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 3721 $val, 3722 new Horde_Imap_Client_Data_Format_String($key) 3723 )) 3724 ); 3725 } 3726 } 3727 3728 return $this->_sendCmd($pipeline)->data['metadata']; 3729 } 3730 3731 /** 3732 * Split a name for the METADATA extension into the correct syntax for the 3733 * older ANNOTATEMORE version. 3734 * 3735 * @param string $name A name for a metadata entry. 3736 * 3737 * @return array A list of two elements: The entry name and the value 3738 * type. 3739 * 3740 * @throws Horde_Imap_Client_Exception 3741 */ 3742 protected function _getAnnotateMoreEntry($name) 3743 { 3744 if (substr($name, 0, 7) === '/shared') { 3745 return array(substr($name, 7), 'value.shared'); 3746 } else if (substr($name, 0, 8) === '/private') { 3747 return array(substr($name, 8), 'value.priv'); 3748 } 3749 3750 throw new Horde_Imap_Client_Exception( 3751 sprintf(Horde_Imap_Client_Translation::r("Invalid METADATA entry: \"%s\"."), $name), 3752 Horde_Imap_Client_Exception::METADATA_INVALID 3753 ); 3754 } 3755 3756 /** 3757 */ 3758 protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox, $data) 3759 { 3760 if ($this->queryCapability('METADATA') || 3761 (strlen($mailbox) && $this->queryCapability('METADATA-SERVER'))) { 3762 $data_elts = new Horde_Imap_Client_Data_Format_List(); 3763 3764 foreach ($data as $key => $value) { 3765 $data_elts->add(array( 3766 new Horde_Imap_Client_Data_Format_Astring($key), 3767 new Horde_Imap_Client_Data_Format_Nstring($value) 3768 )); 3769 } 3770 3771 $cmd = $this->_command('SETMETADATA')->add(array( 3772 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 3773 $data_elts 3774 )); 3775 } else { 3776 if (!$this->queryCapability('ANNOTATEMORE') && 3777 !$this->queryCapability('ANNOTATEMORE2')) { 3778 throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA'); 3779 } 3780 3781 $cmd = $this->_pipeline(); 3782 3783 foreach ($data as $md_entry => $value) { 3784 list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry); 3785 3786 $cmd->add( 3787 $this->_command('SETANNOTATION')->add(array( 3788 new Horde_Imap_Client_Data_Format_Mailbox($mailbox), 3789 new Horde_Imap_Client_Data_Format_String($entry), 3790 new Horde_Imap_Client_Data_Format_List(array( 3791 new Horde_Imap_Client_Data_Format_String($type), 3792 new Horde_Imap_Client_Data_Format_Nstring($value) 3793 )) 3794 )) 3795 ); 3796 } 3797 } 3798 3799 $this->_sendCmd($cmd); 3800 } 3801 3802 /** 3803 * Parse an ANNOTATION response (ANNOTATEMORE/ANNOTATEMORE2). 3804 * 3805 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3806 * object. 3807 * @param Horde_Imap_Client_Tokenize $data The server response. 3808 * 3809 * @throws Horde_Imap_Client_Exception 3810 */ 3811 protected function _parseAnnotation( 3812 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3813 Horde_Imap_Client_Tokenize $data 3814 ) 3815 { 3816 // Mailbox name is in UTF7-IMAP. 3817 $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true); 3818 $entry = $data->next(); 3819 3820 // Ignore unsolicited responses. 3821 if ($data->next() !== true) { 3822 return; 3823 } 3824 3825 while (($type = $data->next()) !== false) { 3826 switch ($type) { 3827 case 'value.priv': 3828 $pipeline->data['metadata'][strval($mbox)]['/private' . $entry] = $data->next(); 3829 break; 3830 3831 case 'value.shared': 3832 $pipeline->data['metadata'][strval($mbox)]['/shared' . $entry] = $data->next(); 3833 break; 3834 3835 default: 3836 throw new Horde_Imap_Client_Exception( 3837 sprintf(Horde_Imap_Client_Translation::r("Invalid METADATA value type \"%s\"."), $type), 3838 Horde_Imap_Client_Exception::METADATA_INVALID 3839 ); 3840 } 3841 } 3842 } 3843 3844 /** 3845 * Parse a METADATA response (RFC 5464 [4.4]). 3846 * 3847 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3848 * object. 3849 * @param Horde_Imap_Client_Tokenize $data The server response. 3850 * 3851 * @throws Horde_Imap_Client_Exception 3852 */ 3853 protected function _parseMetadata( 3854 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3855 Horde_Imap_Client_Tokenize $data 3856 ) 3857 { 3858 // Mailbox name is in UTF7-IMAP. 3859 $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true); 3860 3861 // Ignore unsolicited responses. 3862 if ($data->next() === true) { 3863 while (($entry = $data->next()) !== false) { 3864 $pipeline->data['metadata'][strval($mbox)][$entry] = $data->next(); 3865 } 3866 } 3867 } 3868 3869 /* Overriden methods. */ 3870 3871 /** 3872 * @param array $opts Options: 3873 * - decrement: (boolean) If true, decrement the message count. 3874 * - pipeline: (Horde_Imap_Client_Interaction_Pipeline) Pipeline object. 3875 */ 3876 protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox, 3877 Horde_Imap_Client_Ids $ids, 3878 array $opts = array()) 3879 { 3880 /* If there are pending FETCH cache writes, we need to write them 3881 * before the UID -> sequence number mapping changes. */ 3882 if (isset($opts['pipeline'])) { 3883 $this->_updateCache($opts['pipeline']->fetch); 3884 } 3885 3886 $res = parent::_deleteMsgs($mailbox, $ids); 3887 3888 if (isset($this->_temp['expunged'])) { 3889 $this->_temp['expunged']->add($res); 3890 } 3891 3892 if (!empty($opts['decrement'])) { 3893 $mbox_ob = $this->_mailboxOb(); 3894 $mbox_ob->setStatus( 3895 Horde_Imap_Client::STATUS_MESSAGES, 3896 $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) - count($ids) 3897 ); 3898 } 3899 } 3900 3901 /* Internal functions. */ 3902 3903 /** 3904 * Sends command(s) to the IMAP server. A connection to the server must 3905 * have already been made. 3906 * 3907 * @param mixed $cmd Either a Command object or a Pipeline object. 3908 * 3909 * @return Horde_Imap_Client_Interaction_Pipeline A pipeline object. 3910 * @throws Horde_Imap_Client_Exception 3911 */ 3912 protected function _sendCmd($cmd) 3913 { 3914 $pipeline = ($cmd instanceof Horde_Imap_Client_Interaction_Command) 3915 ? $this->_pipeline($cmd) 3916 : $cmd; 3917 3918 if (!empty($this->_cmdQueue)) { 3919 /* Add commands in reverse order. */ 3920 foreach (array_reverse($this->_cmdQueue) as $val) { 3921 $pipeline->add($val, true); 3922 } 3923 3924 $this->_cmdQueue = array(); 3925 } 3926 3927 $cmd_list = array(); 3928 3929 foreach ($pipeline as $val) { 3930 if ($val->continuation) { 3931 $this->_sendCmdChunk($pipeline, $cmd_list); 3932 $this->_sendCmdChunk($pipeline, array($val)); 3933 $cmd_list = array(); 3934 } else { 3935 $cmd_list[] = $val; 3936 } 3937 } 3938 3939 $this->_sendCmdChunk($pipeline, $cmd_list); 3940 3941 /* If any FLAGS responses contain MODSEQs but not UIDs, don't 3942 * cache any data and immediately close the mailbox. */ 3943 foreach ($pipeline->data['modseqs_nouid'] as $val) { 3944 if (!$pipeline->fetch[$val]->getUid()) { 3945 $this->_debug->info( 3946 'Server provided FLAGS MODSEQ without providing UID.' 3947 ); 3948 $this->close(); 3949 return $pipeline; 3950 } 3951 } 3952 3953 /* Update HIGHESTMODSEQ value. */ 3954 if (!empty($pipeline->data['modseqs'])) { 3955 $modseq = max($pipeline->data['modseqs']); 3956 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ, $modseq); 3957 /* CONDSTORE has not yet updated flag information, so don't update 3958 * modseq yet. */ 3959 if (!empty($this->_temp['enabled']['QRESYNC'])) { 3960 $this->_updateModSeq($modseq); 3961 } 3962 } 3963 3964 /* Update cache items. */ 3965 $this->_updateCache($pipeline->fetch); 3966 3967 return $pipeline; 3968 } 3969 3970 /** 3971 * Send a chunk of commands and/or continuation fragments to the server. 3972 * 3973 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline 3974 * object. 3975 * @param array $chunk List of commands to send. 3976 * 3977 * @throws Horde_Imap_Client_Exception 3978 */ 3979 protected function _sendCmdChunk($pipeline, $chunk) 3980 { 3981 if (empty($chunk)) { 3982 return; 3983 } 3984 3985 $cmd_count = count($chunk); 3986 $exception = null; 3987 3988 foreach ($chunk as $val) { 3989 try { 3990 $old_debug = $this->_debug->debug; 3991 if (!is_null($val->debug)) { 3992 $this->_debug->raw($val->tag . ' ' . $val->debug . "\n"); 3993 $this->_debug->debug = false; 3994 } 3995 if ($this->_processCmd($pipeline, $val, $val)) { 3996 $this->_connection->write('', true); 3997 } else { 3998 $cmd_count = 0; 3999 } 4000 $this->_debug->debug = $old_debug; 4001 } catch (Horde_Imap_Client_Exception $e) { 4002 $this->_debug->debug = $old_debug; 4003 4004 switch ($e->getCode()) { 4005 case Horde_Imap_Client_Exception::SERVER_WRITEERROR: 4006 $this->_temp['logout'] = true; 4007 $this->logout(); 4008 break; 4009 } 4010 4011 throw $e; 4012 } 4013 } 4014 4015 while ($cmd_count) { 4016 try { 4017 if ($this->_getLine($pipeline) instanceof Horde_Imap_Client_Interaction_Server_Tagged) { 4018 --$cmd_count; 4019 } 4020 } catch (Horde_Imap_Client_Exception $e) { 4021 switch ($e->getCode()) { 4022 case $e::DISCONNECT: 4023 /* Guaranteed to have no more data incoming, so we can 4024 * immediately logout. */ 4025 $this->_temp['logout'] = true; 4026 $this->logout(); 4027 throw $e; 4028 } 4029 4030 /* For all other issues, catch and store exception; don't 4031 * throw until all input is read since we need to clear 4032 * incoming queue. (For now, only store first exception.) */ 4033 if (is_null($exception)) { 4034 $exception = $e; 4035 } 4036 4037 if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) && 4038 $e->command) { 4039 --$cmd_count; 4040 } 4041 } 4042 } 4043 4044 if (!is_null($exception)) { 4045 throw $exception; 4046 } 4047 } 4048 4049 /** 4050 * Process/send a command to the remote server. 4051 * 4052 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline 4053 * object. 4054 * @param Horde_Imap_Client_Interaction_Command $cmd The master command. 4055 * @param Horde_Imap_Client_Data_Format_List $data Commands to send. 4056 * 4057 * @return boolean True if EOL needed to finish command. 4058 * @throws Horde_Imap_Client_Exception 4059 * @throws Horde_Imap_Client_Exception_NoSupport 4060 */ 4061 protected function _processCmd($pipeline, $cmd, $data) 4062 { 4063 if ($this->_debug->debug && 4064 ($data instanceof Horde_Imap_Client_Interaction_Command)) { 4065 $data->startTimer(); 4066 } 4067 4068 foreach ($data as $key => $val) { 4069 if ($val instanceof Horde_Imap_Client_Interaction_Command_Continuation) { 4070 $this->_connection->write('', true); 4071 4072 /* Check for optional continuation responses when the command 4073 * has already finished. */ 4074 if (!$cmd_continuation = $this->_processCmdContinuation($pipeline, $val->optional)) { 4075 return false; 4076 } 4077 4078 $this->_processCmd( 4079 $pipeline, 4080 $cmd, 4081 $val->getCommands($cmd_continuation) 4082 ); 4083 continue; 4084 } 4085 4086 if ($key) { 4087 $this->_connection->write(' '); 4088 } 4089 4090 if ($val instanceof Horde_Imap_Client_Data_Format_List) { 4091 $this->_connection->write('('); 4092 $this->_processCmd($pipeline, $cmd, $val); 4093 $this->_connection->write(')'); 4094 } elseif (($val instanceof Horde_Imap_Client_Data_Format_String) && 4095 $val->literal()) { 4096 /* RFC 3516/4466: Send literal8 if we have binary data. */ 4097 if ($cmd->literal8 && 4098 $val->binary() && 4099 $this->queryCapability('BINARY')) { 4100 $binary = true; 4101 $this->_connection->write('~'); 4102 } else { 4103 $binary = false; 4104 } 4105 4106 $literal_len = $val->length(); 4107 $this->_connection->write('{' . $literal_len); 4108 4109 /* RFC 2088 - If LITERAL+ is available, saves a roundtrip from 4110 * the server. */ 4111 if ($cmd->literalplus && $this->queryCapability('LITERAL+')) { 4112 $this->_connection->write('+}', true); 4113 } else { 4114 $this->_connection->write('}', true); 4115 $this->_processCmdContinuation($pipeline); 4116 } 4117 4118 $this->_connection->writeLiteral($val->getStream(), $literal_len, $binary); 4119 } else { 4120 $this->_connection->write($val->escape()); 4121 } 4122 } 4123 4124 return true; 4125 } 4126 4127 /** 4128 * Process a command continuation response. 4129 * 4130 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline 4131 * object. 4132 * @param boolean $noexception Don't throw 4133 * exception if 4134 * continuation 4135 * does not occur. 4136 * 4137 * @return mixed A Horde_Imap_Client_Interaction_Server_Continuation 4138 * object or false. 4139 * 4140 * @throws Horde_Imap_Client_Exception 4141 */ 4142 protected function _processCmdContinuation($pipeline, $noexception = false) 4143 { 4144 do { 4145 $ob = $this->_getLine($pipeline); 4146 } while ($ob instanceof Horde_Imap_Client_Interaction_Server_Untagged); 4147 4148 if ($ob instanceof Horde_Imap_Client_Interaction_Server_Continuation) { 4149 return $ob; 4150 } elseif ($noexception) { 4151 return false; 4152 } 4153 4154 $this->_debug->info( 4155 'ERROR: Unexpected response from server while waiting for a continuation request.' 4156 ); 4157 $e = new Horde_Imap_Client_Exception( 4158 Horde_Imap_Client_Translation::r("Error when communicating with the mail server."), 4159 Horde_Imap_Client_Exception::SERVER_READERROR 4160 ); 4161 $e->details = strval($ob); 4162 4163 throw $e; 4164 } 4165 4166 /** 4167 * Shortcut to creating a new IMAP client command object. 4168 * 4169 * @param string $cmd The IMAP command. 4170 * 4171 * @return Horde_Imap_Client_Interaction_Command A command object. 4172 */ 4173 protected function _command($cmd) 4174 { 4175 return new Horde_Imap_Client_Interaction_Command($cmd, ++$this->_tag); 4176 } 4177 4178 /** 4179 * Shortcut to creating a new pipeline object. 4180 * 4181 * @param Horde_Imap_Client_Interaction_Command $cmd An IMAP command to 4182 * add. 4183 * 4184 * @return Horde_Imap_Client_Interaction_Pipeline A pipeline object. 4185 */ 4186 protected function _pipeline($cmd = null) 4187 { 4188 if (!isset($this->_temp['fetchob'])) { 4189 $this->_temp['fetchob'] = new Horde_Imap_Client_Fetch_Results( 4190 $this->_fetchDataClass, 4191 Horde_Imap_Client_Fetch_Results::SEQUENCE 4192 ); 4193 } 4194 4195 $ob = new Horde_Imap_Client_Interaction_Pipeline( 4196 clone $this->_temp['fetchob'] 4197 ); 4198 4199 if (!is_null($cmd)) { 4200 $ob->add($cmd); 4201 } 4202 4203 return $ob; 4204 } 4205 4206 /** 4207 * Gets data from the IMAP server stream and parses it. 4208 * 4209 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4210 * object. 4211 * 4212 * @return Horde_Imap_Client_Interaction_Server Server object. 4213 * 4214 * @throws Horde_Imap_Client_Exception 4215 */ 4216 protected function _getLine( 4217 Horde_Imap_Client_Interaction_Pipeline $pipeline 4218 ) 4219 { 4220 $server = Horde_Imap_Client_Interaction_Server::create( 4221 $this->_connection->read() 4222 ); 4223 4224 switch (get_class($server)) { 4225 case 'Horde_Imap_Client_Interaction_Server_Continuation': 4226 $this->_responseCode($pipeline, $server); 4227 break; 4228 4229 case 'Horde_Imap_Client_Interaction_Server_Tagged': 4230 $cmd = $pipeline->complete($server); 4231 if ($timer = $cmd->getTimer()) { 4232 $this->_debug->info(sprintf( 4233 'Command %s took %s seconds.', 4234 $cmd->tag, 4235 $timer 4236 )); 4237 } 4238 $this->_responseCode($pipeline, $server); 4239 break; 4240 4241 case 'Horde_Imap_Client_Interaction_Server_Untagged': 4242 if (is_null($server->status)) { 4243 $this->_serverResponse($pipeline, $server); 4244 } else { 4245 $this->_responseCode($pipeline, $server); 4246 } 4247 break; 4248 } 4249 4250 switch ($server->status) { 4251 case $server::BAD: 4252 case $server::NO: 4253 /* A tagged BAD response indicates that the tagged command caused 4254 * the error. This information is unknown if untagged (RFC 3501 4255 * [7.1.3]) - ignore these untagged responses. 4256 * An untagged NO response indicates a warning; ignore and assume 4257 * that it also included response text code that is handled 4258 * elsewhere. Throw exception if tagged; command handlers can 4259 * catch this if able to workaround this issue (RFC 3501 4260 * [7.1.2]). */ 4261 if ($server instanceof Horde_Imap_Client_Interaction_Server_Tagged) { 4262 throw new Horde_Imap_Client_Exception_ServerResponse( 4263 Horde_Imap_Client_Translation::r("IMAP error reported by server."), 4264 0, 4265 $server, 4266 $pipeline 4267 ); 4268 } 4269 break; 4270 4271 case $server::BYE: 4272 /* A BYE response received as part of a logout command should be 4273 * be treated like a regular command: a client MUST process the 4274 * entire command until logging out (RFC 3501 [3.4; 7.1.5]). */ 4275 if (empty($this->_temp['logout'])) { 4276 $e = new Horde_Imap_Client_Exception( 4277 Horde_Imap_Client_Translation::r("IMAP Server closed the connection."), 4278 Horde_Imap_Client_Exception::DISCONNECT 4279 ); 4280 $e->details = strval($server); 4281 throw $e; 4282 } 4283 break; 4284 4285 case $server::PREAUTH: 4286 /* The user was pre-authenticated. (RFC 3501 [7.1.4]) */ 4287 $this->_temp['preauth'] = true; 4288 break; 4289 } 4290 4291 return $server; 4292 } 4293 4294 /** 4295 * Handle untagged server responses (see RFC 3501 [2.2.2]). 4296 * 4297 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4298 * object. 4299 * @param Horde_Imap_Client_Interaction_Server $ob Server 4300 * response. 4301 */ 4302 protected function _serverResponse( 4303 Horde_Imap_Client_Interaction_Pipeline $pipeline, 4304 Horde_Imap_Client_Interaction_Server $ob 4305 ) 4306 { 4307 $token = $ob->token; 4308 4309 /* First, catch untagged responses where the name appears first on the 4310 * line. */ 4311 switch ($first = strtoupper($token->current())) { 4312 case 'CAPABILITY': 4313 $this->_parseCapability($pipeline, $token->flushIterator()); 4314 break; 4315 4316 case 'LIST': 4317 case 'LSUB': 4318 $this->_parseList($pipeline, $token); 4319 break; 4320 4321 case 'STATUS': 4322 // Parse a STATUS response (RFC 3501 [7.2.4]). 4323 $this->_parseStatus($token); 4324 break; 4325 4326 case 'SEARCH': 4327 case 'SORT': 4328 // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] & RFC 5256 [4]). 4329 $this->_parseSearch($pipeline, $token->flushIterator()); 4330 break; 4331 4332 case 'ESEARCH': 4333 // Parse an ESEARCH response (RFC 4466 [2.6.2]). 4334 $this->_parseEsearch($pipeline, $token); 4335 break; 4336 4337 case 'FLAGS': 4338 $token->next(); 4339 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FLAGS, array_map('strtolower', $token->flushIterator())); 4340 break; 4341 4342 case 'QUOTA': 4343 $this->_parseQuota($pipeline, $token); 4344 break; 4345 4346 case 'QUOTAROOT': 4347 // Ignore this line - we can get this information from 4348 // the untagged QUOTA responses. 4349 break; 4350 4351 case 'NAMESPACE': 4352 $this->_parseNamespace($pipeline, $token); 4353 break; 4354 4355 case 'THREAD': 4356 $this->_parseThread($pipeline, $token); 4357 break; 4358 4359 case 'ACL': 4360 $this->_parseACL($pipeline, $token); 4361 break; 4362 4363 case 'LISTRIGHTS': 4364 $this->_parseListRights($pipeline, $token); 4365 break; 4366 4367 case 'MYRIGHTS': 4368 $this->_parseMyRights($pipeline, $token); 4369 break; 4370 4371 case 'ID': 4372 // ID extension (RFC 2971) 4373 $this->_parseID($pipeline, $token); 4374 break; 4375 4376 case 'ENABLED': 4377 // ENABLE extension (RFC 5161) 4378 $this->_parseEnabled($token); 4379 break; 4380 4381 case 'LANGUAGE': 4382 // LANGUAGE extension (RFC 5255 [3.2]) 4383 $this->_parseLanguage($token); 4384 break; 4385 4386 case 'COMPARATOR': 4387 // I18NLEVEL=2 extension (RFC 5255 [4.7]) 4388 $this->_parseComparator($pipeline, $token); 4389 break; 4390 4391 case 'VANISHED': 4392 // QRESYNC extension (RFC 7162 [3.2.10]) 4393 $this->_parseVanished($pipeline, $token); 4394 break; 4395 4396 case 'ANNOTATION': 4397 // Parse an ANNOTATION response. 4398 $this->_parseAnnotation($pipeline, $token); 4399 break; 4400 4401 case 'METADATA': 4402 // Parse a METADATA response. 4403 $this->_parseMetadata($pipeline, $token); 4404 break; 4405 4406 default: 4407 // Next, look for responses where the keywords occur second. 4408 switch (strtoupper($token->next())) { 4409 case 'EXISTS': 4410 // EXISTS response - RFC 3501 [7.3.2] 4411 $mbox_ob = $this->_mailboxOb(); 4412 4413 // Increment UIDNEXT if it is set. 4414 if ($mbox_ob->open && 4415 ($uidnext = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDNEXT))) { 4416 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $uidnext + $first - $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES)); 4417 } 4418 4419 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_MESSAGES, $first); 4420 break; 4421 4422 case 'RECENT': 4423 // RECENT response - RFC 3501 [7.3.1] 4424 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_RECENT, $first); 4425 break; 4426 4427 case 'EXPUNGE': 4428 // EXPUNGE response - RFC 3501 [7.4.1] 4429 $this->_deleteMsgs($this->_selected, $this->getIdsOb($first, true), array( 4430 'decrement' => true, 4431 'pipeline' => $pipeline 4432 )); 4433 $pipeline->data['expunge_seen'] = true; 4434 break; 4435 4436 case 'FETCH': 4437 // FETCH response - RFC 3501 [7.4.2] 4438 $this->_parseFetch($pipeline, $first, $token); 4439 break; 4440 } 4441 break; 4442 } 4443 } 4444 4445 /** 4446 * Handle status responses (see RFC 3501 [7.1]). 4447 * 4448 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4449 * object. 4450 * @param Horde_Imap_Client_Interaction_Server $ob Server object. 4451 * 4452 * @throws Horde_Imap_Client_Exception_ServerResponse 4453 */ 4454 protected function _responseCode( 4455 Horde_Imap_Client_Interaction_Pipeline $pipeline, 4456 Horde_Imap_Client_Interaction_Server $ob 4457 ) 4458 { 4459 if (is_null($ob->responseCode)) { 4460 return; 4461 } 4462 4463 $rc = $ob->responseCode; 4464 4465 switch ($rc->code) { 4466 case 'ALERT': 4467 // Defined by RFC 5530 [3] - Treat as an alert for now. 4468 case 'CONTACTADMIN': 4469 if (!isset($this->_temp['alerts'])) { 4470 $this->_temp['alerts'] = array(); 4471 } 4472 $this->_temp['alerts'][] = strval($ob->token); 4473 break; 4474 4475 case 'BADCHARSET': 4476 /* Store valid search charsets if returned by server. */ 4477 $s_charset = array(); 4478 foreach ($rc->data[0] as $val) { 4479 $s_charset[$val] = true; 4480 } 4481 4482 if (!empty($s_charset)) { 4483 $this->_setInit('s_charset', array_merge( 4484 $this->_init['s_charset'], 4485 $s_charset 4486 )); 4487 } 4488 4489 throw new Horde_Imap_Client_Exception_ServerResponse( 4490 Horde_Imap_Client_Translation::r("Charset used in search query is not supported on the mail server."), 4491 Horde_Imap_Client_Exception::BADCHARSET, 4492 $ob, 4493 $pipeline 4494 ); 4495 4496 case 'CAPABILITY': 4497 $this->_parseCapability($pipeline, $rc->data); 4498 break; 4499 4500 case 'PARSE': 4501 /* Only throw error on NO/BAD. Message is human readable. */ 4502 switch ($ob->status) { 4503 case Horde_Imap_Client_Interaction_Server::BAD: 4504 case Horde_Imap_Client_Interaction_Server::NO: 4505 throw new Horde_Imap_Client_Exception_ServerResponse( 4506 sprintf(Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message: %s"), strval($ob->token)), 4507 Horde_Imap_Client_Exception::PARSEERROR, 4508 $ob, 4509 $pipeline 4510 ); 4511 } 4512 break; 4513 4514 case 'READ-ONLY': 4515 $this->_mode = Horde_Imap_Client::OPEN_READONLY; 4516 break; 4517 4518 case 'READ-WRITE': 4519 $this->_mode = Horde_Imap_Client::OPEN_READWRITE; 4520 break; 4521 4522 case 'TRYCREATE': 4523 // RFC 3501 [7.1] 4524 $pipeline->data['trycreate'] = true; 4525 break; 4526 4527 case 'PERMANENTFLAGS': 4528 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_PERMFLAGS, array_map('strtolower', $rc->data[0])); 4529 break; 4530 4531 case 'UIDNEXT': 4532 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $rc->data[0]); 4533 break; 4534 4535 case 'UIDVALIDITY': 4536 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDVALIDITY, $rc->data[0]); 4537 break; 4538 4539 case 'UNSEEN': 4540 /* This is different from the STATUS UNSEEN response - this item, 4541 * if defined, returns the first UNSEEN message in the mailbox. */ 4542 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $rc->data[0]); 4543 break; 4544 4545 case 'REFERRAL': 4546 // Defined by RFC 2221 4547 $pipeline->data['referral'] = new Horde_Imap_Client_Url($rc->data[0]); 4548 break; 4549 4550 case 'UNKNOWN-CTE': 4551 // Defined by RFC 3516 4552 throw new Horde_Imap_Client_Exception_ServerResponse( 4553 Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message."), 4554 Horde_Imap_Client_Exception::UNKNOWNCTE, 4555 $ob, 4556 $pipeline 4557 ); 4558 4559 case 'APPENDUID': 4560 // Defined by RFC 4315 4561 // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s) 4562 $pipeline->data['appenduid'] = $this->getIdsOb($rc->data[1]); 4563 break; 4564 4565 case 'COPYUID': 4566 // Defined by RFC 4315 4567 // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO 4568 $pipeline->data['copyuid'] = array_combine( 4569 $this->getIdsOb($rc->data[1])->ids, 4570 $this->getIdsOb($rc->data[2])->ids 4571 ); 4572 4573 /* Use UIDPLUS information to move cached data to new mailbox (see 4574 * RFC 4549 [4.2.2.1]). Need to move now, because a MOVE might 4575 * EXPUNGE immediately afterwards. */ 4576 $this->_moveCache($pipeline->data['copydest'], $pipeline->data['copyuid'], $rc->data[0]); 4577 break; 4578 4579 case 'UIDNOTSTICKY': 4580 // Defined by RFC 4315 [3] 4581 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY, true); 4582 break; 4583 4584 case 'BADURL': 4585 // Defined by RFC 4469 [4.1] 4586 throw new Horde_Imap_Client_Exception_ServerResponse( 4587 Horde_Imap_Client_Translation::r("Could not save message on server."), 4588 Horde_Imap_Client_Exception::CATENATE_BADURL, 4589 $ob, 4590 $pipeline 4591 ); 4592 4593 case 'TOOBIG': 4594 // Defined by RFC 4469 [4.2] 4595 throw new Horde_Imap_Client_Exception_ServerResponse( 4596 Horde_Imap_Client_Translation::r("Could not save message data because it is too large."), 4597 Horde_Imap_Client_Exception::CATENATE_TOOBIG, 4598 $ob, 4599 $pipeline 4600 ); 4601 4602 case 'HIGHESTMODSEQ': 4603 // Defined by RFC 7162 [3.1.2.1] 4604 $pipeline->data['modseqs'][] = $rc->data[0]; 4605 break; 4606 4607 case 'NOMODSEQ': 4608 // Defined by RFC 7162 [3.1.2.2] 4609 $pipeline->data['modseqs'][] = 0; 4610 break; 4611 4612 case 'MODIFIED': 4613 // Defined by RFC 7162 [3.1.3] 4614 $pipeline->data['modified']->add($rc->data[0]); 4615 break; 4616 4617 case 'CLOSED': 4618 // Defined by RFC 7162 [3.2.11] 4619 if (isset($pipeline->data['qresyncmbox'])) { 4620 /* If there is any pending FETCH cache entries, flush them 4621 * now before changing mailboxes. */ 4622 $this->_updateCache($pipeline->fetch); 4623 $pipeline->fetch->clear(); 4624 4625 $this->_changeSelected( 4626 $pipeline->data['qresyncmbox'][0], 4627 $pipeline->data['qresyncmbox'][1] 4628 ); 4629 unset($pipeline->data['qresyncmbox']); 4630 } 4631 break; 4632 4633 case 'NOTSAVED': 4634 // Defined by RFC 5182 [2.5] 4635 $pipeline->data['searchnotsaved'] = true; 4636 break; 4637 4638 case 'BADCOMPARATOR': 4639 // Defined by RFC 5255 [4.9] 4640 throw new Horde_Imap_Client_Exception_ServerResponse( 4641 Horde_Imap_Client_Translation::r("The comparison algorithm was not recognized by the server."), 4642 Horde_Imap_Client_Exception::BADCOMPARATOR, 4643 $ob, 4644 $pipeline 4645 ); 4646 4647 case 'METADATA': 4648 $md = $rc->data[0]; 4649 4650 switch ($md[0]) { 4651 case 'LONGENTRIES': 4652 // Defined by RFC 5464 [4.2.1] 4653 $pipeline->data['metadata']['*longentries'] = intval($md[1]); 4654 break; 4655 4656 case 'MAXSIZE': 4657 // Defined by RFC 5464 [4.3] 4658 throw new Horde_Imap_Client_Exception_ServerResponse( 4659 Horde_Imap_Client_Translation::r("The metadata item could not be saved because it is too large."), 4660 Horde_Imap_Client_Exception::METADATA_MAXSIZE, 4661 $ob, 4662 $pipeline 4663 ); 4664 4665 case 'NOPRIVATE': 4666 // Defined by RFC 5464 [4.3] 4667 throw new Horde_Imap_Client_Exception_ServerResponse( 4668 Horde_Imap_Client_Translation::r("The metadata item could not be saved because the server does not support private annotations."), 4669 Horde_Imap_Client_Exception::METADATA_NOPRIVATE, 4670 $ob, 4671 $pipeline 4672 ); 4673 4674 case 'TOOMANY': 4675 // Defined by RFC 5464 [4.3] 4676 throw new Horde_Imap_Client_Exception_ServerResponse( 4677 Horde_Imap_Client_Translation::r("The metadata item could not be saved because the maximum number of annotations has been exceeded."), 4678 Horde_Imap_Client_Exception::METADATA_TOOMANY, 4679 $ob, 4680 $pipeline 4681 ); 4682 } 4683 break; 4684 4685 case 'UNAVAILABLE': 4686 // Defined by RFC 5530 [3] 4687 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 4688 Horde_Imap_Client_Translation::r("Remote server is temporarily unavailable."), 4689 Horde_Imap_Client_Exception::LOGIN_UNAVAILABLE 4690 ); 4691 break; 4692 4693 case 'AUTHENTICATIONFAILED': 4694 // Defined by RFC 5530 [3] 4695 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 4696 Horde_Imap_Client_Translation::r("Authentication failed."), 4697 Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED 4698 ); 4699 break; 4700 4701 case 'AUTHORIZATIONFAILED': 4702 // Defined by RFC 5530 [3] 4703 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 4704 Horde_Imap_Client_Translation::r("Authentication was successful, but authorization failed."), 4705 Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED 4706 ); 4707 break; 4708 4709 case 'EXPIRED': 4710 // Defined by RFC 5530 [3] 4711 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 4712 Horde_Imap_Client_Translation::r("Authentication credentials have expired."), 4713 Horde_Imap_Client_Exception::LOGIN_EXPIRED 4714 ); 4715 break; 4716 4717 case 'PRIVACYREQUIRED': 4718 // Defined by RFC 5530 [3] 4719 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 4720 Horde_Imap_Client_Translation::r("Operation failed due to a lack of a secure connection."), 4721 Horde_Imap_Client_Exception::LOGIN_PRIVACYREQUIRED 4722 ); 4723 break; 4724 4725 case 'NOPERM': 4726 // Defined by RFC 5530 [3] 4727 throw new Horde_Imap_Client_Exception_ServerResponse( 4728 Horde_Imap_Client_Translation::r("You do not have adequate permissions to carry out this operation."), 4729 Horde_Imap_Client_Exception::NOPERM, 4730 $ob, 4731 $pipeline 4732 ); 4733 4734 case 'INUSE': 4735 // Defined by RFC 5530 [3] 4736 throw new Horde_Imap_Client_Exception_ServerResponse( 4737 Horde_Imap_Client_Translation::r("There was a temporary issue when attempting this operation. Please try again later."), 4738 Horde_Imap_Client_Exception::INUSE, 4739 $ob, 4740 $pipeline 4741 ); 4742 4743 case 'EXPUNGEISSUED': 4744 // Defined by RFC 5530 [3] 4745 $pipeline->data['expungeissued'] = true; 4746 break; 4747 4748 case 'CORRUPTION': 4749 // Defined by RFC 5530 [3] 4750 throw new Horde_Imap_Client_Exception_ServerResponse( 4751 Horde_Imap_Client_Translation::r("The mail server is reporting corrupt data in your mailbox."), 4752 Horde_Imap_Client_Exception::CORRUPTION, 4753 $ob, 4754 $pipeline 4755 ); 4756 4757 case 'SERVERBUG': 4758 case 'CLIENTBUG': 4759 case 'CANNOT': 4760 // Defined by RFC 5530 [3] 4761 $this->_debug->info( 4762 'ERROR: mail server explicitly reporting an error.' 4763 ); 4764 break; 4765 4766 case 'LIMIT': 4767 // Defined by RFC 5530 [3] 4768 throw new Horde_Imap_Client_Exception_ServerResponse( 4769 Horde_Imap_Client_Translation::r("The mail server has denied the request."), 4770 Horde_Imap_Client_Exception::LIMIT, 4771 $ob, 4772 $pipeline 4773 ); 4774 4775 case 'OVERQUOTA': 4776 // Defined by RFC 5530 [3] 4777 throw new Horde_Imap_Client_Exception_ServerResponse( 4778 Horde_Imap_Client_Translation::r("The operation failed because the quota has been exceeded on the mail server."), 4779 Horde_Imap_Client_Exception::OVERQUOTA, 4780 $ob, 4781 $pipeline 4782 ); 4783 4784 case 'ALREADYEXISTS': 4785 // Defined by RFC 5530 [3] 4786 throw new Horde_Imap_Client_Exception_ServerResponse( 4787 Horde_Imap_Client_Translation::r("The object could not be created because it already exists."), 4788 Horde_Imap_Client_Exception::ALREADYEXISTS, 4789 $ob, 4790 $pipeline 4791 ); 4792 4793 case 'NONEXISTENT': 4794 // Defined by RFC 5530 [3] 4795 throw new Horde_Imap_Client_Exception_ServerResponse( 4796 Horde_Imap_Client_Translation::r("The object could not be deleted because it does not exist."), 4797 Horde_Imap_Client_Exception::NONEXISTENT, 4798 $ob, 4799 $pipeline 4800 ); 4801 4802 case 'USEATTR': 4803 // Defined by RFC 6154 [3] 4804 throw new Horde_Imap_Client_Exception_ServerResponse( 4805 Horde_Imap_Client_Translation::r("The special-use attribute requested for the mailbox is not supported."), 4806 Horde_Imap_Client_Exception::USEATTR, 4807 $ob, 4808 $pipeline 4809 ); 4810 4811 case 'DOWNGRADED': 4812 // Defined by RFC 6858 [3] 4813 $downgraded = $this->getIdsOb($rc->data[0]); 4814 foreach ($pipeline->fetch as $val) { 4815 if (in_array($val->getUid(), $downgraded)) { 4816 $val->setDowngraded(true); 4817 } 4818 } 4819 break; 4820 4821 case 'XPROXYREUSE': 4822 // The proxy connection was reused, so no need to do login tasks. 4823 $pipeline->data['proxyreuse'] = true; 4824 break; 4825 4826 default: 4827 // Unknown response codes SHOULD be ignored - RFC 3501 [7.1] 4828 break; 4829 } 4830 } 4831 4832 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Thu Aug 11 10:00:09 2016 | Cross-referenced by PHPXref 0.7.1 |