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