1<?php 2/** 3 * An IMAP based driver for accessing Kolab storage. 4 * 5 * PHP version 5 6 * 7 * @category Kolab 8 * @package Kolab_Storage 9 * @author Gunnar Wrobel <wrobel@pardus.de> 10 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 11 */ 12 13/** 14 * The IMAP driver class for accessing Kolab storage. 15 * 16 * Copyright 2009-2017 Horde LLC (http://www.horde.org/) 17 * 18 * See the enclosed file COPYING for license information (LGPL). If you 19 * did not receive this file, see http://www.horde.org/licenses/lgpl21. 20 * 21 * @category Kolab 22 * @package Kolab_Storage 23 * @author Gunnar Wrobel <wrobel@pardus.de> 24 * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 25 */ 26class Horde_Kolab_Storage_Driver_Imap 27extends Horde_Kolab_Storage_Driver_Base 28{ 29 /** 30 * Create the backend driver. 31 * 32 * @return mixed The backend driver. 33 */ 34 public function createBackend() 35 { 36 $config = $this->getParams(); 37 $config['hostspec'] = $config['host']; 38 unset($config['host']); 39 if (isset($config['debug']) && $config['debug'] == 'STDOUT') { 40 $config['debug'] = STDOUT; 41 } 42 return new Horde_Imap_Client_Socket($config); 43 } 44 45 /** 46 * Retrieves a list of folders from the server. 47 * 48 * @return array The list of folders. 49 */ 50 public function listFolders() 51 { 52 try { 53 return $this->getBackend()->listMailboxes( 54 '*', Horde_Imap_Client::MBOX_ALL, array('flat' => true)); 55 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 56 throw new Horde_Kolab_Storage_Exception($e->details); 57 } catch (Horde_Imap_Client_Exception $e) { 58 throw new Horde_Kolab_Storage_Exception($e); 59 } 60 } 61 62 /** 63 * Create the specified folder. 64 * 65 * @param string $folder The folder to create. 66 * 67 * @return NULL 68 */ 69 public function create($folder) 70 { 71 try { 72 return $this->getBackend()->createMailbox($folder); 73 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 74 throw new Horde_Kolab_Storage_Exception($e->details); 75 } catch (Horde_Imap_Client_Exception $e) { 76 throw new Horde_Kolab_Storage_Exception($e); 77 } 78 } 79 80 /** 81 * Delete the specified folder. 82 * 83 * @param string $folder The folder to delete. 84 * 85 * @return NULL 86 */ 87 public function delete($folder) 88 { 89 try { 90 $this->getBackend()->deleteMailbox($folder); 91 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 92 throw new Horde_Kolab_Storage_Exception($e->details); 93 } catch (Horde_Imap_Client_Exception $e) { 94 throw new Horde_Kolab_Storage_Exception($e); 95 } 96 } 97 98 /** 99 * Rename the specified folder. 100 * 101 * @param string $old The folder to rename. 102 * @param string $new The new name of the folder. 103 * 104 * @return NULL 105 */ 106 public function rename($old, $new) 107 { 108 try { 109 $this->getBackend()->renameMailbox($old, $new); 110 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 111 throw new Horde_Kolab_Storage_Exception($e->details); 112 } catch (Horde_Imap_Client_Exception $e) { 113 throw new Horde_Kolab_Storage_Exception($e); 114 } 115 } 116 117 /** 118 * Does the backend support ACL? 119 * 120 * @return boolean True if the backend supports ACLs. 121 */ 122 public function hasAclSupport() 123 { 124 try { 125 return $this->getBackend()->queryCapability('ACL'); 126 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 127 throw new Horde_Kolab_Storage_Exception($e->details); 128 } catch (Horde_Imap_Client_Exception $e) { 129 throw new Horde_Kolab_Storage_Exception($e); 130 } 131 } 132 133 /** 134 * Retrieve the access rights for a folder. 135 * 136 * @param Horde_Kolab_Storage_Folder $folder The folder to retrieve the ACL for. 137 * 138 * @return An array of rights. 139 */ 140 public function getAcl($folder) 141 { 142 try { 143 $acl = $this->getBackend()->getACL($folder); 144 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 145 throw new Horde_Kolab_Storage_Exception($e->details); 146 } catch (Horde_Imap_Client_Exception $e) { 147 throw new Horde_Kolab_Storage_Exception($e); 148 } 149 150 $result = array(); 151 foreach ($acl as $user => $rights) { 152 $result[$user] = strval($rights); 153 } 154 155 return $result; 156 } 157 158 /** 159 * Retrieve the access rights the current user has on a folder. 160 * 161 * @param string $folder The folder to retrieve the user ACL for. 162 * 163 * @return string The user rights. 164 */ 165 public function getMyAcl($folder) 166 { 167 try { 168 return strval($this->getBackend()->getMyACLRights($folder)); 169 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 170 throw new Horde_Kolab_Storage_Exception($e->details); 171 } catch (Horde_Imap_Client_Exception $e) { 172 throw new Horde_Kolab_Storage_Exception($e); 173 } 174 } 175 176 /** 177 * Set the access rights for a folder. 178 * 179 * @param string $folder The folder to act upon. 180 * @param string $user The user to set the ACL for. 181 * @param string $acl The ACL. 182 * 183 * @return NULL 184 */ 185 public function setAcl($folder, $user, $acl) 186 { 187 try { 188 $this->getBackend()->setACL($folder, $user, array('rights' => $acl)); 189 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 190 throw new Horde_Kolab_Storage_Exception($e->details); 191 } catch (Horde_Imap_Client_Exception $e) { 192 throw new Horde_Kolab_Storage_Exception($e); 193 } 194 } 195 196 /** 197 * Delete the access rights for user on a folder. 198 * 199 * @param string $folder The folder to act upon. 200 * @param string $user The user to delete the ACL for 201 * 202 * @return NULL 203 */ 204 public function deleteAcl($folder, $user) 205 { 206 try { 207 $this->getBackend()->deleteACL($folder, $user); 208 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 209 throw new Horde_Kolab_Storage_Exception($e->details); 210 } catch (Horde_Imap_Client_Exception $e) { 211 throw new Horde_Kolab_Storage_Exception($e); 212 } 213 } 214 215 /** 216 * Retrieves the specified annotation for the complete list of folders. 217 * 218 * @param string $annotation The name of the annotation to retrieve. 219 * 220 * @return array An associative array combining the folder names as key with 221 * the corresponding annotation value. 222 */ 223 public function listAnnotation($annotation) 224 { 225 $data = array(); 226 227 try { 228 foreach ($this->listFolders() as $val) { 229 if (strlen($res = $this->getAnnotation((string)$val, $annotation))) { 230 $data[(string)$val] = $res; 231 } 232 } 233 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 234 throw new Horde_Kolab_Storage_Exception($e->details); 235 } catch (Horde_Imap_Client_Exception $e) { 236 throw new Horde_Kolab_Storage_Exception($e); 237 } 238 239 return $data; 240 } 241 242 /** 243 * Fetches the annotation from a folder. 244 * 245 * @param string $folder The name of the folder. 246 * @param string $annotation The annotation to get. 247 * 248 * @return string The annotation value. 249 */ 250 public function getAnnotation($folder, $annotation) 251 { 252 try { 253 $result = $this->getBackend()->getMetadata($folder, $annotation); 254 } catch (Exception $e) { 255 return ''; 256 } 257 return isset($result[$folder][$annotation]) ? $result[$folder][$annotation] : ''; 258 } 259 260 /** 261 * Sets the annotation on a folder. 262 * 263 * @param string $folder The name of the folder. 264 * @param string $annotation The annotation to set. 265 * @param array $value The values to set 266 * 267 * @return NULL 268 */ 269 public function setAnnotation($folder, $annotation, $value) 270 { 271 try { 272 return $this->getBackend()->setMetadata( 273 $folder, array($annotation => $value) 274 ); 275 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 276 throw new Horde_Kolab_Storage_Exception($e->details); 277 } catch (Horde_Imap_Client_Exception $e) { 278 throw new Horde_Kolab_Storage_Exception($e); 279 } 280 } 281 282 /** 283 * Retrieve the namespace information for this connection. 284 * 285 * @return Horde_Kolab_Storage_Driver_Namespace The initialized namespace handler. 286 */ 287 public function getNamespace() 288 { 289 if ($this->_namespace !== null) { 290 return parent::getNamespace(); 291 } 292 try { 293 $this->getBackend()->login(); 294 if ($this->getBackend()->queryCapability('NAMESPACE') === true) { 295 $c = array(); 296 $configuration = $this->getParam('namespaces', array()); 297 foreach ($this->getBackend()->getNamespaces() as $namespace) { 298 if (in_array($namespace['name'], array_keys($configuration))) { 299 $namespace = array_merge($namespace, $configuration[$namespace['name']]); 300 } 301 302 switch ($namespace['type']) { 303 case Horde_Imap_Client::NS_PERSONAL: 304 $namespace['type'] = Horde_Kolab_Storage_Folder_Namespace::PERSONAL; 305 break; 306 307 case Horde_Imap_Client::NS_OTHER: 308 $namespace['type'] = Horde_Kolab_Storage_Folder_Namespace::OTHER; 309 break; 310 311 case Horde_Imap_Client::NS_SHARED: 312 $namespace['type'] = Horde_Kolab_Storage_Folder_Namespace::SHARED; 313 break; 314 } 315 316 $c[] = $namespace; 317 } 318 $this->_namespace = $this->getFactory()->createNamespace('imap', $this->getAuth(), $c); 319 } 320 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 321 throw new Horde_Kolab_Storage_Exception($e->details); 322 } catch (Horde_Imap_Client_Exception $e) { 323 throw new Horde_Kolab_Storage_Exception($e); 324 } 325 326 return parent::getNamespace(); 327 } 328 329 /** 330 * Opens the given folder. 331 * 332 * @param string $folder The folder to open 333 * 334 * @return NULL 335 */ 336 public function select($folder, $mode = Horde_Imap_Client::OPEN_AUTO) 337 { 338 try { 339 $this->getBackend()->openMailbox($folder, $mode); 340 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 341 throw new Horde_Kolab_Storage_Exception($e->details); 342 } catch (Horde_Imap_Client_Exception $e) { 343 throw new Horde_Kolab_Storage_Exception($e); 344 } 345 } 346 347 /** 348 * Returns the status of the current folder. 349 * 350 * @param string $folder Check the status of this folder. 351 * 352 * @return array An array that contains 'uidvalidity', 'uidnext', and 353 * 'token'. 354 */ 355 public function status($folder) 356 { 357 try { 358 $status = $this->getBackend()->status( 359 $folder, 360 Horde_Imap_Client::STATUS_UIDNEXT | 361 Horde_Imap_Client::STATUS_UIDVALIDITY | 362 Horde_Imap_Client::STATUS_FORCE_REFRESH 363 ); 364 $status['token'] = $this->getBackend()->getSyncToken($folder); 365 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 366 throw new Horde_Kolab_Storage_Exception($e->details); 367 } catch (Horde_Imap_Client_Exception $e) { 368 throw new Horde_Kolab_Storage_Exception($e); 369 } 370 371 return $status; 372 } 373 374 /** 375 * Synchrozine using a token provided by the IMAP client. 376 * 377 * @param string $folder The folder to synchronize. 378 * @param string $token The sync token provided by the IMAP client. 379 * @param array $ids The list of IMAP message UIDs we currently know 380 * about. If omitted, the server will return 381 * VANISHED data only if it supports QRESYNC. 382 * 383 * @return array An array containing the following keys and values: 384 * Horde_Kolab_Storage_Folder_Stamp_Uids::DELETED - Contains the UIDs that 385 * have VANISHED from the IMAP server. 386 * Horde_Kolab_Storage_Folder_Stamp_Uids::ADDED - Contains the UIDs that 387 * have been added to the IMAP server since the last sync. 388 */ 389 public function sync($folder, $token, array $ids = array()) 390 { 391 $mbox = new Horde_Imap_Client_Mailbox($folder); 392 $options = array('ids' => new Horde_Imap_Client_Ids($ids)); 393 $sync_data = $this->getBackend()->sync($mbox, $token, $options); 394 if ($sync_data->flags) { 395 // Flag changes, we must check for /deleted since some Kolab clients 396 // like e.g., Kontact only flag as /deleted and do not automatially 397 // expunge. 398 $query = new Horde_Imap_Client_Search_Query(); 399 $query->flag(Horde_Imap_Client::FLAG_DELETED); 400 $query->ids($sync_data->flagsuids); 401 $search_ret = $this->getBackend()->search($mbox, $query); 402 $deleted = array_merge($sync_data->vanisheduids->ids, $search_ret['match']->ids); 403 } else { 404 $deleted = $sync_data->vanisheduids->ids; 405 } 406 407 return array( 408 Horde_Kolab_Storage_Folder_Stamp_Uids::DELETED => $deleted, 409 Horde_Kolab_Storage_Folder_Stamp_Uids::ADDED => $sync_data->newmsgsuids->ids 410 ); 411 } 412 413 /** 414 * Returns a stamp for the current folder status. This stamp can be used to 415 * identify changes in the folder data. This method, as opposed to 416 * self::getStamp(), uses the IMAP client's token to calculate the changes. 417 * 418 * @param string $folder Return the stamp for this folder. 419 * @param string $token A sync token provided by the IMAP server. 420 * @param array $ids An array of UIDs that we know about. 421 * 422 * @return Horde_Kolab_Storage_Folder_Stamp A stamp indicating the current 423 * folder status. 424 */ 425 public function getStampFromToken($folder, $token, array $ids) 426 { 427 // always get folder status first, then sync() 428 $status = $this->status($folder); 429 $sync = $this->sync($folder, $token, $ids); 430 431 $ids = array_diff( 432 $ids, 433 $sync[Horde_Kolab_Storage_Folder_Stamp_Uids::DELETED] 434 ); 435 $ids = array_merge( 436 $ids, 437 $sync[Horde_Kolab_Storage_Folder_Stamp_Uids::ADDED] 438 ); 439 return new Horde_Kolab_Storage_Folder_Stamp_Uids( 440 $status, 441 $ids 442 ); 443 } 444 445 /** 446 * Returns the message ids of the messages in this folder. 447 * 448 * @param string $folder Check the status of this folder. 449 * 450 * @return array The message ids. 451 */ 452 public function getUids($folder) 453 { 454 $search_query = new Horde_Imap_Client_Search_Query(); 455 $search_query->flag('DELETED', false); 456 try { 457 $uidsearch = $this->getBackend()->search($folder, $search_query); 458 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 459 throw new Horde_Kolab_Storage_Exception($e->details); 460 } catch (Horde_Imap_Client_Exception $e) { 461 throw new Horde_Kolab_Storage_Exception($e); 462 } 463 $uids = $uidsearch['match']; 464 465 return $uids->ids; 466 } 467 468 /** 469 * Retrieves a complete message. 470 * 471 * @param string $folder The folder to fetch the messages from. 472 * @param array $uid The message UID. 473 * 474 * @return array The message encapsuled as an array that contains a 475 * Horde_Mime_Headers and a Horde_Mime_Part object. 476 */ 477 public function fetchComplete($folder, $uid) 478 { 479 $query = new Horde_Imap_Client_Fetch_Query(); 480 $query->fullText(); 481 482 try { 483 $ret = $this->getBackend()->fetch( 484 $folder, 485 $query, 486 array('ids' => new Horde_Imap_Client_Ids($uid)) 487 ); 488 489 if (!isset($ret[$uid])) { 490 throw new Horde_Kolab_Storage_Exception( 491 sprintf( 492 Horde_Kolab_Storage_Translation::t( 493 "Failed fetching message %s in folder %s." 494 ), $uid, $folder 495 ) 496 ); 497 } 498 499 $msg = $ret[$uid]->getFullMsg(); 500 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 501 throw new Horde_Kolab_Storage_Exception($e->details); 502 } catch (Horde_Imap_Client_Exception $e) { 503 throw new Horde_Kolab_Storage_Exception($e); 504 } 505 return array( 506 Horde_Mime_Headers::parseHeaders($msg), 507 Horde_Mime_Part::parseMessage($msg) 508 ); 509 } 510 511 /** 512 * Retrieves the message headers. 513 * 514 * @param string $folder The folder to fetch the message from. 515 * @param array $uid The message UID. 516 * 517 * @return Horde_Mime_Headers The message headers. 518 */ 519 public function fetchHeaders($folder, $uid) 520 { 521 $query = new Horde_Imap_Client_Fetch_Query(); 522 $query->headerText(); 523 524 try { 525 $ret = $this->getBackend()->fetch( 526 $folder, 527 $query, 528 array('ids' => new Horde_Imap_Client_Ids($uid)) 529 ); 530 $msg = $ret[$uid]->getHeaderText(); 531 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 532 throw new Horde_Kolab_Storage_Exception($e->details); 533 } catch (Horde_Imap_Client_Exception $e) { 534 throw new Horde_Kolab_Storage_Exception($e); 535 } 536 return Horde_Mime_Headers::parseHeaders($msg); 537 } 538 539 /** 540 * Retrieves the messages for the given message ids. 541 * 542 * @param string $folder The folder to fetch the messages from. 543 * @param array $uids The message UIDs. 544 * 545 * @return array An array of message structures parsed into Horde_Mime_Part 546 * instances. 547 */ 548 public function fetchStructure($folder, $uids) 549 { 550 if (empty($uids)) { 551 return array(); 552 } 553 554 $query = new Horde_Imap_Client_Fetch_Query(); 555 $query->structure(); 556 557 try { 558 $ret = $this->getBackend()->fetch( 559 $folder, 560 $query, 561 array('ids' => new Horde_Imap_Client_Ids($uids)) 562 ); 563 564 $out = array(); 565 foreach ($ret as $key => $result) { 566 $out[$key]['structure'] = $result->getStructure(); 567 } 568 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 569 throw new Horde_Kolab_Storage_Exception($e->details); 570 } catch (Horde_Imap_Client_Exception $e) { 571 throw new Horde_Kolab_Storage_Exception($e); 572 } 573 574 return $out; 575 } 576 577 /** 578 * Retrieves a bodypart for the given message ID and mime part ID. 579 * 580 * @param string $folder The folder to fetch the messages from. 581 * @param array $uid The message UID. 582 * @param array $id The mime part ID. 583 * 584 * @return resource The body part, as a stream resource. The contents are 585 * already transfer decoded and presented as 8bit data. 586 */ 587 public function fetchBodypart($folder, $uid, $id) 588 { 589 $query = new Horde_Imap_Client_Fetch_Query(); 590 $query->structure(); 591 $query->bodyPart($id, array('decode' => true)); 592 593 try { 594 $ret = $this->getBackend()->fetch( 595 $folder, 596 $query, 597 array('ids' => new Horde_Imap_Client_Ids($uid)) 598 ); 599 600 // Already decoded? 601 if ($ret[$uid]->getBodyPartDecode($id)) { 602 return $ret[$uid]->getBodyPart($id, true); 603 } 604 605 // Not already decoded, let Horde_Mime do it. 606 $part = $ret[$uid]->getStructure()->getPart($id); 607 $part->setContents( 608 $ret[$uid]->getBodyPart($id, true), 609 array( 610 'encoding' => $ret[$uid]->getBodyPartDecode($id), 611 'usestream' => true 612 ) 613 ); 614 return $part->getContents(array('stream' => true)); 615 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 616 throw new Horde_Kolab_Storage_Exception($e->details); 617 } catch (Horde_Imap_Client_Exception $e) { 618 throw new Horde_Kolab_Storage_Exception($e); 619 } 620 } 621 622 /** 623 * Appends a message to the given folder. 624 * 625 * @param string $folder The folder to append the message(s) to. 626 * @param resource $msg The message to append. 627 * 628 * @return mixed True or the UID of the new message in case the backend 629 * supports UIDPLUS. 630 */ 631 public function appendMessage($folder, $msg) 632 { 633 try { 634 $result = $this->getBackend() 635 ->append($folder, array(array('data' => $msg))); 636 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 637 throw new Horde_Kolab_Storage_Exception($e->details); 638 } catch (Horde_Imap_Client_Exception $e) { 639 throw new Horde_Kolab_Storage_Exception($e); 640 } 641 return $result->ids[0]; 642 } 643 644 /** 645 * Deletes messages from the specified folder. 646 * 647 * @param string $folder The folder to delete messages from. 648 * @param integer $uids IMAP message ids. 649 * 650 * @return NULL 651 */ 652 public function deleteMessages($folder, $uids) 653 { 654 try { 655 return $this->getBackend()->store($folder, array( 656 'add' => array('\\deleted'), 657 'ids' => new Horde_Imap_Client_Ids($uids) 658 )); 659 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 660 throw new Horde_Kolab_Storage_Exception($e->details); 661 } catch (Horde_Imap_Client_Exception $e) { 662 throw new Horde_Kolab_Storage_Exception($e); 663 } 664 } 665 666 /** 667 * Moves a message to a new folder. 668 * 669 * @param integer $uid IMAP message id. 670 * @param string $old_folder Source folder. 671 * @param string $new_folder Target folder. 672 * 673 * @return NULL 674 */ 675 public function moveMessage($uid, $old_folder, $new_folder) 676 { 677 $options = array('ids' => new Horde_Imap_Client_Ids($uid), 678 'move' => true); 679 try { 680 return $this->getBackend() 681 ->copy($old_folder, $new_folder, $options); 682 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 683 throw new Horde_Kolab_Storage_Exception($e->details); 684 } catch (Horde_Imap_Client_Exception $e) { 685 throw new Horde_Kolab_Storage_Exception($e); 686 } 687 } 688 689 /** 690 * Expunges messages in the current folder. 691 * 692 * @param string $folder The folder to expunge. 693 * 694 * @return NULL 695 */ 696 public function expunge($folder) 697 { 698 try { 699 return $this->getBackend()->expunge($folder); 700 } catch (Horde_Imap_Client_Exception_ServerResponse $e) { 701 throw new Horde_Kolab_Storage_Exception($e->details); 702 } catch (Horde_Imap_Client_Exception $e) { 703 throw new Horde_Kolab_Storage_Exception($e); 704 } 705 } 706} 707