See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
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($lr as $val_utf8 => $tmp) { 1547 $lr[$val_utf8]['status'] = $this->_prepareStatusResponse($status_opts, $val_utf8); 1548 } 1549 } 1550 1551 return $lr; 1552 } 1553 1554 /** 1555 * Parse a LIST/LSUB response (RFC 3501 [7.2.2 & 7.2.3]). 1556 * 1557 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 1558 * object. 1559 * @param Horde_Imap_Client_Tokenize $data The server response (includes 1560 * type as first token). 1561 * 1562 * @throws Horde_Imap_Client_Exception 1563 */ 1564 protected function _parseList( 1565 Horde_Imap_Client_Interaction_Pipeline $pipeline, 1566 Horde_Imap_Client_Tokenize $data 1567 ) 1568 { 1569 $data->next(); 1570 $attr = null; 1571 $attr_raw = $data->flushIterator(); 1572 $delimiter = $data->next(); 1573 $mbox = Horde_Imap_Client_Mailbox::get( 1574 $data->next(), 1575 !$this->_capability()->isEnabled('UTF8=ACCEPT') 1576 ); 1577 $ml = $pipeline->data['mailboxlist']; 1578 1579 switch ($ml['mode']) { 1580 case Horde_Imap_Client::MBOX_ALL_SUBSCRIBED: 1581 case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS: 1582 case Horde_Imap_Client::MBOX_UNSUBSCRIBED: 1583 $attr = array_flip(array_map('Horde_String::lower', $attr_raw)); 1584 1585 /* Subscribed list is in UTF-8. */ 1586 if (is_null($ml['sub']) && 1587 !isset($attr['\\subscribed']) && 1588 (strcasecmp($mbox, 'INBOX') === 0)) { 1589 $attr['\\subscribed'] = 1; 1590 } elseif (isset($ml['sub'][strval($mbox)])) { 1591 $attr['\\subscribed'] = 1; 1592 } 1593 break; 1594 } 1595 1596 switch ($ml['mode']) { 1597 case Horde_Imap_Client::MBOX_SUBSCRIBED_EXISTS: 1598 if (isset($attr['\\nonexistent']) || 1599 !isset($attr['\\subscribed'])) { 1600 return; 1601 } 1602 break; 1603 1604 case Horde_Imap_Client::MBOX_UNSUBSCRIBED: 1605 if (isset($attr['\\subscribed'])) { 1606 return; 1607 } 1608 break; 1609 } 1610 1611 if (!empty($ml['opts']['flat'])) { 1612 $pipeline->data['listresponse'][] = $mbox; 1613 return; 1614 } 1615 1616 $tmp = array( 1617 'delimiter' => $delimiter, 1618 'mailbox' => $mbox 1619 ); 1620 1621 if ($attr || !empty($ml['opts']['attributes'])) { 1622 if (is_null($attr)) { 1623 $attr = array_flip(array_map('Horde_String::lower', $attr_raw)); 1624 } 1625 1626 /* RFC 5258 [3.4]: inferred attributes. */ 1627 if ($ml['ext']) { 1628 if (isset($attr['\\noinferiors'])) { 1629 $attr['\\hasnochildren'] = 1; 1630 } 1631 if (isset($attr['\\nonexistent'])) { 1632 $attr['\\noselect'] = 1; 1633 } 1634 } 1635 $tmp['attributes'] = array_keys($attr); 1636 } 1637 1638 if ($data->next() !== false) { 1639 $tmp['extended'] = $data->flushIterator(); 1640 } 1641 1642 $pipeline->data['listresponse'][strval($mbox)] = $tmp; 1643 } 1644 1645 /** 1646 */ 1647 protected function _status($mboxes, $flags) 1648 { 1649 $on_error = null; 1650 $out = $to_process = array(); 1651 $pipeline = $this->_pipeline(); 1652 $unseen_flags = array( 1653 Horde_Imap_Client::STATUS_FIRSTUNSEEN, 1654 Horde_Imap_Client::STATUS_UNSEEN 1655 ); 1656 1657 foreach ($mboxes as $mailbox) { 1658 /* If FLAGS/PERMFLAGS/UIDNOTSTICKY/FIRSTUNSEEN are needed, we must 1659 * do a SELECT/EXAMINE to get this information (data will be 1660 * caught in the code below). */ 1661 if (($flags & Horde_Imap_Client::STATUS_FIRSTUNSEEN) || 1662 ($flags & Horde_Imap_Client::STATUS_FLAGS) || 1663 ($flags & Horde_Imap_Client::STATUS_PERMFLAGS) || 1664 ($flags & Horde_Imap_Client::STATUS_UIDNOTSTICKY)) { 1665 $this->openMailbox($mailbox); 1666 } 1667 1668 $mbox_ob = $this->_mailboxOb($mailbox); 1669 $data = $query = array(); 1670 1671 foreach ($this->_statusFields as $key => $val) { 1672 if (!($val & $flags)) { 1673 continue; 1674 } 1675 1676 if ($val == Horde_Imap_Client::STATUS_HIGHESTMODSEQ) { 1677 $c = $this->_capability(); 1678 1679 /* Don't include modseq returns if server does not support 1680 * it. */ 1681 if (!$c->query('CONDSTORE')) { 1682 continue; 1683 } 1684 1685 /* Even though CONDSTORE is available, it may not yet have 1686 * been enabled. */ 1687 $c->enable('CONDSTORE'); 1688 $on_error = function() use ($c) { 1689 $c->enable('CONDSTORE', false); 1690 }; 1691 } 1692 1693 if ($mailbox->equals($this->_selected)) { 1694 if (!is_null($tmp = $mbox_ob->getStatus($val))) { 1695 $data[$key] = $tmp; 1696 } elseif (($val == Horde_Imap_Client::STATUS_UIDNEXT) && 1697 ($flags & Horde_Imap_Client::STATUS_UIDNEXT_FORCE)) { 1698 /* UIDNEXT is not mandatory. */ 1699 if ($mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) == 0) { 1700 $data[$key] = 0; 1701 } else { 1702 $fquery = new Horde_Imap_Client_Fetch_Query(); 1703 $fquery->uid(); 1704 $fetch_res = $this->fetch($this->_selected, $fquery, array( 1705 'ids' => $this->getIdsOb(Horde_Imap_Client_Ids::LARGEST) 1706 )); 1707 $data[$key] = $fetch_res->first()->getUid() + 1; 1708 } 1709 } elseif (in_array($val, $unseen_flags)) { 1710 /* RFC 3501 [6.3.1] - FIRSTUNSEEN information is not 1711 * mandatory. If missing in EXAMINE/SELECT results, we 1712 * need to do a search. An UNSEEN count also requires 1713 * a search. */ 1714 $squery = new Horde_Imap_Client_Search_Query(); 1715 $squery->flag(Horde_Imap_Client::FLAG_SEEN, false); 1716 $search = $this->search($mailbox, $squery, array( 1717 'results' => array( 1718 Horde_Imap_Client::SEARCH_RESULTS_MIN, 1719 Horde_Imap_Client::SEARCH_RESULTS_COUNT 1720 ), 1721 'sequence' => true 1722 )); 1723 1724 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $search['min']); 1725 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UNSEEN, $search['count']); 1726 1727 $data[$key] = $mbox_ob->getStatus($val); 1728 } 1729 } else { 1730 $query[] = $key; 1731 } 1732 } 1733 1734 $out[strval($mailbox)] = $data; 1735 1736 if (count($query)) { 1737 $cmd = $this->_command('STATUS')->add(array( 1738 $this->_getMboxFormatOb($mailbox), 1739 new Horde_Imap_Client_Data_Format_List( 1740 array_map('Horde_String::upper', $query) 1741 ) 1742 )); 1743 $cmd->on_error = $on_error; 1744 1745 $pipeline->add($cmd); 1746 $to_process[] = array($query, $mailbox); 1747 } 1748 } 1749 1750 if (count($pipeline)) { 1751 $this->_sendCmd($pipeline); 1752 1753 foreach ($to_process as $val) { 1754 $out[strval($val[1])] += $this->_prepareStatusResponse($val[0], $val[1]); 1755 } 1756 } 1757 1758 return $out; 1759 } 1760 1761 /** 1762 * Parse a STATUS response (RFC 3501 [7.2.4]). 1763 * 1764 * @param Horde_Imap_Client_Tokenize $data Token data 1765 */ 1766 protected function _parseStatus(Horde_Imap_Client_Tokenize $data) 1767 { 1768 // Mailbox name is in UTF7-IMAP (unless UTF8 has been enabled). 1769 $mbox_ob = $this->_mailboxOb( 1770 Horde_Imap_Client_Mailbox::get( 1771 $data->next(), 1772 !$this->_capability()->isEnabled('UTF8=ACCEPT') 1773 ) 1774 ); 1775 1776 $data->next(); 1777 1778 while (($k = $data->next()) !== false) { 1779 $mbox_ob->setStatus( 1780 $this->_statusFields[Horde_String::lower($k)], 1781 $data->next() 1782 ); 1783 } 1784 } 1785 1786 /** 1787 * Prepares a status response for a mailbox. 1788 * 1789 * @param array $request The status keys to return. 1790 * @param string $mailbox The mailbox to query. 1791 */ 1792 protected function _prepareStatusResponse($request, $mailbox) 1793 { 1794 $mbox_ob = $this->_mailboxOb($mailbox); 1795 $out = array(); 1796 1797 foreach ($request as $val) { 1798 $out[$val] = $mbox_ob->getStatus($this->_statusFields[$val]); 1799 } 1800 1801 return $out; 1802 } 1803 1804 /** 1805 */ 1806 protected function _append(Horde_Imap_Client_Mailbox $mailbox, $data, 1807 $options) 1808 { 1809 $c = $this->_capability(); 1810 1811 // Check for MULTIAPPEND extension (RFC 3502) 1812 if ((count($data) > 1) && !$c->query('MULTIAPPEND')) { 1813 $result = $this->getIdsOb(); 1814 foreach (array_keys($data) as $key) { 1815 $res = $this->_append($mailbox, array($data[$key]), $options); 1816 if (($res === true) || ($result === true)) { 1817 $result = true; 1818 } else { 1819 $result->add($res); 1820 } 1821 } 1822 return $result; 1823 } 1824 1825 // Check for extensions. 1826 $binary = $c->query('BINARY'); 1827 $catenate = $c->query('CATENATE'); 1828 $utf8 = $c->isEnabled('UTF8=ACCEPT'); 1829 1830 $asize = 0; 1831 1832 $cmd = $this->_command('APPEND')->add( 1833 $this->_getMboxFormatOb($mailbox) 1834 ); 1835 $cmd->literal8 = true; 1836 1837 foreach (array_keys($data) as $key) { 1838 if (!empty($data[$key]['flags'])) { 1839 $tmp = new Horde_Imap_Client_Data_Format_List(); 1840 foreach ($data[$key]['flags'] as $val) { 1841 /* Ignore recent flag. RFC 3501 [9]: flag definition */ 1842 if (strcasecmp($val, Horde_Imap_Client::FLAG_RECENT) !== 0) { 1843 $tmp->add($val); 1844 } 1845 } 1846 $cmd->add($tmp); 1847 } 1848 1849 if (!empty($data[$key]['internaldate'])) { 1850 $cmd->add(new Horde_Imap_Client_Data_Format_DateTime($data[$key]['internaldate'])); 1851 } 1852 1853 $adata = null; 1854 1855 if (is_array($data[$key]['data'])) { 1856 if ($catenate) { 1857 $cmd->add('CATENATE'); 1858 $tmp = new Horde_Imap_Client_Data_Format_List(); 1859 } else { 1860 $data_stream = new Horde_Stream_Temp(); 1861 } 1862 1863 foreach ($data[$key]['data'] as $v) { 1864 switch ($v['t']) { 1865 case 'text': 1866 if ($catenate) { 1867 $tdata = $this->_appendData($v['v'], $asize); 1868 if ($utf8) { 1869 /* RFC 6855 [4]: CATENATE UTF8 extension. */ 1870 $tdata->forceBinary(); 1871 $tmp->add(array( 1872 'UTF8', 1873 new Horde_Imap_Client_Data_Format_List($tdata) 1874 )); 1875 } else { 1876 $tmp->add(array( 1877 'TEXT', 1878 $tdata 1879 )); 1880 } 1881 } else { 1882 if (is_resource($v['v'])) { 1883 rewind($v['v']); 1884 } 1885 $data_stream->add($v['v']); 1886 } 1887 break; 1888 1889 case 'url': 1890 if ($catenate) { 1891 $tmp->add(array( 1892 'URL', 1893 new Horde_Imap_Client_Data_Format_Astring($v['v']) 1894 )); 1895 } else { 1896 $data_stream->add($this->_convertCatenateUrl($v['v'])); 1897 } 1898 break; 1899 } 1900 } 1901 1902 if ($catenate) { 1903 $cmd->add($tmp); 1904 } else { 1905 $adata = $this->_appendData($data_stream->stream, $asize); 1906 } 1907 } else { 1908 $adata = $this->_appendData($data[$key]['data'], $asize); 1909 } 1910 1911 if (!is_null($adata)) { 1912 if ($utf8) { 1913 /* RFC 6855 [4]: APPEND UTF8 extension. */ 1914 $adata->forceBinary(); 1915 $cmd->add(array( 1916 'UTF8', 1917 new Horde_Imap_Client_Data_Format_List($adata) 1918 )); 1919 } else { 1920 $cmd->add($adata); 1921 } 1922 } 1923 } 1924 1925 /* Although it is normally more efficient to use LITERAL+, disable if 1926 * payload is over 50 KB because it allows the server to throw error 1927 * before we potentially push a lot of data to server that would 1928 * otherwise be ignored (see RFC 4549 [4.2.2.3]). 1929 * Additionally, since so many IMAP servers have issues with APPEND 1930 * + BINARY, don't use LITERAL+ since servers may send BAD 1931 * (incorrectly) after initial command. */ 1932 $cmd->literalplus = (($asize < (1024 * 50)) && !$binary); 1933 1934 // If the mailbox is currently selected read-only, we need to close 1935 // because some IMAP implementations won't allow an append. And some 1936 // implementations don't support append on ANY open mailbox. Be safe 1937 // and always make sure we are in a non-selected state. 1938 $this->close(); 1939 1940 try { 1941 $resp = $this->_sendCmd($cmd); 1942 } catch (Horde_Imap_Client_Exception $e) { 1943 switch ($e->getCode()) { 1944 case $e::CATENATE_BADURL: 1945 case $e::CATENATE_TOOBIG: 1946 /* Cyrus 2.4 (at least as of .14) has a broken CATENATE (see 1947 * Bug #11111). Regardless, if CATENATE is broken, we can try 1948 * to fallback to APPEND. */ 1949 $c->remove('CATENATE'); 1950 return $this->_append($mailbox, $data, $options); 1951 1952 case $e::DISCONNECT: 1953 /* Workaround broken literal8 on Cyrus. */ 1954 if ($binary) { 1955 // Need to re-login first before removing capability. 1956 $this->login(); 1957 $c->remove('BINARY'); 1958 return $this->_append($mailbox, $data, $options); 1959 } 1960 break; 1961 } 1962 1963 if (!empty($options['create']) && 1964 !empty($e->resp_data['trycreate'])) { 1965 $this->createMailbox($mailbox); 1966 unset($options['create']); 1967 return $this->_append($mailbox, $data, $options); 1968 } 1969 1970 /* RFC 3516/4466 says we should be able to append binary data 1971 * using literal8 "~{#} format", but it doesn't seem to work on 1972 * all servers tried (UW-IMAP/Cyrus). Do a last-ditch check for 1973 * broken BINARY and attempt to fix here. */ 1974 if ($c->query('BINARY') && 1975 ($e instanceof Horde_Imap_Client_Exception_ServerResponse)) { 1976 switch ($e->status) { 1977 case Horde_Imap_Client_Interaction_Server::BAD: 1978 case Horde_Imap_Client_Interaction_Server::NO: 1979 $c->remove('BINARY'); 1980 return $this->_append($mailbox, $data, $options); 1981 } 1982 } 1983 1984 throw $e; 1985 } 1986 1987 /* If we reach this point and have data in 'appenduid', UIDPLUS (RFC 1988 * 4315) has done the dirty work for us. */ 1989 return isset($resp->data['appenduid']) 1990 ? $resp->data['appenduid'] 1991 : true; 1992 } 1993 1994 /** 1995 * Prepares append message data for insertion into the IMAP command 1996 * string. 1997 * 1998 * @param mixed $data Either a resource or a string. 1999 * @param integer &$asize Total append size. 2000 * 2001 * @return Horde_Imap_Client_Data_Format_String_Nonascii The data object. 2002 */ 2003 protected function _appendData($data, &$asize) 2004 { 2005 if (is_resource($data)) { 2006 rewind($data); 2007 } 2008 2009 /* Since this is body text, with possible embedded charset 2010 * information, non-ASCII characters are supported. */ 2011 $ob = new Horde_Imap_Client_Data_Format_String_Nonascii($data, array( 2012 'eol' => true, 2013 'skipscan' => true 2014 )); 2015 2016 // APPEND data MUST be sent in a literal (RFC 3501 [6.3.11]). 2017 $ob->forceLiteral(); 2018 2019 $asize += $ob->length(); 2020 2021 return $ob; 2022 } 2023 2024 /** 2025 * Converts a CATENATE URL to stream data. 2026 * 2027 * @param string $url The CATENATE URL. 2028 * 2029 * @return resource A stream containing the data. 2030 */ 2031 protected function _convertCatenateUrl($url) 2032 { 2033 $e = $part = null; 2034 $url = new Horde_Imap_Client_Url_Imap($url); 2035 2036 if (!is_null($url->mailbox) && !is_null($url->uid)) { 2037 try { 2038 $status_res = is_null($url->uidvalidity) 2039 ? null 2040 : $this->status($url->mailbox, Horde_Imap_Client::STATUS_UIDVALIDITY); 2041 2042 if (is_null($status_res) || 2043 ($status_res['uidvalidity'] == $url->uidvalidity)) { 2044 if (!isset($this->_temp['catenate_ob'])) { 2045 $this->_temp['catenate_ob'] = new Horde_Imap_Client_Socket_Catenate($this); 2046 } 2047 $part = $this->_temp['catenate_ob']->fetchFromUrl($url); 2048 } 2049 } catch (Horde_Imap_Client_Exception $e) {} 2050 } 2051 2052 if (is_null($part)) { 2053 $message = 'Bad IMAP URL given in CATENATE data: ' . strval($url); 2054 if ($e) { 2055 $message .= ' ' . $e->getMessage(); 2056 } 2057 2058 throw new InvalidArgumentException($message); 2059 } 2060 2061 return $part; 2062 } 2063 2064 /** 2065 */ 2066 protected function _check() 2067 { 2068 // CHECK returns no untagged information (RFC 3501 [6.4.1]) 2069 $this->_sendCmd($this->_command('CHECK')); 2070 } 2071 2072 /** 2073 */ 2074 protected function _close($options) 2075 { 2076 if (empty($options['expunge'])) { 2077 if ($this->_capability('UNSELECT')) { 2078 // RFC 3691 defines 'UNSELECT' for precisely this purpose 2079 $this->_sendCmd($this->_command('UNSELECT')); 2080 } else { 2081 /* RFC 3501 [6.4.2]: to close a mailbox without expunge, 2082 * select a non-existent mailbox. */ 2083 try { 2084 $this->_sendCmd($this->_command('EXAMINE')->add( 2085 $this->_getMboxFormatOb("\24nonexist\24") 2086 )); 2087 2088 /* Not pipelining, since the odds that this CLOSE is even 2089 * needed is tiny; and it returns BAD, which should be 2090 * avoided, if possible. */ 2091 $this->_sendCmd($this->_command('CLOSE')); 2092 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 2093 // Ignore error; it is expected. 2094 } 2095 } 2096 } else { 2097 // If caching, we need to know the UIDs being deleted, so call 2098 // expunge() before calling close(). 2099 if ($this->_initCache(true)) { 2100 $this->expunge($this->_selected); 2101 } 2102 2103 // CLOSE returns no untagged information (RFC 3501 [6.4.2]) 2104 $this->_sendCmd($this->_command('CLOSE')); 2105 } 2106 } 2107 2108 /** 2109 */ 2110 protected function _expunge($options) 2111 { 2112 $expunged_ob = $modseq = null; 2113 $ids = $options['ids']; 2114 $list_msgs = !empty($options['list']); 2115 $mailbox = $this->_selected; 2116 $uidplus = $this->_capability('UIDPLUS'); 2117 $unflag = array(); 2118 $use_cache = $this->_initCache(true); 2119 2120 if ($ids->all) { 2121 if (!$uidplus || $list_msgs || $use_cache) { 2122 $ids = $this->resolveIds($mailbox, $ids, 2); 2123 } 2124 } elseif ($uidplus) { 2125 /* If QRESYNC is not available, and we are returning the list of 2126 * expunged messages (or we are caching), we have to make sure we 2127 * have a mapping of Sequence -> UIDs. If we have QRESYNC, the 2128 * server SHOULD return a VANISHED response with UIDs. However, 2129 * even if the server returns EXPUNGEs instead, we can use 2130 * vanished() to grab the list. */ 2131 unset($this->_temp['search_save']); 2132 if ($this->_capability()->isEnabled('QRESYNC')) { 2133 $ids = $this->resolveIds($mailbox, $ids, 1); 2134 if ($list_msgs) { 2135 $modseq = $this->_mailboxOb()->getStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ); 2136 } 2137 } else { 2138 $ids = $this->resolveIds($mailbox, $ids, ($list_msgs || $use_cache) ? 2 : 1); 2139 } 2140 if (!empty($this->_temp['search_save'])) { 2141 $ids = $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES); 2142 } 2143 } else { 2144 /* Without UIDPLUS, need to temporarily unflag all messages marked 2145 * as deleted but not a part of requested IDs to delete. Use NOT 2146 * searches to accomplish this goal. */ 2147 $squery = new Horde_Imap_Client_Search_Query(); 2148 $squery->flag(Horde_Imap_Client::FLAG_DELETED, true); 2149 $squery->ids($ids, true); 2150 2151 $s_res = $this->search($mailbox, $squery, array( 2152 'results' => array( 2153 Horde_Imap_Client::SEARCH_RESULTS_MATCH, 2154 Horde_Imap_Client::SEARCH_RESULTS_SAVE 2155 ) 2156 )); 2157 2158 $this->store($mailbox, array( 2159 'ids' => empty($s_res['save']) ? $s_res['match'] : $this->getIdsOb(Horde_Imap_Client_Ids::SEARCH_RES), 2160 'remove' => array(Horde_Imap_Client::FLAG_DELETED) 2161 )); 2162 2163 $unflag = $s_res['match']; 2164 } 2165 2166 if ($list_msgs) { 2167 $expunged_ob = $this->getIdsOb(); 2168 $this->_temp['expunged'] = $expunged_ob; 2169 } 2170 2171 /* Always use UID EXPUNGE if available. */ 2172 if ($uidplus) { 2173 /* We can only pipeline STORE w/ EXPUNGE if using UIDs and UIDPLUS 2174 * is available. */ 2175 if (empty($options['delete'])) { 2176 $pipeline = $this->_pipeline(); 2177 } else { 2178 $pipeline = $this->_storeCmd(array( 2179 'add' => array( 2180 Horde_Imap_Client::FLAG_DELETED 2181 ), 2182 'ids' => $ids 2183 )); 2184 } 2185 2186 foreach ($ids->split(2000) as $val) { 2187 $pipeline->add( 2188 $this->_command('UID EXPUNGE')->add($val) 2189 ); 2190 } 2191 2192 $resp = $this->_sendCmd($pipeline); 2193 } else { 2194 if (!empty($options['delete'])) { 2195 $this->store($mailbox, array( 2196 'add' => array(Horde_Imap_Client::FLAG_DELETED), 2197 'ids' => $ids 2198 )); 2199 } 2200 2201 if ($use_cache || $list_msgs) { 2202 $this->_sendCmd($this->_command('EXPUNGE')); 2203 } else { 2204 /* This is faster than an EXPUNGE because the server will not 2205 * return untagged EXPUNGE responses. We can only do this if 2206 * we are not updating cache information. */ 2207 $this->close(array('expunge' => true)); 2208 } 2209 } 2210 2211 unset($this->_temp['expunged']); 2212 2213 if (!empty($unflag)) { 2214 $this->store($mailbox, array( 2215 'add' => array(Horde_Imap_Client::FLAG_DELETED), 2216 'ids' => $unflag 2217 )); 2218 } 2219 2220 if (!is_null($modseq) && !empty($resp->data['expunge_seen'])) { 2221 /* There's a chance we actually did a full map of sequence -> UID, 2222 * but this code should never be reached in the first place so 2223 * be ultra-safe and just do a full VANISHED search. */ 2224 $expunged_ob = $this->vanished($mailbox, $modseq, array( 2225 'ids' => $ids 2226 )); 2227 $this->_deleteMsgs($mailbox, $expunged_ob, array( 2228 'pipeline' => $resp 2229 )); 2230 } 2231 2232 return $expunged_ob; 2233 } 2234 2235 /** 2236 * Parse a VANISHED response (RFC 7162 [3.2.10]). 2237 * 2238 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2239 * object. 2240 * @param Horde_Imap_Client_Tokenize $data The response data. 2241 */ 2242 protected function _parseVanished( 2243 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2244 Horde_Imap_Client_Tokenize $data 2245 ) 2246 { 2247 /* There are two forms of VANISHED. VANISHED (EARLIER) will be sent 2248 * in a FETCH (VANISHED) or SELECT/EXAMINE (QRESYNC) call. 2249 * If this is the case, we can go ahead and update the cache 2250 * immediately (we know we are caching or else QRESYNC would not be 2251 * enabled). HIGHESTMODSEQ information will be updated via the tagged 2252 * response. */ 2253 if (($curr = $data->next()) === true) { 2254 if (Horde_String::upper($data->next()) === 'EARLIER') { 2255 /* Caching is guaranteed to be active if we are using 2256 * QRESYNC. */ 2257 $data->next(); 2258 $vanished = $this->getIdsOb($data->next()); 2259 if (isset($pipeline->data['vanished'])) { 2260 $pipeline->data['vanished']->add($vanished); 2261 } else { 2262 $this->_deleteMsgs($this->_selected, $vanished, array( 2263 'pipeline' => $pipeline 2264 )); 2265 } 2266 } 2267 } else { 2268 /* The second form is just VANISHED. This is analogous to EXPUNGE 2269 * and requires the message count to decrement. */ 2270 $this->_deleteMsgs($this->_selected, $this->getIdsOb($curr), array( 2271 'decrement' => true, 2272 'pipeline' => $pipeline 2273 )); 2274 } 2275 } 2276 2277 /** 2278 * Search a mailbox. This driver supports all IMAP4rev1 search criteria 2279 * as defined in RFC 3501. 2280 */ 2281 protected function _search($query, $options) 2282 { 2283 $sort_criteria = array( 2284 Horde_Imap_Client::SORT_ARRIVAL => 'ARRIVAL', 2285 Horde_Imap_Client::SORT_CC => 'CC', 2286 Horde_Imap_Client::SORT_DATE => 'DATE', 2287 Horde_Imap_Client::SORT_DISPLAYFROM => 'DISPLAYFROM', 2288 Horde_Imap_Client::SORT_DISPLAYTO => 'DISPLAYTO', 2289 Horde_Imap_Client::SORT_FROM => 'FROM', 2290 Horde_Imap_Client::SORT_REVERSE => 'REVERSE', 2291 Horde_Imap_Client::SORT_RELEVANCY => 'RELEVANCY', 2292 // This is a bogus entry to allow the sort options check to 2293 // correctly work below. 2294 Horde_Imap_Client::SORT_SEQUENCE => 'SEQUENCE', 2295 Horde_Imap_Client::SORT_SIZE => 'SIZE', 2296 Horde_Imap_Client::SORT_SUBJECT => 'SUBJECT', 2297 Horde_Imap_Client::SORT_TO => 'TO' 2298 ); 2299 2300 $results_criteria = array( 2301 Horde_Imap_Client::SEARCH_RESULTS_COUNT => 'COUNT', 2302 Horde_Imap_Client::SEARCH_RESULTS_MATCH => 'ALL', 2303 Horde_Imap_Client::SEARCH_RESULTS_MAX => 'MAX', 2304 Horde_Imap_Client::SEARCH_RESULTS_MIN => 'MIN', 2305 Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY => 'RELEVANCY', 2306 Horde_Imap_Client::SEARCH_RESULTS_SAVE => 'SAVE' 2307 ); 2308 2309 // Check if the server supports sorting (RFC 5256). 2310 $esearch = $return_sort = $server_seq_sort = $server_sort = false; 2311 if (!empty($options['sort'])) { 2312 /* Make sure sort options are correct. If not, default to no 2313 * sort. */ 2314 if (count(array_intersect($options['sort'], array_keys($sort_criteria))) === 0) { 2315 unset($options['sort']); 2316 } else { 2317 $return_sort = true; 2318 2319 if ($this->_capability('SORT')) { 2320 /* Make sure server supports DISPLAYFROM & DISPLAYTO. */ 2321 $server_sort = 2322 !array_intersect($options['sort'], array(Horde_Imap_Client::SORT_DISPLAYFROM, Horde_Imap_Client::SORT_DISPLAYTO)) || 2323 $this->_capability('SORT', 'DISPLAY'); 2324 } 2325 2326 /* If doing a sequence sort, need to do this on the client 2327 * side. */ 2328 if ($server_sort && 2329 in_array(Horde_Imap_Client::SORT_SEQUENCE, $options['sort'])) { 2330 $server_sort = false; 2331 2332 /* Optimization: If doing only a sequence sort, just do a 2333 * simple search and sort UIDs/sequences on client side. */ 2334 switch (count($options['sort'])) { 2335 case 1: 2336 $server_seq_sort = true; 2337 break; 2338 2339 case 2: 2340 $server_seq_sort = (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE); 2341 break; 2342 } 2343 } 2344 } 2345 } 2346 2347 $charset = is_null($options['_query']['charset']) 2348 ? 'US-ASCII' 2349 : $options['_query']['charset']; 2350 $partial = false; 2351 2352 if ($server_sort) { 2353 $cmd = $this->_command( 2354 empty($options['sequence']) ? 'UID SORT' : 'SORT' 2355 ); 2356 $results = array(); 2357 2358 // Use ESEARCH (RFC 4466) response if server supports. 2359 $esearch = false; 2360 2361 // Check for ESORT capability (RFC 5267) 2362 if ($this->_capability('ESORT')) { 2363 foreach ($options['results'] as $val) { 2364 if (isset($results_criteria[$val]) && 2365 ($val != Horde_Imap_Client::SEARCH_RESULTS_SAVE)) { 2366 $results[] = $results_criteria[$val]; 2367 } 2368 } 2369 $esearch = true; 2370 } 2371 2372 // Add PARTIAL limiting (RFC 5267 [4.4]) 2373 if ((!$esearch || !empty($options['partial'])) && 2374 $this->_capability('CONTEXT', 'SORT')) { 2375 /* RFC 5267 indicates RFC 4466 ESEARCH-like support, 2376 * notwithstanding "real" RFC 4731 support. */ 2377 $esearch = true; 2378 2379 if (!empty($options['partial'])) { 2380 /* Can't have both ALL and PARTIAL returns. */ 2381 $results = array_diff($results, array('ALL')); 2382 2383 $results[] = 'PARTIAL'; 2384 $results[] = $options['partial']; 2385 $partial = true; 2386 } 2387 } 2388 2389 if ($esearch && empty($this->_init['noesearch'])) { 2390 $cmd->add(array( 2391 'RETURN', 2392 new Horde_Imap_Client_Data_Format_List($results) 2393 )); 2394 } 2395 2396 $tmp = new Horde_Imap_Client_Data_Format_List(); 2397 foreach ($options['sort'] as $val) { 2398 if (isset($sort_criteria[$val])) { 2399 $tmp->add($sort_criteria[$val]); 2400 } 2401 } 2402 $cmd->add($tmp); 2403 2404 /* Charset is mandatory for SORT (RFC 5256 [3]). 2405 * If UTF-8 support is activated, a client MUST ONLY 2406 * send the 'UTF-8' specification (RFC 6855 [3]; Errata 4029). */ 2407 if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) { 2408 $cmd->add($charset); 2409 } else { 2410 $cmd->add('UTF-8'); 2411 } 2412 } else { 2413 $cmd = $this->_command( 2414 empty($options['sequence']) ? 'UID SEARCH' : 'SEARCH' 2415 ); 2416 $esearch = false; 2417 $results = array(); 2418 2419 // Check if the server supports ESEARCH (RFC 4731). 2420 if ($this->_capability('ESEARCH')) { 2421 foreach ($options['results'] as $val) { 2422 if (isset($results_criteria[$val])) { 2423 $results[] = $results_criteria[$val]; 2424 } 2425 } 2426 $esearch = true; 2427 } 2428 2429 // Add PARTIAL limiting (RFC 5267 [4.4]). 2430 if ((!$esearch || !empty($options['partial'])) && 2431 $this->_capability('CONTEXT', 'SEARCH')) { 2432 /* RFC 5267 indicates RFC 4466 ESEARCH-like support, 2433 * notwithstanding "real" RFC 4731 support. */ 2434 $esearch = true; 2435 2436 if (!empty($options['partial'])) { 2437 // Can't have both ALL and PARTIAL returns. 2438 $results = array_diff($results, array('ALL')); 2439 2440 $results[] = 'PARTIAL'; 2441 $results[] = $options['partial']; 2442 $partial = true; 2443 } 2444 } 2445 2446 if ($esearch && empty($this->_init['noesearch'])) { 2447 // Always use ESEARCH if available because it returns results 2448 // in a more compact sequence-set list 2449 $cmd->add(array( 2450 'RETURN', 2451 new Horde_Imap_Client_Data_Format_List($results) 2452 )); 2453 } 2454 2455 /* Charset is optional for SEARCH (RFC 3501 [6.4.4]). 2456 * If UTF-8 support is activated, a client MUST NOT 2457 * send the charset specification (RFC 6855 [3]; Errata 4029). */ 2458 if (($charset != 'US-ASCII') && 2459 !$this->_capability()->isEnabled('UTF8=ACCEPT')) { 2460 $cmd->add(array( 2461 'CHARSET', 2462 $options['_query']['charset'] 2463 )); 2464 } 2465 } 2466 2467 $cmd->add($options['_query']['query'], true); 2468 2469 $pipeline = $this->_pipeline($cmd); 2470 $pipeline->data['esearchresp'] = array(); 2471 $er = &$pipeline->data['esearchresp']; 2472 $pipeline->data['searchresp'] = $this->getIdsOb(array(), !empty($options['sequence'])); 2473 $sr = &$pipeline->data['searchresp']; 2474 2475 try { 2476 $resp = $this->_sendCmd($pipeline); 2477 } catch (Horde_Imap_Client_Exception $e) { 2478 if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) && 2479 ($e->status === Horde_Imap_Client_Interaction_Server::NO) && 2480 ($charset != 'US-ASCII')) { 2481 /* RFC 3501 [6.4.4]: BADCHARSET response code is only a 2482 * SHOULD return. If it doesn't exist, need to check for 2483 * command status of 'NO'. List of supported charsets in 2484 * the BADCHARSET response has already been parsed and stored 2485 * at this point. */ 2486 $this->search_charset->setValid($charset, false); 2487 $e->setCode(Horde_Imap_Client_Exception::BADCHARSET); 2488 } 2489 2490 if (empty($this->_temp['search_retry'])) { 2491 $this->_temp['search_retry'] = true; 2492 2493 /* Bug #9842: Workaround broken Cyrus servers (as of 2494 * 2.4.7). */ 2495 if ($esearch && ($charset != 'US-ASCII')) { 2496 $this->_capability()->remove('ESEARCH'); 2497 $this->_setInit('noesearch', true); 2498 2499 try { 2500 return $this->_search($query, $options); 2501 } catch (Horde_Imap_Client_Exception $e) {} 2502 } 2503 2504 /* Try to convert charset. */ 2505 if (($e->getCode() === Horde_Imap_Client_Exception::BADCHARSET) && 2506 ($charset != 'US-ASCII')) { 2507 foreach ($this->search_charset->charsets as $val) { 2508 $this->_temp['search_retry'] = 1; 2509 $new_query = clone($query); 2510 try { 2511 $new_query->charset($val); 2512 $options['_query'] = $new_query->build($this); 2513 return $this->_search($new_query, $options); 2514 } catch (Horde_Imap_Client_Exception $e) {} 2515 } 2516 } 2517 2518 unset($this->_temp['search_retry']); 2519 } 2520 2521 throw $e; 2522 } 2523 2524 if ($return_sort && !$server_sort) { 2525 if ($server_seq_sort) { 2526 $sr->sort(); 2527 if (reset($options['sort']) == Horde_Imap_Client::SORT_REVERSE) { 2528 $sr->reverse(); 2529 } 2530 } else { 2531 if (!isset($this->_temp['clientsort'])) { 2532 $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this); 2533 } 2534 $sr = $this->getIdsOb($this->_temp['clientsort']->clientSort($sr, $options), !empty($options['sequence'])); 2535 } 2536 } 2537 2538 if (!$partial && !empty($options['partial'])) { 2539 $partial = $this->getIdsOb($options['partial'], true); 2540 $min = $partial->min - 1; 2541 2542 $sr = $this->getIdsOb( 2543 array_slice($sr->ids, $min, $partial->max - $min), 2544 !empty($options['sequence']) 2545 ); 2546 } 2547 2548 $ret = array(); 2549 foreach ($options['results'] as $val) { 2550 switch ($val) { 2551 case Horde_Imap_Client::SEARCH_RESULTS_COUNT: 2552 $ret['count'] = ($esearch && !$partial) 2553 ? $er['count'] 2554 : count($sr); 2555 break; 2556 2557 case Horde_Imap_Client::SEARCH_RESULTS_MATCH: 2558 $ret['match'] = $sr; 2559 break; 2560 2561 case Horde_Imap_Client::SEARCH_RESULTS_MAX: 2562 $ret['max'] = $esearch 2563 ? (!$partial && isset($er['max']) ? $er['max'] : null) 2564 : (count($sr) ? max($sr->ids) : null); 2565 break; 2566 2567 case Horde_Imap_Client::SEARCH_RESULTS_MIN: 2568 $ret['min'] = $esearch 2569 ? (!$partial && isset($er['min']) ? $er['min'] : null) 2570 : (count($sr) ? min($sr->ids) : null); 2571 break; 2572 2573 case Horde_Imap_Client::SEARCH_RESULTS_RELEVANCY: 2574 $ret['relevancy'] = ($esearch && isset($er['relevancy'])) ? $er['relevancy'] : array(); 2575 break; 2576 2577 case Horde_Imap_Client::SEARCH_RESULTS_SAVE: 2578 $this->_temp['search_save'] = $ret['save'] = $esearch ? empty($resp->data['searchnotsaved']) : false; 2579 break; 2580 } 2581 } 2582 2583 // Add modseq data, if needed. 2584 if (!empty($er['modseq'])) { 2585 $ret['modseq'] = $er['modseq']; 2586 } 2587 2588 unset($this->_temp['search_retry']); 2589 2590 /* Check for EXPUNGEISSUED (RFC 2180 [4.3]/RFC 5530 [3]). */ 2591 if (!empty($resp->data['expungeissued'])) { 2592 $this->noop(); 2593 } 2594 2595 return $ret; 2596 } 2597 2598 /** 2599 * Parse a SEARCH/SORT response (RFC 3501 [7.2.5]; RFC 4466 [3]; 2600 * RFC 5256 [4]; RFC 5267 [3]). 2601 * 2602 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2603 * object. 2604 * @param array $data A list of IDs (message sequence numbers or UIDs). 2605 */ 2606 protected function _parseSearch( 2607 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2608 $data 2609 ) 2610 { 2611 /* More than one search response may be sent. */ 2612 $pipeline->data['searchresp']->add($data); 2613 } 2614 2615 /** 2616 * Parse an ESEARCH response (RFC 4466 [2.6.2]) 2617 * Format: (TAG "a567") UID COUNT 5 ALL 4:19,21,28 2618 * 2619 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2620 * object. 2621 * @param Horde_Imap_Client_Tokenize $data The server response. 2622 */ 2623 protected function _parseEsearch( 2624 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2625 Horde_Imap_Client_Tokenize $data 2626 ) 2627 { 2628 // Ignore search correlator information 2629 if ($data->next() === true) { 2630 $data->flushIterator(false); 2631 } 2632 2633 // Ignore UID tag 2634 $current = $data->next(); 2635 if (Horde_String::upper($current) === 'UID') { 2636 $current = $data->next(); 2637 } 2638 2639 do { 2640 $val = $data->next(); 2641 $tag = Horde_String::upper($current); 2642 2643 switch ($tag) { 2644 case 'ALL': 2645 $this->_parseSearch($pipeline, $val); 2646 break; 2647 2648 case 'COUNT': 2649 case 'MAX': 2650 case 'MIN': 2651 case 'MODSEQ': 2652 case 'RELEVANCY': 2653 $pipeline->data['esearchresp'][Horde_String::lower($tag)] = $val; 2654 break; 2655 2656 case 'PARTIAL': 2657 // RFC 5267 [4.4] 2658 $partial = $val->flushIterator(); 2659 $this->_parseSearch($pipeline, end($partial)); 2660 break; 2661 } 2662 } while (($current = $data->next()) !== false); 2663 } 2664 2665 /** 2666 */ 2667 protected function _setComparator($comparator) 2668 { 2669 $cmd = $this->_command('COMPARATOR'); 2670 foreach ($comparator as $val) { 2671 $cmd->add(new Horde_Imap_Client_Data_Format_Astring($val)); 2672 } 2673 $this->_sendCmd($cmd); 2674 } 2675 2676 /** 2677 */ 2678 protected function _getComparator() 2679 { 2680 $resp = $this->_sendCmd($this->_command('COMPARATOR')); 2681 2682 return isset($resp->data['comparator']) 2683 ? $resp->data['comparator'] 2684 : null; 2685 } 2686 2687 /** 2688 * Parse a COMPARATOR response (RFC 5255 [4.8]) 2689 * 2690 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2691 * object. 2692 * @param Horde_Imap_Client_Tokenize $data The server response. 2693 */ 2694 protected function _parseComparator( 2695 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2696 $data 2697 ) 2698 { 2699 $pipeline->data['comparator'] = $data->next(); 2700 // Ignore optional matching comparator list 2701 } 2702 2703 /** 2704 * @throws Horde_Imap_Client_Exception_NoSupportExtension 2705 */ 2706 protected function _thread($options) 2707 { 2708 $thread_criteria = array( 2709 Horde_Imap_Client::THREAD_ORDEREDSUBJECT => 'ORDEREDSUBJECT', 2710 Horde_Imap_Client::THREAD_REFERENCES => 'REFERENCES', 2711 Horde_Imap_Client::THREAD_REFS => 'REFS' 2712 ); 2713 2714 $tsort = (isset($options['criteria'])) 2715 ? (is_string($options['criteria']) ? Horde_String::upper($options['criteria']) : $thread_criteria[$options['criteria']]) 2716 : 'ORDEREDSUBJECT'; 2717 2718 if (!$this->_capability('THREAD', $tsort)) { 2719 switch ($tsort) { 2720 case 'ORDEREDSUBJECT': 2721 if (empty($options['search'])) { 2722 $ids = $this->getIdsOb(Horde_Imap_Client_Ids::ALL, !empty($options['sequence'])); 2723 } else { 2724 $search_res = $this->search($this->_selected, $options['search'], array('sequence' => !empty($options['sequence']))); 2725 $ids = $search_res['match']; 2726 } 2727 2728 /* Do client-side ORDEREDSUBJECT threading. */ 2729 $query = new Horde_Imap_Client_Fetch_Query(); 2730 $query->envelope(); 2731 $query->imapDate(); 2732 2733 $fetch_res = $this->fetch($this->_selected, $query, array( 2734 'ids' => $ids 2735 )); 2736 2737 if (!isset($this->_temp['clientsort'])) { 2738 $this->_temp['clientsort'] = new Horde_Imap_Client_Socket_ClientSort($this); 2739 } 2740 return $this->_temp['clientsort']->threadOrderedSubject($fetch_res, empty($options['sequence'])); 2741 2742 case 'REFERENCES': 2743 case 'REFS': 2744 throw new Horde_Imap_Client_Exception_NoSupportExtension( 2745 'THREAD', 2746 sprintf('Server does not support "%s" thread sort.', $tsort) 2747 ); 2748 } 2749 } 2750 2751 $cmd = $this->_command( 2752 empty($options['sequence']) ? 'UID THREAD' : 'THREAD' 2753 )->add($tsort); 2754 2755 /* If UTF-8 support is activated, a client MUST send the UTF-8 2756 * charset specification since charset is mandatory for this 2757 * command (RFC 6855 [3]; Errata 4029). */ 2758 if (empty($options['search'])) { 2759 if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) { 2760 $cmd->add('US-ASCII'); 2761 } else { 2762 $cmd->add('UTF-8'); 2763 } 2764 $cmd->add('ALL'); 2765 } else { 2766 $search_query = $options['search']->build(); 2767 if (!$this->_capability()->isEnabled('UTF8=ACCEPT')) { 2768 $cmd->add(is_null($search_query['charset']) ? 'US-ASCII' : $search_query['charset']); 2769 } 2770 $cmd->add($search_query['query'], true); 2771 } 2772 2773 return new Horde_Imap_Client_Data_Thread( 2774 $this->_sendCmd($cmd)->data['threadparse'], 2775 empty($options['sequence']) ? 'uid' : 'sequence' 2776 ); 2777 } 2778 2779 /** 2780 * Parse a THREAD response (RFC 5256 [4]). 2781 * 2782 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2783 * object. 2784 * @param Horde_Imap_Client_Tokenize $data Thread data. 2785 */ 2786 protected function _parseThread( 2787 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2788 Horde_Imap_Client_Tokenize $data 2789 ) 2790 { 2791 $out = array(); 2792 2793 while ($data->next() !== false) { 2794 $thread = array(); 2795 $this->_parseThreadLevel($thread, $data); 2796 $out[] = $thread; 2797 } 2798 2799 $pipeline->data['threadparse'] = $out; 2800 } 2801 2802 /** 2803 * Parse a level of a THREAD response (RFC 5256 [4]). 2804 * 2805 * @param array $thread Results. 2806 * @param Horde_Imap_Client_Tokenize $data Thread data. 2807 * @param integer $level The current tree level. 2808 */ 2809 protected function _parseThreadLevel(&$thread, 2810 Horde_Imap_Client_Tokenize $data, 2811 $level = 0) 2812 { 2813 while (($curr = $data->next()) !== false) { 2814 if ($curr === true) { 2815 $this->_parseThreadLevel($thread, $data, $level); 2816 } elseif (!is_bool($curr)) { 2817 $thread[$curr] = $level++; 2818 } 2819 } 2820 } 2821 2822 /** 2823 */ 2824 protected function _fetch(Horde_Imap_Client_Fetch_Results $results, 2825 $queries) 2826 { 2827 $pipeline = $this->_pipeline(); 2828 $pipeline->data['fetch_lookup'] = array(); 2829 $pipeline->data['fetch_followup'] = array(); 2830 2831 foreach ($queries as $options) { 2832 $this->_fetchCmd($pipeline, $options); 2833 $sequence = $options['ids']->sequence; 2834 } 2835 2836 try { 2837 $resp = $this->_sendCmd($pipeline); 2838 2839 /* Check for EXPUNGEISSUED (RFC 2180 [4.1]/RFC 5530 [3]). */ 2840 if (!empty($resp->data['expungeissued'])) { 2841 $this->noop(); 2842 } 2843 2844 foreach ($resp->fetch as $k => $v) { 2845 $results->get($sequence ? $k : $v->getUid())->merge($v); 2846 } 2847 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 2848 if ($e->status === Horde_Imap_Client_Interaction_Server::NO) { 2849 if ($e->getCode() === $e::UNKNOWNCTE || 2850 $e->getCode() === $e::PARSEERROR) { 2851 /* UNKNOWN-CTE error. Redo the query without the BINARY 2852 * elements. Also include PARSEERROR in this as 2853 * Dovecot >= 2.2 binary fetch treats broken email as PARSE 2854 * error and no longer UNKNOWN-CTE 2855 */ 2856 if (!empty($pipeline->data['binaryquery'])) { 2857 foreach ($queries as $val) { 2858 foreach ($pipeline->data['binaryquery'] as $key2 => $val2) { 2859 unset($val2['decode']); 2860 $val['_query']->bodyPart($key2, $val2); 2861 $val['_query']->remove(Horde_Imap_Client::FETCH_BODYPARTSIZE, $key2); 2862 } 2863 $pipeline->data['fetch_followup'][] = $val; 2864 } 2865 } else { 2866 $this->noop(); 2867 } 2868 } elseif ($sequence) { 2869 /* A NO response, when coupled with a sequence FETCH, most 2870 * likely means that messages were expunged. (RFC 2180 2871 * [4.1]) */ 2872 $this->noop(); 2873 } 2874 } 2875 } catch (Exception $e) { 2876 // For any other error, ignore the Exception - fetch() is nice in 2877 // that the return value explicitly handles missing data for any 2878 // given message. 2879 } 2880 2881 if (!empty($pipeline->data['fetch_followup'])) { 2882 $this->_fetch($results, $pipeline->data['fetch_followup']); 2883 } 2884 } 2885 2886 /** 2887 * Add a FETCH command to the given pipeline. 2888 * 2889 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 2890 * object. 2891 * @param array $options Fetch query 2892 * options 2893 */ 2894 protected function _fetchCmd( 2895 Horde_Imap_Client_Interaction_Pipeline $pipeline, 2896 $options 2897 ) 2898 { 2899 $fetch = new Horde_Imap_Client_Data_Format_List(); 2900 $sequence = $options['ids']->sequence; 2901 2902 /* Build an IMAP4rev1 compliant FETCH query. We handle the following 2903 * criteria: 2904 * BINARY[.PEEK][<section #>]<<partial>> (RFC 3516) 2905 * see BODY[] response 2906 * BINARY.SIZE[<section #>] (RFC 3516) 2907 * BODY[.PEEK][<section>]<<partial>> 2908 * <section> = HEADER, HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, 2909 * TEXT, empty 2910 * <<partial>> = 0.# (# of bytes) 2911 * BODYSTRUCTURE 2912 * ENVELOPE 2913 * FLAGS 2914 * INTERNALDATE 2915 * MODSEQ (RFC 7162) 2916 * RFC822.SIZE 2917 * UID 2918 * 2919 * No need to support these (can be built from other queries): 2920 * =========================================================== 2921 * ALL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE) 2922 * BODY => Use BODYSTRUCTURE instead 2923 * FAST macro => (FLAGS INTERNALDATE RFC822.SIZE) 2924 * FULL macro => (FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODY) 2925 * RFC822 => BODY[] 2926 * RFC822.HEADER => BODY[HEADER] 2927 * RFC822.TEXT => BODY[TEXT] 2928 */ 2929 2930 foreach ($options['_query'] as $type => $c_val) { 2931 switch ($type) { 2932 case Horde_Imap_Client::FETCH_STRUCTURE: 2933 $fetch->add('BODYSTRUCTURE'); 2934 break; 2935 2936 case Horde_Imap_Client::FETCH_FULLMSG: 2937 if (empty($c_val['peek'])) { 2938 $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE); 2939 } 2940 $fetch->add( 2941 'BODY' . 2942 (!empty($c_val['peek']) ? '.PEEK' : '') . 2943 '[]' . 2944 $this->_partialAtom($c_val) 2945 ); 2946 break; 2947 2948 case Horde_Imap_Client::FETCH_HEADERTEXT: 2949 case Horde_Imap_Client::FETCH_BODYTEXT: 2950 case Horde_Imap_Client::FETCH_MIMEHEADER: 2951 case Horde_Imap_Client::FETCH_BODYPART: 2952 case Horde_Imap_Client::FETCH_HEADERS: 2953 foreach ($c_val as $key => $val) { 2954 $cmd = ((int)$key == 0) 2955 ? '' 2956 : $key . '.'; 2957 $main_cmd = 'BODY'; 2958 2959 switch ($type) { 2960 case Horde_Imap_Client::FETCH_HEADERTEXT: 2961 $cmd .= 'HEADER'; 2962 break; 2963 2964 case Horde_Imap_Client::FETCH_BODYTEXT: 2965 $cmd .= 'TEXT'; 2966 break; 2967 2968 case Horde_Imap_Client::FETCH_MIMEHEADER: 2969 $cmd .= 'MIME'; 2970 break; 2971 2972 case Horde_Imap_Client::FETCH_BODYPART: 2973 // Remove the last dot from the string. 2974 $cmd = substr($cmd, 0, -1); 2975 2976 if (!empty($val['decode']) && 2977 $this->_capability('BINARY')) { 2978 $main_cmd = 'BINARY'; 2979 $pipeline->data['binaryquery'][$key] = $val; 2980 } 2981 break; 2982 2983 case Horde_Imap_Client::FETCH_HEADERS: 2984 $cmd .= 'HEADER.FIELDS'; 2985 if (!empty($val['notsearch'])) { 2986 $cmd .= '.NOT'; 2987 } 2988 $cmd .= ' (' . implode(' ', array_map('Horde_String::upper', $val['headers'])) . ')'; 2989 2990 // Maintain a command -> label lookup so we can put 2991 // the results in the proper location. 2992 $pipeline->data['fetch_lookup'][$cmd] = $key; 2993 } 2994 2995 if (empty($val['peek'])) { 2996 $this->openMailbox($this->_selected, Horde_Imap_Client::OPEN_READWRITE); 2997 } 2998 2999 $fetch->add( 3000 $main_cmd . 3001 (!empty($val['peek']) ? '.PEEK' : '') . 3002 '[' . $cmd . ']' . 3003 $this->_partialAtom($val) 3004 ); 3005 } 3006 break; 3007 3008 case Horde_Imap_Client::FETCH_BODYPARTSIZE: 3009 if ($this->_capability('BINARY')) { 3010 foreach ($c_val as $val) { 3011 $fetch->add('BINARY.SIZE[' . $val . ']'); 3012 } 3013 } 3014 break; 3015 3016 case Horde_Imap_Client::FETCH_ENVELOPE: 3017 $fetch->add('ENVELOPE'); 3018 break; 3019 3020 case Horde_Imap_Client::FETCH_FLAGS: 3021 $fetch->add('FLAGS'); 3022 break; 3023 3024 case Horde_Imap_Client::FETCH_IMAPDATE: 3025 $fetch->add('INTERNALDATE'); 3026 break; 3027 3028 case Horde_Imap_Client::FETCH_SIZE: 3029 $fetch->add('RFC822.SIZE'); 3030 break; 3031 3032 case Horde_Imap_Client::FETCH_UID: 3033 /* A UID FETCH will always return UID information (RFC 3501 3034 * [6.4.8]). Don't add to query as it just creates a longer 3035 * FETCH command. */ 3036 if ($sequence) { 3037 $fetch->add('UID'); 3038 } 3039 break; 3040 3041 case Horde_Imap_Client::FETCH_SEQ: 3042 /* Nothing we need to add to fetch request unless sequence is 3043 * the only criteria (see below). */ 3044 break; 3045 3046 case Horde_Imap_Client::FETCH_MODSEQ: 3047 /* The 'changedsince' modifier implicitly adds the MODSEQ 3048 * FETCH item (RFC 7162 [3.1.4.1]). Don't add to query as it 3049 * just creates a longer FETCH command. */ 3050 if (empty($options['changedsince'])) { 3051 $fetch->add('MODSEQ'); 3052 } 3053 break; 3054 } 3055 } 3056 3057 /* If empty fetch, add UID to make command valid. */ 3058 if (!count($fetch)) { 3059 $fetch->add('UID'); 3060 } 3061 3062 /* Add changedsince parameters. */ 3063 if (empty($options['changedsince'])) { 3064 $fetch_cmd = $fetch; 3065 } else { 3066 /* We might just want the list of UIDs changed since a given 3067 * modseq. In that case, we don't have any other FETCH attributes, 3068 * but RFC 3501 requires at least one specified attribute. */ 3069 $fetch_cmd = array( 3070 $fetch, 3071 new Horde_Imap_Client_Data_Format_List(array( 3072 'CHANGEDSINCE', 3073 new Horde_Imap_Client_Data_Format_Number($options['changedsince']) 3074 )) 3075 ); 3076 } 3077 3078 /* The FETCH command should be the only command issued by this library 3079 * that should ever approach the command length limit. 3080 * @todo Move this check to a more centralized location (_command()?). 3081 * For simplification, assume that the UID list is the limiting factor 3082 * and split this list at a sequence comma delimiter if it exceeds 3083 * the character limit. */ 3084 foreach ($options['ids']->split($this->_capability()->cmdlength) as $val) { 3085 $cmd = $this->_command( 3086 $sequence ? 'FETCH' : 'UID FETCH' 3087 )->add(array( 3088 $val, 3089 $fetch_cmd 3090 )); 3091 $pipeline->add($cmd); 3092 } 3093 } 3094 3095 /** 3096 * Add a partial atom to an IMAP command based on the criteria options. 3097 * 3098 * @param array $opts Criteria options. 3099 * 3100 * @return string The partial atom. 3101 */ 3102 protected function _partialAtom($opts) 3103 { 3104 if (!empty($opts['length'])) { 3105 return '<' . (empty($opts['start']) ? 0 : intval($opts['start'])) . '.' . intval($opts['length']) . '>'; 3106 } 3107 3108 return empty($opts['start']) 3109 ? '' 3110 : ('<' . intval($opts['start']) . '>'); 3111 } 3112 3113 /** 3114 * Parse a FETCH response (RFC 3501 [7.4.2]). A FETCH response may occur 3115 * due to a FETCH command, or due to a change in a message's state (i.e. 3116 * the flags change). 3117 * 3118 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3119 * object. 3120 * @param integer $id The message sequence number. 3121 * @param Horde_Imap_Client_Tokenize $data The server response. 3122 */ 3123 protected function _parseFetch( 3124 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3125 $id, 3126 Horde_Imap_Client_Tokenize $data 3127 ) 3128 { 3129 if ($data->next() !== true) { 3130 return; 3131 } 3132 3133 $ob = $pipeline->fetch->get($id); 3134 $ob->setSeq($id); 3135 3136 $flags = $modseq = $uid = false; 3137 3138 while (($tag = $data->next()) !== false) { 3139 $tag = Horde_String::upper($tag); 3140 3141 /* Catch equivalent RFC822 tags, in case server returns them 3142 * (in error, since we only use BODY in FETCH requests). */ 3143 switch ($tag) { 3144 case 'RFC822': 3145 $tag = 'BODY[]'; 3146 break; 3147 3148 case 'RFC822.HEADER': 3149 $tag = 'BODY[HEADER]'; 3150 break; 3151 3152 case 'RFC822.TEXT': 3153 $tag = 'BODY[TEXT]'; 3154 break; 3155 } 3156 3157 switch ($tag) { 3158 case 'BODYSTRUCTURE': 3159 $data->next(); 3160 $structure = $this->_parseBodystructure($data); 3161 $structure->buildMimeIds(); 3162 $ob->setStructure($structure); 3163 break; 3164 3165 case 'ENVELOPE': 3166 $data->next(); 3167 $ob->setEnvelope($this->_parseEnvelope($data)); 3168 break; 3169 3170 case 'FLAGS': 3171 $data->next(); 3172 $ob->setFlags($data->flushIterator()); 3173 $flags = true; 3174 break; 3175 3176 case 'INTERNALDATE': 3177 $ob->setImapDate($data->next()); 3178 break; 3179 3180 case 'RFC822.SIZE': 3181 $ob->setSize($data->next()); 3182 break; 3183 3184 case 'UID': 3185 $ob->setUid($data->next()); 3186 $uid = true; 3187 break; 3188 3189 case 'MODSEQ': 3190 $data->next(); 3191 $modseq = $data->next(); 3192 $data->next(); 3193 3194 /* MODSEQ must be greater than 0, so do sanity checking. */ 3195 if ($modseq > 0) { 3196 $ob->setModSeq($modseq); 3197 3198 /* Store MODSEQ value. It may be used as the highestmodseq 3199 * once a tagged response is received (RFC 7162 [6]). */ 3200 $pipeline->data['modseqs'][] = $modseq; 3201 } 3202 break; 3203 3204 default: 3205 // Catch BODY[*]<#> responses 3206 if (strpos($tag, 'BODY[') === 0) { 3207 // Remove the beginning 'BODY[' 3208 $tag = substr($tag, 5); 3209 3210 // BODY[HEADER.FIELDS] request 3211 if (!empty($pipeline->data['fetch_lookup']) && 3212 (strpos($tag, 'HEADER.FIELDS') !== false)) { 3213 $data->next(); 3214 $sig = $tag . ' (' . implode(' ', array_map('Horde_String::upper', $data->flushIterator())) . ')'; 3215 3216 // Ignore the trailing bracket 3217 $data->next(); 3218 3219 $ob->setHeaders($pipeline->data['fetch_lookup'][$sig], $data->next()); 3220 } else { 3221 // Remove trailing bracket and octet start info 3222 $tag = substr($tag, 0, strrpos($tag, ']')); 3223 3224 if (!strlen($tag)) { 3225 // BODY[] request 3226 if (!is_null($tmp = $data->nextStream())) { 3227 $ob->setFullMsg($tmp); 3228 } 3229 } elseif (is_numeric(substr($tag, -1))) { 3230 // BODY[MIMEID] request 3231 if (!is_null($tmp = $data->nextStream())) { 3232 $ob->setBodyPart($tag, $tmp); 3233 } 3234 } else { 3235 // BODY[HEADER|TEXT|MIME] request 3236 if (($last_dot = strrpos($tag, '.')) === false) { 3237 $mime_id = 0; 3238 } else { 3239 $mime_id = substr($tag, 0, $last_dot); 3240 $tag = substr($tag, $last_dot + 1); 3241 } 3242 3243 if (!is_null($tmp = $data->nextStream())) { 3244 switch ($tag) { 3245 case 'HEADER': 3246 $ob->setHeaderText($mime_id, $tmp); 3247 break; 3248 3249 case 'TEXT': 3250 $ob->setBodyText($mime_id, $tmp); 3251 break; 3252 3253 case 'MIME': 3254 $ob->setMimeHeader($mime_id, $tmp); 3255 break; 3256 } 3257 } 3258 } 3259 } 3260 } elseif (strpos($tag, 'BINARY[') === 0) { 3261 // Catch BINARY[*]<#> responses 3262 // Remove the beginning 'BINARY[' and the trailing bracket 3263 // and octet start info 3264 $tag = substr($tag, 7, strrpos($tag, ']') - 7); 3265 $body = $data->nextStream(); 3266 3267 if (is_null($body)) { 3268 /* Dovecot bug (as of 2.2.12): binary fetch of body 3269 * part may fail with NIL return if decoding failed on 3270 * server. Try again with non-decoded body. */ 3271 $bq = $pipeline->data['binaryquery'][$tag]; 3272 unset($bq['decode']); 3273 3274 $query = new Horde_Imap_Client_Fetch_Query(); 3275 $query->bodyPart($tag, $bq); 3276 3277 $qids = ($quid = $ob->getUid()) 3278 ? new Horde_Imap_Client_Ids($quid) 3279 : new Horde_Imap_Client_Ids($id, true); 3280 3281 $pipeline->data['fetch_followup'][] = array( 3282 '_query' => $query, 3283 'ids' => $qids 3284 ); 3285 } else { 3286 $ob->setBodyPart( 3287 $tag, 3288 $body, 3289 empty($this->_temp['literal8']) ? '8bit' : 'binary' 3290 ); 3291 } 3292 } elseif (strpos($tag, 'BINARY.SIZE[') === 0) { 3293 // Catch BINARY.SIZE[*] responses 3294 // Remove the beginning 'BINARY.SIZE[' and the trailing 3295 // bracket and octet start info 3296 $tag = substr($tag, 12, strrpos($tag, ']') - 12); 3297 $ob->setBodyPartSize($tag, $data->next()); 3298 } 3299 break; 3300 } 3301 } 3302 3303 /* MODSEQ issue: Oh joy. Per RFC 5162 (see Errata #1807), FETCH FLAGS 3304 * responses are NOT required to provide UID information, even if 3305 * QRESYNC is explicitly enabled. Caveat: the FLAGS information 3306 * returned during a SELECT/EXAMINE MUST contain UIDs so we are OK 3307 * there. 3308 * The good news: all decent IMAP servers (Cyrus, Dovecot) will always 3309 * provide UID information, so this is not normally an issue. 3310 * The bad news: spec-wise, this behavior cannot be 100% guaranteed. 3311 * Compromise: We will watch for a FLAGS response with a MODSEQ and 3312 * check if a UID exists also. If not, put the sequence number in a 3313 * queue - it is possible the UID information may appear later in an 3314 * untagged response. When the command is over, double check to make 3315 * sure there are none of these MODSEQ/FLAGS that are still UID-less. 3316 * In the (rare) event that there is, don't cache anything and 3317 * immediately close the mailbox: flags will be correctly sync'd next 3318 * mailbox open so we only lose a bit of caching efficiency. 3319 * Otherwise, we could end up with an inconsistent cached state. 3320 * This Errata has been fixed in 7162 [3.2.4]. */ 3321 if ($flags && $modseq && !$uid) { 3322 $pipeline->data['modseqs_nouid'][] = $id; 3323 } 3324 } 3325 3326 /** 3327 * Recursively parse BODYSTRUCTURE data from a FETCH return (see 3328 * RFC 3501 [7.4.2]). 3329 * 3330 * @param Horde_Imap_Client_Tokenize $data Data returned from the server. 3331 * 3332 * @return Horde_Mime_Part Mime part object. 3333 */ 3334 protected function _parseBodystructure(Horde_Imap_Client_Tokenize $data) 3335 { 3336 $ob = new Horde_Mime_Part(); 3337 3338 // If index 0 is an array, this is a multipart part. 3339 if (($entry = $data->next()) === true) { 3340 do { 3341 $ob->addPart($this->_parseBodystructure($data)); 3342 } while (($entry = $data->next()) === true); 3343 3344 // The subpart type. 3345 $ob->setType('multipart/' . $entry); 3346 3347 // After the subtype is further extension information. This 3348 // information MAY appear for BODYSTRUCTURE requests. 3349 3350 // This is parameter information. 3351 if (($tmp = $data->next()) === false) { 3352 return $ob; 3353 } elseif ($tmp === true) { 3354 foreach ($this->_parseStructureParams($data) as $key => $val) { 3355 $ob->setContentTypeParameter($key, $val); 3356 } 3357 } 3358 } else { 3359 $ob->setType($entry . '/' . $data->next()); 3360 3361 if ($data->next() === true) { 3362 foreach ($this->_parseStructureParams($data) as $key => $val) { 3363 $ob->setContentTypeParameter($key, $val); 3364 } 3365 } 3366 3367 if (!is_null($tmp = $data->next())) { 3368 $ob->setContentId($tmp); 3369 } 3370 3371 if (!is_null($tmp = $data->next())) { 3372 $ob->setDescription(Horde_Mime::decode($tmp)); 3373 } 3374 3375 $te = $data->next(); 3376 $bytes = $data->next(); 3377 3378 if (!is_null($te)) { 3379 $ob->setTransferEncoding($te); 3380 3381 /* Base64 transfer encoding is approx. 33% larger than 3382 * original data size (RFC 2045 [6.8]). Return from 3383 * BODYSTRUCTURE is the size of the ENCODED data (RFC 3501 3384 * [7.4.2]). */ 3385 if (strcasecmp($te, 'base64') === 0) { 3386 $bytes *= 0.75; 3387 } 3388 } 3389 3390 $ob->setBytes($bytes); 3391 3392 // If the type is 'message/rfc822' or 'text/*', several extra 3393 // fields are included 3394 switch ($ob->getPrimaryType()) { 3395 case 'message': 3396 if ($ob->getSubType() == 'rfc822') { 3397 if ($data->next() === true) { 3398 // Ignore: envelope 3399 $data->flushIterator(false); 3400 } 3401 if ($data->next() === true) { 3402 $ob->addPart($this->_parseBodystructure($data)); 3403 } 3404 $data->next(); // Ignore: lines 3405 } 3406 break; 3407 3408 case 'text': 3409 $data->next(); // Ignore: lines 3410 break; 3411 } 3412 3413 // After the subtype is further extension information. This 3414 // information MAY appear for BODYSTRUCTURE requests. 3415 3416 // Ignore: MD5 3417 if ($data->next() === false) { 3418 return $ob; 3419 } 3420 } 3421 3422 // This is disposition information 3423 if (($tmp = $data->next()) === false) { 3424 return $ob; 3425 } elseif ($tmp === true) { 3426 $ob->setDisposition($data->next()); 3427 3428 if ($data->next() === true) { 3429 foreach ($this->_parseStructureParams($data) as $key => $val) { 3430 $ob->setDispositionParameter($key, $val); 3431 } 3432 } 3433 $data->next(); 3434 } 3435 3436 // This is language information. It is either a single value or a list 3437 // of values. 3438 if (($tmp = $data->next()) === false) { 3439 return $ob; 3440 } elseif (!is_null($tmp)) { 3441 $ob->setLanguage(($tmp === true) ? $data->flushIterator() : $tmp); 3442 } 3443 3444 // Ignore location (RFC 2557) and consume closing paren. 3445 $data->flushIterator(false); 3446 3447 return $ob; 3448 } 3449 3450 /** 3451 * Helper function to parse a parameters-like tokenized array. 3452 * 3453 * @param mixed $data Message data. Either a Horde_Imap_Client_Tokenize 3454 * object or null. 3455 * 3456 * @return array The parameter array. 3457 */ 3458 protected function _parseStructureParams($data) 3459 { 3460 $params = array(); 3461 3462 if (is_null($data)) { 3463 return $params; 3464 } 3465 3466 while (($name = $data->next()) !== false) { 3467 $params[Horde_String::lower($name)] = $data->next(); 3468 } 3469 3470 $cp = new Horde_Mime_Headers_ContentParam('Unused', $params); 3471 3472 return $cp->params; 3473 } 3474 3475 /** 3476 * Parse ENVELOPE data from a FETCH return (see RFC 3501 [7.4.2]). 3477 * 3478 * @param Horde_Imap_Client_Tokenize $data Data returned from the server. 3479 * 3480 * @return Horde_Imap_Client_Data_Envelope An envelope object. 3481 */ 3482 protected function _parseEnvelope(Horde_Imap_Client_Tokenize $data) 3483 { 3484 // 'route', the 2nd element, is deprecated by RFC 2822. 3485 $addr_structure = array( 3486 0 => 'personal', 3487 2 => 'mailbox', 3488 3 => 'host' 3489 ); 3490 $env_data = array( 3491 0 => 'date', 3492 1 => 'subject', 3493 2 => 'from', 3494 3 => 'sender', 3495 4 => 'reply_to', 3496 5 => 'to', 3497 6 => 'cc', 3498 7 => 'bcc', 3499 8 => 'in_reply_to', 3500 9 => 'message_id' 3501 ); 3502 3503 $addr_ob = new Horde_Mail_Rfc822_Address(); 3504 $env_addrs = $this->getParam('envelope_addrs'); 3505 $env_str = $this->getParam('envelope_string'); 3506 $key = 0; 3507 $ret = new Horde_Imap_Client_Data_Envelope(); 3508 3509 while (($val = $data->next()) !== false) { 3510 if (!isset($env_data[$key]) || is_null($val)) { 3511 ++$key; 3512 continue; 3513 } 3514 3515 if (is_string($val)) { 3516 // These entries are text fields. 3517 $ret->{$env_data[$key]} = substr($val, 0, $env_str); 3518 } else { 3519 // These entries are address structures. 3520 $group = null; 3521 $key2 = 0; 3522 $tmp = new Horde_Mail_Rfc822_List(); 3523 3524 while ($data->next() !== false) { 3525 $a_val = $data->flushIterator(); 3526 3527 // RFC 3501 [7.4.2]: Group entry when host is NIL. 3528 // Group end when mailbox is NIL; otherwise, this is 3529 // mailbox name. 3530 if (is_null($a_val[3])) { 3531 if (is_null($a_val[2])) { 3532 $group = null; 3533 } else { 3534 $group = new Horde_Mail_Rfc822_Group($a_val[2]); 3535 $tmp->add($group); 3536 } 3537 } else { 3538 $addr = clone $addr_ob; 3539 3540 foreach ($addr_structure as $add_key => $add_val) { 3541 if (!is_null($a_val[$add_key])) { 3542 $addr->$add_val = $a_val[$add_key]; 3543 } 3544 } 3545 3546 if ($group) { 3547 $group->addresses->add($addr); 3548 } else { 3549 $tmp->add($addr); 3550 } 3551 } 3552 3553 if (++$key2 >= $env_addrs) { 3554 $data->flushIterator(false); 3555 break; 3556 } 3557 } 3558 3559 $ret->{$env_data[$key]} = $tmp; 3560 } 3561 3562 ++$key; 3563 } 3564 3565 return $ret; 3566 } 3567 3568 /** 3569 */ 3570 protected function _vanished($modseq, Horde_Imap_Client_Ids $ids) 3571 { 3572 $pipeline = $this->_pipeline( 3573 $this->_command('UID FETCH')->add(array( 3574 strval($ids), 3575 'UID', 3576 new Horde_Imap_Client_Data_Format_List(array( 3577 'VANISHED', 3578 'CHANGEDSINCE', 3579 new Horde_Imap_Client_Data_Format_Number($modseq) 3580 )) 3581 )) 3582 ); 3583 $pipeline->data['vanished'] = $this->getIdsOb(); 3584 3585 return $this->_sendCmd($pipeline)->data['vanished']; 3586 } 3587 3588 /** 3589 */ 3590 protected function _store($options) 3591 { 3592 $pipeline = $this->_storeCmd($options); 3593 $pipeline->data['modified'] = $this->getIdsOb(); 3594 3595 try { 3596 $resp = $this->_sendCmd($pipeline); 3597 3598 /* Check for EXPUNGEISSUED (RFC 2180 [4.2]/RFC 5530 [3]). */ 3599 if (!empty($resp->data['expungeissued'])) { 3600 $this->noop(); 3601 } 3602 3603 return $resp->data['modified']; 3604 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 3605 /* A NO response, when coupled with a sequence STORE and 3606 * non-SILENT behavior, most likely means that messages were 3607 * expunged. RFC 2180 [4.2] */ 3608 if (empty($pipeline->data['store_silent']) && 3609 !empty($options['sequence']) && 3610 ($e->status === Horde_Imap_Client_Interaction_Server::NO)) { 3611 $this->noop(); 3612 } 3613 3614 return $pipeline->data['modified']; 3615 } 3616 } 3617 3618 /** 3619 * Create a store command. 3620 * 3621 * @param array $options See Horde_Imap_Client_Base#_store(). 3622 * 3623 * @return Horde_Imap_Client_Interaction_Pipeline Pipeline object. 3624 */ 3625 protected function _storeCmd($options) 3626 { 3627 $cmds = array(); 3628 $silent = empty($options['unchangedsince']) 3629 ? !($this->_debug->debug || $this->_initCache(true)) 3630 : false; 3631 3632 if (!empty($options['replace'])) { 3633 $cmds[] = array( 3634 'FLAGS' . ($silent ? '.SILENT' : ''), 3635 $options['replace'] 3636 ); 3637 } else { 3638 foreach (array('add' => '+', 'remove' => '-') as $k => $v) { 3639 if (!empty($options[$k])) { 3640 $cmds[] = array( 3641 $v . 'FLAGS' . ($silent ? '.SILENT' : ''), 3642 $options[$k] 3643 ); 3644 } 3645 } 3646 } 3647 3648 $pipeline = $this->_pipeline(); 3649 $pipeline->data['store_silent'] = $silent; 3650 3651 foreach ($cmds as $val) { 3652 $cmd = $this->_command( 3653 empty($options['sequence']) ? 'UID STORE' : 'STORE' 3654 )->add(strval($options['ids'])); 3655 if (!empty($options['unchangedsince'])) { 3656 $cmd->add(new Horde_Imap_Client_Data_Format_List(array( 3657 'UNCHANGEDSINCE', 3658 new Horde_Imap_Client_Data_Format_Number(intval($options['unchangedsince'])) 3659 ))); 3660 } 3661 $cmd->add($val); 3662 3663 $pipeline->add($cmd); 3664 } 3665 3666 return $pipeline; 3667 } 3668 3669 /** 3670 */ 3671 protected function _copy(Horde_Imap_Client_Mailbox $dest, $options) 3672 { 3673 /* Check for MOVE command (RFC 6851). */ 3674 $move_cmd = (!empty($options['move']) && 3675 $this->_capability('MOVE')); 3676 3677 $cmd = $this->_pipeline( 3678 $this->_command( 3679 ($options['ids']->sequence ? '' : 'UID ') . ($move_cmd ? 'MOVE' : 'COPY') 3680 )->add(array( 3681 strval($options['ids']), 3682 $this->_getMboxFormatOb($dest) 3683 )) 3684 ); 3685 $cmd->data['copydest'] = $dest; 3686 3687 // COPY returns no untagged information (RFC 3501 [6.4.7]) 3688 try { 3689 $resp = $this->_sendCmd($cmd); 3690 } catch (Horde_Imap_Client_Exception $e) { 3691 if (!empty($options['create']) && 3692 !empty($e->resp_data['trycreate'])) { 3693 $this->createMailbox($dest); 3694 unset($options['create']); 3695 return $this->_copy($dest, $options); 3696 } 3697 throw $e; 3698 } 3699 3700 // If moving, delete the old messages now. Short-circuit if nothing 3701 // was moved. 3702 if (!$move_cmd && 3703 !empty($options['move']) && 3704 (isset($resp->data['copyuid']) || 3705 !$this->_capability('UIDPLUS'))) { 3706 $this->expunge($this->_selected, array( 3707 'delete' => true, 3708 'ids' => $options['ids'] 3709 )); 3710 } 3711 3712 return isset($resp->data['copyuid']) 3713 ? $resp->data['copyuid'] 3714 : true; 3715 } 3716 3717 /** 3718 */ 3719 protected function _setQuota(Horde_Imap_Client_Mailbox $root, $resources) 3720 { 3721 $limits = new Horde_Imap_Client_Data_Format_List(); 3722 3723 foreach ($resources as $key => $val) { 3724 $limits->add(array( 3725 Horde_String::upper($key), 3726 new Horde_Imap_Client_Data_Format_Number($val) 3727 )); 3728 } 3729 3730 $this->_sendCmd( 3731 $this->_command('SETQUOTA')->add(array( 3732 $this->_getMboxFormatOb($root), 3733 $limits 3734 )) 3735 ); 3736 } 3737 3738 /** 3739 */ 3740 protected function _getQuota(Horde_Imap_Client_Mailbox $root) 3741 { 3742 $pipeline = $this->_pipeline( 3743 $this->_command('GETQUOTA')->add( 3744 $this->_getMboxFormatOb($root) 3745 ) 3746 ); 3747 $pipeline->data['quotaresp'] = array(); 3748 3749 return reset($this->_sendCmd($pipeline)->data['quotaresp']); 3750 } 3751 3752 /** 3753 * Parse a QUOTA response (RFC 2087 [5.1]). 3754 * 3755 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3756 * object. 3757 * @param Horde_Imap_Client_Tokenize $data The server response. 3758 */ 3759 protected function _parseQuota( 3760 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3761 Horde_Imap_Client_Tokenize $data 3762 ) 3763 { 3764 $c = &$pipeline->data['quotaresp']; 3765 3766 $root = $data->next(); 3767 $c[$root] = array(); 3768 3769 $data->next(); 3770 3771 while (($curr = $data->next()) !== false) { 3772 $c[$root][Horde_String::lower($curr)] = array( 3773 'usage' => $data->next(), 3774 'limit' => $data->next() 3775 ); 3776 } 3777 } 3778 3779 /** 3780 */ 3781 protected function _getQuotaRoot(Horde_Imap_Client_Mailbox $mailbox) 3782 { 3783 $pipeline = $this->_pipeline( 3784 $this->_command('GETQUOTAROOT')->add( 3785 $this->_getMboxFormatOb($mailbox) 3786 ) 3787 ); 3788 $pipeline->data['quotaresp'] = array(); 3789 3790 return $this->_sendCmd($pipeline)->data['quotaresp']; 3791 } 3792 3793 /** 3794 */ 3795 protected function _setACL(Horde_Imap_Client_Mailbox $mailbox, $identifier, 3796 $options) 3797 { 3798 // SETACL returns no untagged information (RFC 4314 [3.1]). 3799 $this->_sendCmd( 3800 $this->_command('SETACL')->add(array( 3801 $this->_getMboxFormatOb($mailbox), 3802 new Horde_Imap_Client_Data_Format_Astring($identifier), 3803 new Horde_Imap_Client_Data_Format_Astring($options['rights']) 3804 )) 3805 ); 3806 } 3807 3808 /** 3809 */ 3810 protected function _deleteACL(Horde_Imap_Client_Mailbox $mailbox, $identifier) 3811 { 3812 // DELETEACL returns no untagged information (RFC 4314 [3.2]). 3813 $this->_sendCmd( 3814 $this->_command('DELETEACL')->add(array( 3815 $this->_getMboxFormatOb($mailbox), 3816 new Horde_Imap_Client_Data_Format_Astring($identifier) 3817 )) 3818 ); 3819 } 3820 3821 /** 3822 */ 3823 protected function _getACL(Horde_Imap_Client_Mailbox $mailbox) 3824 { 3825 return $this->_sendCmd( 3826 $this->_command('GETACL')->add( 3827 $this->_getMboxFormatOb($mailbox) 3828 ) 3829 )->data['getacl']; 3830 } 3831 3832 /** 3833 * Parse an ACL response (RFC 4314 [3.6]). 3834 * 3835 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3836 * object. 3837 * @param Horde_Imap_Client_Tokenize $data The server response. 3838 */ 3839 protected function _parseACL( 3840 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3841 Horde_Imap_Client_Tokenize $data 3842 ) 3843 { 3844 $acl = array(); 3845 3846 // Ignore mailbox argument -> index 1 3847 $data->next(); 3848 3849 while (($curr = $data->next()) !== false) { 3850 $acl[$curr] = ($curr[0] === '-') 3851 ? new Horde_Imap_Client_Data_AclNegative($data->next()) 3852 : new Horde_Imap_Client_Data_Acl($data->next()); 3853 } 3854 3855 $pipeline->data['getacl'] = $acl; 3856 } 3857 3858 /** 3859 */ 3860 protected function _listACLRights(Horde_Imap_Client_Mailbox $mailbox, 3861 $identifier) 3862 { 3863 $resp = $this->_sendCmd( 3864 $this->_command('LISTRIGHTS')->add(array( 3865 $this->_getMboxFormatOb($mailbox), 3866 new Horde_Imap_Client_Data_Format_Astring($identifier) 3867 )) 3868 ); 3869 3870 return isset($resp->data['listaclrights']) 3871 ? $resp->data['listaclrights'] 3872 : new Horde_Imap_Client_Data_AclRights(); 3873 } 3874 3875 /** 3876 * Parse a LISTRIGHTS response (RFC 4314 [3.7]). 3877 * 3878 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3879 * object. 3880 * @param Horde_Imap_Client_Tokenize $data The server response. 3881 */ 3882 protected function _parseListRights( 3883 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3884 Horde_Imap_Client_Tokenize $data 3885 ) 3886 { 3887 // Ignore mailbox and identifier arguments 3888 $data->next(); 3889 $data->next(); 3890 3891 $pipeline->data['listaclrights'] = new Horde_Imap_Client_Data_AclRights( 3892 str_split($data->next()), 3893 $data->flushIterator() 3894 ); 3895 } 3896 3897 /** 3898 */ 3899 protected function _getMyACLRights(Horde_Imap_Client_Mailbox $mailbox) 3900 { 3901 $resp = $this->_sendCmd( 3902 $this->_command('MYRIGHTS')->add( 3903 $this->_getMboxFormatOb($mailbox) 3904 ) 3905 ); 3906 3907 return isset($resp->data['myrights']) 3908 ? $resp->data['myrights'] 3909 : new Horde_Imap_Client_Data_Acl(); 3910 } 3911 3912 /** 3913 * Parse a MYRIGHTS response (RFC 4314 [3.8]). 3914 * 3915 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 3916 * object. 3917 * @param Horde_Imap_Client_Tokenize $data The server response. 3918 */ 3919 protected function _parseMyRights( 3920 Horde_Imap_Client_Interaction_Pipeline $pipeline, 3921 Horde_Imap_Client_Tokenize $data 3922 ) 3923 { 3924 // Ignore 1st token (mailbox name) 3925 $data->next(); 3926 3927 $pipeline->data['myrights'] = new Horde_Imap_Client_Data_Acl($data->next()); 3928 } 3929 3930 /** 3931 */ 3932 protected function _getMetadata(Horde_Imap_Client_Mailbox $mailbox, 3933 $entries, $options) 3934 { 3935 $pipeline = $this->_pipeline(); 3936 $pipeline->data['metadata'] = array(); 3937 3938 if ($this->_capability('METADATA') || 3939 (strlen($mailbox) && $this->_capability('METADATA-SERVER'))) { 3940 $cmd_options = new Horde_Imap_Client_Data_Format_List(); 3941 3942 if (!empty($options['maxsize'])) { 3943 $cmd_options->add(array( 3944 'MAXSIZE', 3945 new Horde_Imap_Client_Data_Format_Number($options['maxsize']) 3946 )); 3947 } 3948 if (!empty($options['depth'])) { 3949 $cmd_options->add(array( 3950 'DEPTH', 3951 new Horde_Imap_Client_Data_Format_Number($options['depth']) 3952 )); 3953 } 3954 3955 $queries = new Horde_Imap_Client_Data_Format_List(); 3956 foreach ($entries as $md_entry) { 3957 $queries->add(new Horde_Imap_Client_Data_Format_Astring($md_entry)); 3958 } 3959 3960 $cmd = $this->_command('GETMETADATA')->add( 3961 $this->_getMboxFormatOb($mailbox) 3962 ); 3963 if (count($cmd_options)) { 3964 $cmd->add($cmd_options); 3965 } 3966 $cmd->add($queries); 3967 3968 $pipeline->add($cmd); 3969 } else { 3970 if (!$this->_capability('ANNOTATEMORE') && 3971 !$this->_capability('ANNOTATEMORE2')) { 3972 throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA'); 3973 } 3974 3975 $queries = array(); 3976 foreach ($entries as $md_entry) { 3977 list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry); 3978 3979 if (!isset($queries[$type])) { 3980 $queries[$type] = new Horde_Imap_Client_Data_Format_List(); 3981 } 3982 $queries[$type]->add(new Horde_Imap_Client_Data_Format_String($entry)); 3983 } 3984 3985 foreach ($queries as $key => $val) { 3986 // TODO: Honor maxsize and depth options. 3987 $pipeline->add( 3988 $this->_command('GETANNOTATION')->add(array( 3989 $this->_getMboxFormatOb($mailbox), 3990 $val, 3991 new Horde_Imap_Client_Data_Format_String($key) 3992 )) 3993 ); 3994 } 3995 } 3996 3997 return $this->_sendCmd($pipeline)->data['metadata']; 3998 } 3999 4000 /** 4001 * Split a name for the METADATA extension into the correct syntax for the 4002 * older ANNOTATEMORE version. 4003 * 4004 * @param string $name A name for a metadata entry. 4005 * 4006 * @return array A list of two elements: The entry name and the value 4007 * type. 4008 * 4009 * @throws Horde_Imap_Client_Exception 4010 */ 4011 protected function _getAnnotateMoreEntry($name) 4012 { 4013 if (substr($name, 0, 7) === '/shared') { 4014 return array(substr($name, 7), 'value.shared'); 4015 } else if (substr($name, 0, 8) === '/private') { 4016 return array(substr($name, 8), 'value.priv'); 4017 } 4018 4019 $e = new Horde_Imap_Client_Exception( 4020 Horde_Imap_Client_Translation::r("Invalid METADATA entry: \"%s\"."), 4021 Horde_Imap_Client_Exception::METADATA_INVALID 4022 ); 4023 $e->messagePrintf(array($name)); 4024 throw $e; 4025 } 4026 4027 /** 4028 */ 4029 protected function _setMetadata(Horde_Imap_Client_Mailbox $mailbox, $data) 4030 { 4031 if ($this->_capability('METADATA') || 4032 (strlen($mailbox) && $this->_capability('METADATA-SERVER'))) { 4033 $data_elts = new Horde_Imap_Client_Data_Format_List(); 4034 4035 foreach ($data as $key => $value) { 4036 $data_elts->add(array( 4037 new Horde_Imap_Client_Data_Format_Astring($key), 4038 /* METADATA supports literal8 - thus, it implicitly 4039 * supports non-ASCII characters in the data. */ 4040 new Horde_Imap_Client_Data_Format_Nstring_Nonascii($value) 4041 )); 4042 } 4043 4044 $cmd = $this->_command('SETMETADATA')->add(array( 4045 $this->_getMboxFormatOb($mailbox), 4046 $data_elts 4047 )); 4048 } else { 4049 if (!$this->_capability('ANNOTATEMORE') && 4050 !$this->_capability('ANNOTATEMORE2')) { 4051 throw new Horde_Imap_Client_Exception_NoSupportExtension('METADATA'); 4052 } 4053 4054 $cmd = $this->_pipeline(); 4055 4056 foreach ($data as $md_entry => $value) { 4057 list($entry, $type) = $this->_getAnnotateMoreEntry($md_entry); 4058 4059 $cmd->add( 4060 $this->_command('SETANNOTATION')->add(array( 4061 $this->_getMboxFormatOb($mailbox), 4062 new Horde_Imap_Client_Data_Format_String($entry), 4063 new Horde_Imap_Client_Data_Format_List(array( 4064 new Horde_Imap_Client_Data_Format_String($type), 4065 /* ANNOTATEMORE supports literal8 - thus, it 4066 * implicitly supports non-ASCII characters in the 4067 * data. */ 4068 new Horde_Imap_Client_Data_Format_Nstring_Nonascii($value) 4069 )) 4070 )) 4071 ); 4072 } 4073 } 4074 4075 $this->_sendCmd($cmd); 4076 } 4077 4078 /** 4079 * Parse an ANNOTATION response (ANNOTATEMORE/ANNOTATEMORE2). 4080 * 4081 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4082 * object. 4083 * @param Horde_Imap_Client_Tokenize $data The server response. 4084 * 4085 * @throws Horde_Imap_Client_Exception 4086 */ 4087 protected function _parseAnnotation( 4088 Horde_Imap_Client_Interaction_Pipeline $pipeline, 4089 Horde_Imap_Client_Tokenize $data 4090 ) 4091 { 4092 // Mailbox name is in UTF7-IMAP. 4093 $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true); 4094 $entry = $data->next(); 4095 4096 // Ignore unsolicited responses. 4097 if ($data->next() !== true) { 4098 return; 4099 } 4100 4101 while (($type = $data->next()) !== false) { 4102 switch ($type) { 4103 case 'value.priv': 4104 $pipeline->data['metadata'][strval($mbox)]['/private' . $entry] = $data->next(); 4105 break; 4106 4107 case 'value.shared': 4108 $pipeline->data['metadata'][strval($mbox)]['/shared' . $entry] = $data->next(); 4109 break; 4110 4111 default: 4112 $e = new Horde_Imap_Client_Exception( 4113 Horde_Imap_Client_Translation::r("Invalid METADATA value type \"%s\"."), 4114 Horde_Imap_Client_Exception::METADATA_INVALID 4115 ); 4116 $e->messagePrintf(array($type)); 4117 throw $e; 4118 } 4119 } 4120 } 4121 4122 /** 4123 * Parse a METADATA response (RFC 5464 [4.4]). 4124 * 4125 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4126 * object. 4127 * @param Horde_Imap_Client_Tokenize $data The server response. 4128 * 4129 * @throws Horde_Imap_Client_Exception 4130 */ 4131 protected function _parseMetadata( 4132 Horde_Imap_Client_Interaction_Pipeline $pipeline, 4133 Horde_Imap_Client_Tokenize $data 4134 ) 4135 { 4136 // Mailbox name is in UTF7-IMAP. 4137 $mbox = Horde_Imap_Client_Mailbox::get($data->next(), true); 4138 4139 // Ignore unsolicited responses. 4140 if ($data->next() === true) { 4141 while (($entry = $data->next()) !== false) { 4142 $pipeline->data['metadata'][strval($mbox)][$entry] = $data->next(); 4143 } 4144 } 4145 } 4146 4147 /* Overriden methods. */ 4148 4149 /** 4150 * @param array $opts Options: 4151 * - decrement: (boolean) If true, decrement the message count. 4152 * - pipeline: (Horde_Imap_Client_Interaction_Pipeline) Pipeline object. 4153 */ 4154 protected function _deleteMsgs(Horde_Imap_Client_Mailbox $mailbox, 4155 Horde_Imap_Client_Ids $ids, 4156 array $opts = array()) 4157 { 4158 /* If there are pending FETCH cache writes, we need to write them 4159 * before the UID -> sequence number mapping changes. */ 4160 if (isset($opts['pipeline'])) { 4161 $this->_updateCache($opts['pipeline']->fetch); 4162 } 4163 4164 $res = parent::_deleteMsgs($mailbox, $ids); 4165 4166 if (isset($this->_temp['expunged'])) { 4167 $this->_temp['expunged']->add($res); 4168 } 4169 4170 if (!empty($opts['decrement'])) { 4171 $mbox_ob = $this->_mailboxOb(); 4172 $mbox_ob->setStatus( 4173 Horde_Imap_Client::STATUS_MESSAGES, 4174 $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES) - count($ids) 4175 ); 4176 } 4177 } 4178 4179 /* Internal functions. */ 4180 4181 /** 4182 * Return the proper mailbox format object based on the server's 4183 * capabilities. 4184 * 4185 * @param string $mailbox The mailbox. 4186 * @param boolean $list Is this object used in a LIST command? 4187 * 4188 * @return Horde_Imap_Client_Data_Format_Mailbox A mailbox format object. 4189 */ 4190 protected function _getMboxFormatOb($mailbox, $list = false) 4191 { 4192 if ($this->_capability()->isEnabled('UTF8=ACCEPT')) { 4193 try { 4194 return $list 4195 ? new Horde_Imap_Client_Data_Format_ListMailbox_Utf8($mailbox) 4196 : new Horde_Imap_Client_Data_Format_Mailbox_Utf8($mailbox); 4197 } catch (Horde_Imap_Client_Data_Format_Exception $e) {} 4198 } 4199 4200 return $list 4201 ? new Horde_Imap_Client_Data_Format_ListMailbox($mailbox) 4202 : new Horde_Imap_Client_Data_Format_Mailbox($mailbox); 4203 } 4204 4205 /** 4206 * Sends command(s) to the IMAP server. A connection to the server must 4207 * have already been made. 4208 * 4209 * @param mixed $cmd Either a Command object or a Pipeline object. 4210 * 4211 * @return Horde_Imap_Client_Interaction_Pipeline A pipeline object. 4212 * @throws Horde_Imap_Client_Exception 4213 */ 4214 protected function _sendCmd($cmd) 4215 { 4216 $pipeline = ($cmd instanceof Horde_Imap_Client_Interaction_Command) 4217 ? $this->_pipeline($cmd) 4218 : $cmd; 4219 4220 if (!empty($this->_cmdQueue)) { 4221 /* Add commands in reverse order. */ 4222 foreach (array_reverse($this->_cmdQueue) as $val) { 4223 $pipeline->add($val, true); 4224 } 4225 4226 $this->_cmdQueue = array(); 4227 } 4228 4229 $cmd_list = array(); 4230 4231 foreach ($pipeline as $val) { 4232 if ($val->continuation) { 4233 $this->_sendCmdChunk($pipeline, $cmd_list); 4234 $this->_sendCmdChunk($pipeline, array($val)); 4235 $cmd_list = array(); 4236 } else { 4237 $cmd_list[] = $val; 4238 } 4239 } 4240 4241 $this->_sendCmdChunk($pipeline, $cmd_list); 4242 4243 /* If any FLAGS responses contain MODSEQs but not UIDs, don't 4244 * cache any data and immediately close the mailbox. */ 4245 foreach ($pipeline->data['modseqs_nouid'] as $val) { 4246 if (!$pipeline->fetch[$val]->getUid()) { 4247 $this->_debug->info( 4248 'Server provided FLAGS MODSEQ without providing UID.' 4249 ); 4250 $this->close(); 4251 return $pipeline; 4252 } 4253 } 4254 4255 /* Update HIGHESTMODSEQ value. */ 4256 if (!empty($pipeline->data['modseqs'])) { 4257 $modseq = max($pipeline->data['modseqs']); 4258 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_HIGHESTMODSEQ, $modseq); 4259 /* CONDSTORE has not yet updated flag information, so don't update 4260 * modseq yet. */ 4261 if ($this->_capability()->isEnabled('QRESYNC')) { 4262 $this->_updateModSeq($modseq); 4263 } 4264 } 4265 4266 /* Update cache items. */ 4267 $this->_updateCache($pipeline->fetch); 4268 4269 return $pipeline; 4270 } 4271 4272 /** 4273 * Send a chunk of commands and/or continuation fragments to the server. 4274 * 4275 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline 4276 * object. 4277 * @param array $chunk List of commands to send. 4278 * 4279 * @throws Horde_Imap_Client_Exception 4280 */ 4281 protected function _sendCmdChunk($pipeline, $chunk) 4282 { 4283 if (empty($chunk)) { 4284 return; 4285 } 4286 4287 $cmd_count = count($chunk); 4288 $exception = null; 4289 4290 foreach ($chunk as $val) { 4291 $val->pipeline = $pipeline; 4292 4293 try { 4294 if ($this->_processCmd($pipeline, $val, $val)) { 4295 $this->_connection->write('', true); 4296 } else { 4297 $cmd_count = 0; 4298 } 4299 } catch (Horde_Imap_Client_Exception $e) { 4300 switch ($e->getCode()) { 4301 case Horde_Imap_Client_Exception::SERVER_WRITEERROR: 4302 $this->_temp['logout'] = true; 4303 $this->logout(); 4304 break; 4305 } 4306 4307 throw $e; 4308 } 4309 } 4310 4311 while ($cmd_count) { 4312 try { 4313 if ($this->_getLine($pipeline) instanceof Horde_Imap_Client_Interaction_Server_Tagged) { 4314 --$cmd_count; 4315 } 4316 } catch (Horde_Imap_Client_Exception $e) { 4317 switch ($e->getCode()) { 4318 case $e::DISCONNECT: 4319 /* Guaranteed to have no more data incoming, so we can 4320 * immediately logout. */ 4321 $this->_temp['logout'] = true; 4322 $this->logout(); 4323 throw $e; 4324 } 4325 4326 /* For all other issues, catch and store exception; don't 4327 * throw until all input is read since we need to clear 4328 * incoming queue. (For now, only store first exception.) */ 4329 if (is_null($exception)) { 4330 $exception = $e; 4331 } 4332 4333 if (($e instanceof Horde_Imap_Client_Exception_ServerResponse) && 4334 $e->command) { 4335 --$cmd_count; 4336 } 4337 } 4338 } 4339 4340 if (!is_null($exception)) { 4341 throw $exception; 4342 } 4343 } 4344 4345 /** 4346 * Process/send a command to the remote server. 4347 * 4348 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline 4349 * object. 4350 * @param Horde_Imap_Client_Interaction_Command $cmd The master command. 4351 * @param Horde_Imap_Client_Data_Format_List $data Commands to send. 4352 * 4353 * @return boolean True if EOL needed to finish command. 4354 * @throws Horde_Imap_Client_Exception 4355 * @throws Horde_Imap_Client_Exception_NoSupport 4356 */ 4357 protected function _processCmd($pipeline, $cmd, $data) 4358 { 4359 if ($this->_debug->debug && 4360 ($data instanceof Horde_Imap_Client_Interaction_Command)) { 4361 $data->startTimer(); 4362 } 4363 4364 foreach ($data as $key => $val) { 4365 if ($val instanceof Horde_Imap_Client_Interaction_Command_Continuation) { 4366 $this->_connection->write('', true); 4367 4368 /* Check for optional continuation responses when the command 4369 * has already finished. */ 4370 if (!$cmd_continuation = $this->_processCmdContinuation($pipeline, $val->optional)) { 4371 return false; 4372 } 4373 4374 $this->_processCmd( 4375 $pipeline, 4376 $cmd, 4377 $val->getCommands($cmd_continuation) 4378 ); 4379 continue; 4380 } 4381 4382 if (!is_null($debug_msg = array_shift($cmd->debug))) { 4383 $this->_debug->client( 4384 (($cmd == $data) ? $cmd->tag . ' ' : '') . $debug_msg 4385 ); 4386 $this->_connection->client_debug = false; 4387 } 4388 4389 if ($key) { 4390 $this->_connection->write(' '); 4391 } 4392 4393 if ($val instanceof Horde_Imap_Client_Data_Format_List) { 4394 $this->_connection->write('('); 4395 $this->_processCmd($pipeline, $cmd, $val); 4396 $this->_connection->write(')'); 4397 } elseif (($val instanceof Horde_Imap_Client_Data_Format_String) && 4398 $val->literal()) { 4399 $c = $this->_capability(); 4400 4401 /* RFC 6855: If UTF8 extension is available, quote short 4402 * strings instead of sending as literal. */ 4403 if ($c->isEnabled('UTF8=ACCEPT') && ($val->length() < 100)) { 4404 $val->forceQuoted(); 4405 $this->_connection->write($val->escape()); 4406 } else { 4407 /* RFC 3516/4466: Send literal8 if we have binary data. */ 4408 if ($cmd->literal8 && 4409 $val->binary() && 4410 ($c->query('BINARY') || $c->isEnabled('UTF8=ACCEPT'))) { 4411 $binary = true; 4412 $this->_connection->write('~'); 4413 } else { 4414 $binary = false; 4415 } 4416 4417 $literal_len = $val->length(); 4418 $this->_connection->write('{' . $literal_len); 4419 4420 /* RFC 2088 - If LITERAL+ is available, saves a roundtrip 4421 * from the server. */ 4422 if ($cmd->literalplus && $c->query('LITERAL+')) { 4423 $this->_connection->write('+}', true); 4424 } else { 4425 $this->_connection->write('}', true); 4426 $this->_processCmdContinuation($pipeline); 4427 } 4428 4429 if ($debug_msg) { 4430 $this->_connection->client_debug = false; 4431 } 4432 4433 $this->_connection->writeLiteral( 4434 $val->getStream(), 4435 $literal_len, 4436 $binary 4437 ); 4438 } 4439 } else { 4440 $this->_connection->write($val->escape()); 4441 } 4442 } 4443 4444 return true; 4445 } 4446 4447 /** 4448 * Process a command continuation response. 4449 * 4450 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline The pipeline 4451 * object. 4452 * @param boolean $noexception Don't throw 4453 * exception if 4454 * continuation 4455 * does not occur. 4456 * 4457 * @return mixed A Horde_Imap_Client_Interaction_Server_Continuation 4458 * object or false. 4459 * 4460 * @throws Horde_Imap_Client_Exception 4461 */ 4462 protected function _processCmdContinuation($pipeline, $noexception = false) 4463 { 4464 do { 4465 $ob = $this->_getLine($pipeline); 4466 } while ($ob instanceof Horde_Imap_Client_Interaction_Server_Untagged); 4467 4468 if ($ob instanceof Horde_Imap_Client_Interaction_Server_Continuation) { 4469 return $ob; 4470 } elseif ($noexception) { 4471 return false; 4472 } 4473 4474 $this->_debug->info( 4475 'ERROR: Unexpected response from server while waiting for a continuation request.' 4476 ); 4477 $e = new Horde_Imap_Client_Exception( 4478 Horde_Imap_Client_Translation::r("Error when communicating with the mail server."), 4479 Horde_Imap_Client_Exception::SERVER_READERROR 4480 ); 4481 $e->details = strval($ob); 4482 4483 throw $e; 4484 } 4485 4486 /** 4487 * Shortcut to creating a new IMAP client command object. 4488 * 4489 * @param string $cmd The IMAP command. 4490 * 4491 * @return Horde_Imap_Client_Interaction_Command A command object. 4492 */ 4493 protected function _command($cmd) 4494 { 4495 return new Horde_Imap_Client_Interaction_Command($cmd, ++$this->_tag); 4496 } 4497 4498 /** 4499 * Shortcut to creating a new pipeline object. 4500 * 4501 * @param Horde_Imap_Client_Interaction_Command $cmd An IMAP command to 4502 * add. 4503 * 4504 * @return Horde_Imap_Client_Interaction_Pipeline A pipeline object. 4505 */ 4506 protected function _pipeline($cmd = null) 4507 { 4508 if (!isset($this->_temp['fetchob'])) { 4509 $this->_temp['fetchob'] = new Horde_Imap_Client_Fetch_Results( 4510 $this->_fetchDataClass, 4511 Horde_Imap_Client_Fetch_Results::SEQUENCE 4512 ); 4513 } 4514 4515 $ob = new Horde_Imap_Client_Interaction_Pipeline( 4516 clone $this->_temp['fetchob'] 4517 ); 4518 4519 if (!is_null($cmd)) { 4520 $ob->add($cmd); 4521 } 4522 4523 return $ob; 4524 } 4525 4526 /** 4527 * Gets data from the IMAP server stream and parses it. 4528 * 4529 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4530 * object. 4531 * 4532 * @return Horde_Imap_Client_Interaction_Server Server object. 4533 * 4534 * @throws Horde_Imap_Client_Exception 4535 */ 4536 protected function _getLine( 4537 Horde_Imap_Client_Interaction_Pipeline $pipeline 4538 ) 4539 { 4540 $server = Horde_Imap_Client_Interaction_Server::create( 4541 $this->_connection->read() 4542 ); 4543 4544 switch (get_class($server)) { 4545 case 'Horde_Imap_Client_Interaction_Server_Continuation': 4546 $this->_responseCode($pipeline, $server); 4547 break; 4548 4549 case 'Horde_Imap_Client_Interaction_Server_Tagged': 4550 $cmd = $pipeline->complete($server); 4551 if (is_null($cmd)) { 4552 /* This indicates a "dangling" tagged response - it was either 4553 * generated by an aborted previous pipeline object or is the 4554 * result of spurious output by the server. Ignore. */ 4555 return $this->_getLine($pipeline); 4556 } 4557 4558 if ($timer = $cmd->getTimer()) { 4559 $this->_debug->info(sprintf( 4560 'Command %s took %s seconds.', 4561 $cmd->tag, 4562 $timer 4563 )); 4564 } 4565 $this->_responseCode($pipeline, $server); 4566 4567 if (is_callable($cmd->on_success)) { 4568 call_user_func($cmd->on_success); 4569 } 4570 break; 4571 4572 case 'Horde_Imap_Client_Interaction_Server_Untagged': 4573 if (is_null($server->status)) { 4574 $this->_serverResponse($pipeline, $server); 4575 } else { 4576 $this->_responseCode($pipeline, $server); 4577 } 4578 break; 4579 } 4580 4581 switch ($server->status) { 4582 case $server::BAD: 4583 case $server::NO: 4584 /* A tagged BAD response indicates that the tagged command caused 4585 * the error. This information is unknown if untagged (RFC 3501 4586 * [7.1.3]) - ignore these untagged responses. 4587 * An untagged NO response indicates a warning; ignore and assume 4588 * that it also included response text code that is handled 4589 * elsewhere. Throw exception if tagged; command handlers can 4590 * catch this if able to workaround this issue (RFC 3501 4591 * [7.1.2]). */ 4592 if ($server instanceof Horde_Imap_Client_Interaction_Server_Tagged) { 4593 /* Check for a on_error callback. If function returns true, 4594 * ignore the error. */ 4595 if (($cmd = $pipeline->getCmd($server->tag)) && 4596 is_callable($cmd->on_error) && 4597 call_user_func($cmd->on_error)) { 4598 break; 4599 } 4600 4601 throw new Horde_Imap_Client_Exception_ServerResponse( 4602 Horde_Imap_Client_Translation::r("IMAP error reported by server."), 4603 0, 4604 $server, 4605 $pipeline 4606 ); 4607 } 4608 break; 4609 4610 case $server::BYE: 4611 /* A BYE response received as part of a logout command should be 4612 * be treated like a regular command: a client MUST process the 4613 * entire command until logging out (RFC 3501 [3.4; 7.1.5]). */ 4614 if (empty($this->_temp['logout'])) { 4615 $e = new Horde_Imap_Client_Exception( 4616 Horde_Imap_Client_Translation::r("IMAP Server closed the connection."), 4617 Horde_Imap_Client_Exception::DISCONNECT 4618 ); 4619 $e->details = strval($server); 4620 throw $e; 4621 } 4622 break; 4623 4624 case $server::PREAUTH: 4625 /* The user was pre-authenticated. (RFC 3501 [7.1.4]) */ 4626 $this->_temp['preauth'] = true; 4627 break; 4628 } 4629 4630 return $server; 4631 } 4632 4633 /** 4634 * Handle untagged server responses (see RFC 3501 [2.2.2]). 4635 * 4636 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4637 * object. 4638 * @param Horde_Imap_Client_Interaction_Server $ob Server 4639 * response. 4640 */ 4641 protected function _serverResponse( 4642 Horde_Imap_Client_Interaction_Pipeline $pipeline, 4643 Horde_Imap_Client_Interaction_Server $ob 4644 ) 4645 { 4646 $token = $ob->token; 4647 4648 /* First, catch untagged responses where the name appears first on the 4649 * line. */ 4650 switch ($first = Horde_String::upper($token->current())) { 4651 case 'CAPABILITY': 4652 $this->_parseCapability($pipeline, $token->flushIterator()); 4653 break; 4654 4655 case 'LIST': 4656 case 'LSUB': 4657 $this->_parseList($pipeline, $token); 4658 break; 4659 4660 case 'STATUS': 4661 // Parse a STATUS response (RFC 3501 [7.2.4]). 4662 $this->_parseStatus($token); 4663 break; 4664 4665 case 'SEARCH': 4666 case 'SORT': 4667 // Parse a SEARCH/SORT response (RFC 3501 [7.2.5] & RFC 5256 [4]). 4668 $this->_parseSearch($pipeline, $token->flushIterator()); 4669 break; 4670 4671 case 'ESEARCH': 4672 // Parse an ESEARCH response (RFC 4466 [2.6.2]). 4673 $this->_parseEsearch($pipeline, $token); 4674 break; 4675 4676 case 'FLAGS': 4677 $token->next(); 4678 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FLAGS, array_map('Horde_String::lower', $token->flushIterator())); 4679 break; 4680 4681 case 'QUOTA': 4682 $this->_parseQuota($pipeline, $token); 4683 break; 4684 4685 case 'QUOTAROOT': 4686 // Ignore this line - we can get this information from 4687 // the untagged QUOTA responses. 4688 break; 4689 4690 case 'NAMESPACE': 4691 $this->_parseNamespace($pipeline, $token); 4692 break; 4693 4694 case 'THREAD': 4695 $this->_parseThread($pipeline, $token); 4696 break; 4697 4698 case 'ACL': 4699 $this->_parseACL($pipeline, $token); 4700 break; 4701 4702 case 'LISTRIGHTS': 4703 $this->_parseListRights($pipeline, $token); 4704 break; 4705 4706 case 'MYRIGHTS': 4707 $this->_parseMyRights($pipeline, $token); 4708 break; 4709 4710 case 'ID': 4711 // ID extension (RFC 2971) 4712 $this->_parseID($pipeline, $token); 4713 break; 4714 4715 case 'ENABLED': 4716 // ENABLE extension (RFC 5161) 4717 $this->_parseEnabled($token); 4718 break; 4719 4720 case 'LANGUAGE': 4721 // LANGUAGE extension (RFC 5255 [3.2]) 4722 $this->_parseLanguage($token); 4723 break; 4724 4725 case 'COMPARATOR': 4726 // I18NLEVEL=2 extension (RFC 5255 [4.7]) 4727 $this->_parseComparator($pipeline, $token); 4728 break; 4729 4730 case 'VANISHED': 4731 // QRESYNC extension (RFC 7162 [3.2.10]) 4732 $this->_parseVanished($pipeline, $token); 4733 break; 4734 4735 case 'ANNOTATION': 4736 // Parse an ANNOTATION response. 4737 $this->_parseAnnotation($pipeline, $token); 4738 break; 4739 4740 case 'METADATA': 4741 // Parse a METADATA response. 4742 $this->_parseMetadata($pipeline, $token); 4743 break; 4744 4745 default: 4746 // Next, look for responses where the keywords occur second. 4747 switch (Horde_String::upper($token->next())) { 4748 case 'EXISTS': 4749 // EXISTS response - RFC 3501 [7.3.2] 4750 $mbox_ob = $this->_mailboxOb(); 4751 4752 // Increment UIDNEXT if it is set. 4753 if ($mbox_ob->open && 4754 ($uidnext = $mbox_ob->getStatus(Horde_Imap_Client::STATUS_UIDNEXT))) { 4755 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $uidnext + $first - $mbox_ob->getStatus(Horde_Imap_Client::STATUS_MESSAGES)); 4756 } 4757 4758 $mbox_ob->setStatus(Horde_Imap_Client::STATUS_MESSAGES, $first); 4759 break; 4760 4761 case 'RECENT': 4762 // RECENT response - RFC 3501 [7.3.1] 4763 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_RECENT, $first); 4764 break; 4765 4766 case 'EXPUNGE': 4767 // EXPUNGE response - RFC 3501 [7.4.1] 4768 $this->_deleteMsgs($this->_selected, $this->getIdsOb($first, true), array( 4769 'decrement' => true, 4770 'pipeline' => $pipeline 4771 )); 4772 $pipeline->data['expunge_seen'] = true; 4773 break; 4774 4775 case 'FETCH': 4776 // FETCH response - RFC 3501 [7.4.2] 4777 $this->_parseFetch($pipeline, $first, $token); 4778 break; 4779 } 4780 break; 4781 } 4782 } 4783 4784 /** 4785 * Handle status responses (see RFC 3501 [7.1]). 4786 * 4787 * @param Horde_Imap_Client_Interaction_Pipeline $pipeline Pipeline 4788 * object. 4789 * @param Horde_Imap_Client_Interaction_Server $ob Server object. 4790 * 4791 * @throws Horde_Imap_Client_Exception_ServerResponse 4792 */ 4793 protected function _responseCode( 4794 Horde_Imap_Client_Interaction_Pipeline $pipeline, 4795 Horde_Imap_Client_Interaction_Server $ob 4796 ) 4797 { 4798 if (is_null($ob->responseCode)) { 4799 return; 4800 } 4801 4802 $rc = $ob->responseCode; 4803 4804 switch ($rc->code) { 4805 case 'ALERT': 4806 // Defined by RFC 5530 [3] - Treat as an alert for now. 4807 case 'CONTACTADMIN': 4808 // Used by Gmail - Treat as an alert for now. 4809 // http://mailman13.u.washington.edu/pipermail/imap-protocol/2014-September/002324.html 4810 case 'WEBALERT': 4811 $this->_alerts->add(strval($ob->token), $rc->code); 4812 break; 4813 4814 case 'BADCHARSET': 4815 /* Store valid search charsets if returned by server. */ 4816 $s = $this->search_charset; 4817 foreach ($rc->data[0] as $val) { 4818 $s->setValid($val, true); 4819 } 4820 4821 throw new Horde_Imap_Client_Exception_ServerResponse( 4822 Horde_Imap_Client_Translation::r("Charset used in search query is not supported on the mail server."), 4823 Horde_Imap_Client_Exception::BADCHARSET, 4824 $ob, 4825 $pipeline 4826 ); 4827 4828 case 'CAPABILITY': 4829 $this->_parseCapability($pipeline, $rc->data); 4830 break; 4831 4832 case 'PARSE': 4833 /* Only throw error on NO/BAD. Message is human readable. */ 4834 switch ($ob->status) { 4835 case Horde_Imap_Client_Interaction_Server::BAD: 4836 case Horde_Imap_Client_Interaction_Server::NO: 4837 $e = new Horde_Imap_Client_Exception_ServerResponse( 4838 Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message: %s"), 4839 Horde_Imap_Client_Exception::PARSEERROR, 4840 $ob, 4841 $pipeline 4842 ); 4843 $e->messagePrintf(array(strval($ob->token))); 4844 throw $e; 4845 } 4846 break; 4847 4848 case 'READ-ONLY': 4849 $this->_mode = Horde_Imap_Client::OPEN_READONLY; 4850 break; 4851 4852 case 'READ-WRITE': 4853 $this->_mode = Horde_Imap_Client::OPEN_READWRITE; 4854 break; 4855 4856 case 'TRYCREATE': 4857 // RFC 3501 [7.1] 4858 $pipeline->data['trycreate'] = true; 4859 break; 4860 4861 case 'PERMANENTFLAGS': 4862 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_PERMFLAGS, array_map('Horde_String::lower', $rc->data[0])); 4863 break; 4864 4865 case 'UIDNEXT': 4866 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNEXT, $rc->data[0]); 4867 break; 4868 4869 case 'UIDVALIDITY': 4870 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDVALIDITY, $rc->data[0]); 4871 break; 4872 4873 case 'UNSEEN': 4874 /* This is different from the STATUS UNSEEN response - this item, 4875 * if defined, returns the first UNSEEN message in the mailbox. */ 4876 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_FIRSTUNSEEN, $rc->data[0]); 4877 break; 4878 4879 case 'REFERRAL': 4880 // Defined by RFC 2221 4881 $pipeline->data['referral'] = new Horde_Imap_Client_Url_Imap($rc->data[0]); 4882 break; 4883 4884 case 'UNKNOWN-CTE': 4885 // Defined by RFC 3516 4886 throw new Horde_Imap_Client_Exception_ServerResponse( 4887 Horde_Imap_Client_Translation::r("The mail server was unable to parse the contents of the mail message."), 4888 Horde_Imap_Client_Exception::UNKNOWNCTE, 4889 $ob, 4890 $pipeline 4891 ); 4892 4893 case 'APPENDUID': 4894 // Defined by RFC 4315 4895 // APPENDUID: [0] = UIDVALIDITY, [1] = UID(s) 4896 $pipeline->data['appenduid'] = $this->getIdsOb($rc->data[1]); 4897 break; 4898 4899 case 'COPYUID': 4900 // Defined by RFC 4315 4901 // COPYUID: [0] = UIDVALIDITY, [1] = UIDFROM, [2] = UIDTO 4902 $pipeline->data['copyuid'] = array_combine( 4903 $this->getIdsOb($rc->data[1])->ids, 4904 $this->getIdsOb($rc->data[2])->ids 4905 ); 4906 4907 /* Use UIDPLUS information to move cached data to new mailbox (see 4908 * RFC 4549 [4.2.2.1]). Need to move now, because a MOVE might 4909 * EXPUNGE immediately afterwards. */ 4910 $this->_moveCache($pipeline->data['copydest'], $pipeline->data['copyuid'], $rc->data[0]); 4911 break; 4912 4913 case 'UIDNOTSTICKY': 4914 // Defined by RFC 4315 [3] 4915 $this->_mailboxOb()->setStatus(Horde_Imap_Client::STATUS_UIDNOTSTICKY, true); 4916 break; 4917 4918 case 'BADURL': 4919 // Defined by RFC 4469 [4.1] 4920 throw new Horde_Imap_Client_Exception_ServerResponse( 4921 Horde_Imap_Client_Translation::r("Could not save message on server."), 4922 Horde_Imap_Client_Exception::CATENATE_BADURL, 4923 $ob, 4924 $pipeline 4925 ); 4926 4927 case 'TOOBIG': 4928 // Defined by RFC 4469 [4.2] 4929 throw new Horde_Imap_Client_Exception_ServerResponse( 4930 Horde_Imap_Client_Translation::r("Could not save message data because it is too large."), 4931 Horde_Imap_Client_Exception::CATENATE_TOOBIG, 4932 $ob, 4933 $pipeline 4934 ); 4935 4936 case 'HIGHESTMODSEQ': 4937 // Defined by RFC 7162 [3.1.2.1] 4938 $pipeline->data['modseqs'][] = $rc->data[0]; 4939 break; 4940 4941 case 'NOMODSEQ': 4942 // Defined by RFC 7162 [3.1.2.2] 4943 $pipeline->data['modseqs'][] = 0; 4944 break; 4945 4946 case 'MODIFIED': 4947 // Defined by RFC 7162 [3.1.3] 4948 $pipeline->data['modified']->add($rc->data[0]); 4949 break; 4950 4951 case 'CLOSED': 4952 // Defined by RFC 7162 [3.2.11] 4953 if (isset($pipeline->data['qresyncmbox'])) { 4954 /* If there is any pending FETCH cache entries, flush them 4955 * now before changing mailboxes. */ 4956 $this->_updateCache($pipeline->fetch); 4957 $pipeline->fetch->clear(); 4958 4959 $this->_changeSelected( 4960 $pipeline->data['qresyncmbox'][0], 4961 $pipeline->data['qresyncmbox'][1] 4962 ); 4963 unset($pipeline->data['qresyncmbox']); 4964 } 4965 break; 4966 4967 case 'NOTSAVED': 4968 // Defined by RFC 5182 [2.5] 4969 $pipeline->data['searchnotsaved'] = true; 4970 break; 4971 4972 case 'BADCOMPARATOR': 4973 // Defined by RFC 5255 [4.9] 4974 throw new Horde_Imap_Client_Exception_ServerResponse( 4975 Horde_Imap_Client_Translation::r("The comparison algorithm was not recognized by the server."), 4976 Horde_Imap_Client_Exception::BADCOMPARATOR, 4977 $ob, 4978 $pipeline 4979 ); 4980 4981 case 'METADATA': 4982 $md = $rc->data[0]; 4983 4984 switch ($md[0]) { 4985 case 'LONGENTRIES': 4986 // Defined by RFC 5464 [4.2.1] 4987 $pipeline->data['metadata']['*longentries'] = intval($md[1]); 4988 break; 4989 4990 case 'MAXSIZE': 4991 // Defined by RFC 5464 [4.3] 4992 throw new Horde_Imap_Client_Exception_ServerResponse( 4993 Horde_Imap_Client_Translation::r("The metadata item could not be saved because it is too large."), 4994 Horde_Imap_Client_Exception::METADATA_MAXSIZE, 4995 $ob, 4996 $pipeline 4997 ); 4998 4999 case 'NOPRIVATE': 5000 // Defined by RFC 5464 [4.3] 5001 throw new Horde_Imap_Client_Exception_ServerResponse( 5002 Horde_Imap_Client_Translation::r("The metadata item could not be saved because the server does not support private annotations."), 5003 Horde_Imap_Client_Exception::METADATA_NOPRIVATE, 5004 $ob, 5005 $pipeline 5006 ); 5007 5008 case 'TOOMANY': 5009 // Defined by RFC 5464 [4.3] 5010 throw new Horde_Imap_Client_Exception_ServerResponse( 5011 Horde_Imap_Client_Translation::r("The metadata item could not be saved because the maximum number of annotations has been exceeded."), 5012 Horde_Imap_Client_Exception::METADATA_TOOMANY, 5013 $ob, 5014 $pipeline 5015 ); 5016 } 5017 break; 5018 5019 case 'UNAVAILABLE': 5020 // Defined by RFC 5530 [3] 5021 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 5022 Horde_Imap_Client_Translation::r("Remote server is temporarily unavailable."), 5023 Horde_Imap_Client_Exception::LOGIN_UNAVAILABLE 5024 ); 5025 break; 5026 5027 case 'AUTHENTICATIONFAILED': 5028 // Defined by RFC 5530 [3] 5029 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 5030 Horde_Imap_Client_Translation::r("Authentication failed."), 5031 Horde_Imap_Client_Exception::LOGIN_AUTHENTICATIONFAILED 5032 ); 5033 break; 5034 5035 case 'AUTHORIZATIONFAILED': 5036 // Defined by RFC 5530 [3] 5037 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 5038 Horde_Imap_Client_Translation::r("Authentication was successful, but authorization failed."), 5039 Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED 5040 ); 5041 break; 5042 5043 case 'EXPIRED': 5044 // Defined by RFC 5530 [3] 5045 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 5046 Horde_Imap_Client_Translation::r("Authentication credentials have expired."), 5047 Horde_Imap_Client_Exception::LOGIN_EXPIRED 5048 ); 5049 break; 5050 5051 case 'PRIVACYREQUIRED': 5052 // Defined by RFC 5530 [3] 5053 $pipeline->data['loginerr'] = new Horde_Imap_Client_Exception( 5054 Horde_Imap_Client_Translation::r("Operation failed due to a lack of a secure connection."), 5055 Horde_Imap_Client_Exception::LOGIN_PRIVACYREQUIRED 5056 ); 5057 break; 5058 5059 case 'NOPERM': 5060 // Defined by RFC 5530 [3] 5061 throw new Horde_Imap_Client_Exception_ServerResponse( 5062 Horde_Imap_Client_Translation::r("You do not have adequate permissions to carry out this operation."), 5063 Horde_Imap_Client_Exception::NOPERM, 5064 $ob, 5065 $pipeline 5066 ); 5067 5068 case 'INUSE': 5069 // Defined by RFC 5530 [3] 5070 throw new Horde_Imap_Client_Exception_ServerResponse( 5071 Horde_Imap_Client_Translation::r("There was a temporary issue when attempting this operation. Please try again later."), 5072 Horde_Imap_Client_Exception::INUSE, 5073 $ob, 5074 $pipeline 5075 ); 5076 5077 case 'EXPUNGEISSUED': 5078 // Defined by RFC 5530 [3] 5079 $pipeline->data['expungeissued'] = true; 5080 break; 5081 5082 case 'CORRUPTION': 5083 // Defined by RFC 5530 [3] 5084 throw new Horde_Imap_Client_Exception_ServerResponse( 5085 Horde_Imap_Client_Translation::r("The mail server is reporting corrupt data in your mailbox."), 5086 Horde_Imap_Client_Exception::CORRUPTION, 5087 $ob, 5088 $pipeline 5089 ); 5090 5091 case 'SERVERBUG': 5092 case 'CLIENTBUG': 5093 case 'CANNOT': 5094 // Defined by RFC 5530 [3] 5095 $this->_debug->info( 5096 'ERROR: mail server explicitly reporting an error.' 5097 ); 5098 break; 5099 5100 case 'LIMIT': 5101 // Defined by RFC 5530 [3] 5102 throw new Horde_Imap_Client_Exception_ServerResponse( 5103 Horde_Imap_Client_Translation::r("The mail server has denied the request."), 5104 Horde_Imap_Client_Exception::LIMIT, 5105 $ob, 5106 $pipeline 5107 ); 5108 5109 case 'OVERQUOTA': 5110 // Defined by RFC 5530 [3] 5111 throw new Horde_Imap_Client_Exception_ServerResponse( 5112 Horde_Imap_Client_Translation::r("The operation failed because the quota has been exceeded on the mail server."), 5113 Horde_Imap_Client_Exception::OVERQUOTA, 5114 $ob, 5115 $pipeline 5116 ); 5117 5118 case 'ALREADYEXISTS': 5119 // Defined by RFC 5530 [3] 5120 throw new Horde_Imap_Client_Exception_ServerResponse( 5121 Horde_Imap_Client_Translation::r("The object could not be created because it already exists."), 5122 Horde_Imap_Client_Exception::ALREADYEXISTS, 5123 $ob, 5124 $pipeline 5125 ); 5126 5127 case 'NONEXISTENT': 5128 // Defined by RFC 5530 [3] 5129 throw new Horde_Imap_Client_Exception_ServerResponse( 5130 Horde_Imap_Client_Translation::r("The object could not be deleted because it does not exist."), 5131 Horde_Imap_Client_Exception::NONEXISTENT, 5132 $ob, 5133 $pipeline 5134 ); 5135 5136 case 'USEATTR': 5137 // Defined by RFC 6154 [3] 5138 throw new Horde_Imap_Client_Exception_ServerResponse( 5139 Horde_Imap_Client_Translation::r("The special-use attribute requested for the mailbox is not supported."), 5140 Horde_Imap_Client_Exception::USEATTR, 5141 $ob, 5142 $pipeline 5143 ); 5144 5145 case 'DOWNGRADED': 5146 // Defined by RFC 6858 [3] 5147 $downgraded = $this->getIdsOb($rc->data[0]); 5148 foreach ($pipeline->fetch as $val) { 5149 if (in_array($val->getUid(), $downgraded)) { 5150 $val->setDowngraded(true); 5151 } 5152 } 5153 break; 5154 5155 case 'XPROXYREUSE': 5156 // The proxy connection was reused, so no need to do login tasks. 5157 $pipeline->data['proxyreuse'] = true; 5158 break; 5159 5160 default: 5161 // Unknown response codes SHOULD be ignored - RFC 3501 [7.1] 5162 break; 5163 } 5164 } 5165 5166 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body