diff --git a/doc/folders.rst b/doc/folders.rst index 08d78d4..3682c66 100644 --- a/doc/folders.rst +++ b/doc/folders.rst @@ -1,239 +1,237 @@ ============ ``/folders`` ============ ``GET /folders`` ================ Request a full list of folders. **Example Result** .. parsed-literal:: [ (...) { "fullpath": "Calendar", "name": "Calendar", "type": "event.default", "uid": "6c11cd1e5283576e" }, { "fullpath": "Calendar/Personal", "name": "Personal", "parent": "6c11cd1e5283576e", "type": "event", "uid": "2faf3307-7d32-4d22-bb1b-9a8a40fb3872" }, (...) { "fullpath": "Drafts", "name": "Drafts", "type": "mail.drafts", "uid": "16dd16e25283576d" }, (...) { "fullpath": "INBOX", "name": "INBOX", "type": "mail.inbox", "uid": "169aad0b52725a31" }, (...) { "fullpath": "Sent", "name": "Sent", "type": "mail.sentitems", "uid": "5deaff235283576d" }, { "fullpath": "Spam", "name": "Spam", "type": "mail.junkemail", "uid": "5df585705283576e" }, (...) { "fullpath": "Trash", "name": "Trash", "type": "mail.wastebasket", "uid": "0e307e915283576e" } ] .. NOTE:: The response includes properties that can be filtered. For example, ``GET /folders?properties=name`` will return only the names of the folders. .. NOTE:: This API call requires the client to filter the folders by type. ``GET /folders/`` ============================= Get information about a specific folder. **Example** ``GET /folders/169aad0b52725a31`` .. parsed-literal:: { "fullpath": "INBOX", "name": "INBOX", "type": "mail.inbox", "uid": "169aad0b52725a31" } .. NOTE:: This call should include additional metadata about the folder contents; * recent, * unread, * total objects ``DELETE /folders/`` ================================ Delete a folder ``POST /folders`` ================= Create a folder. **Example** ``POST /folders -d '{"name":"Test"}'`` .. parsed-literal: { "uid": "92ba0f35-40fe-4d0c-9b2a-81052e4e2cec" } ``POST /folders//deleteobjects`` ============================================ Delete selected objects from the folder. Supply a list of object UIDs. **Example** ``POST /folders/169aad0b52725a31/deleteobjects`` with ``[112,113]`` will delete objects 112 and 113. ``POST /folders//empty`` ==================================== Clear out the contents of the folder. ``POST /folders//move/`` ================================================ Move objects to another folder. The first folder uid is the source, the second folder uid the target. Supply a list of object uids to move. ``HEAD /folders/`` ============================== Verify the folder exists. ``PUT /folders/`` ============================= Update the folder properties. ``GET /folders//folders`` ===================================== Retrieve a list of sub-folders, if any. ``HEAD /folders//folders`` ====================================== Retrieve the number of sub-folders (as an ``X-Count`` response header value). ``GET /folders//objects`` ===================================== Obtain a list of objects in the folder specified with ``uid``. **Example** ``GET /folders/169aad0b52725a31/objects`` .. parsed-literal:: [ (...) { "bcc": [], "categories": [], "cc": [], "date": "Mon, 4 Sep 2017 01:09:20 +0100", "flags": [ "seen" ], "from": { "address": "noreply_support@comodo.com", "name": "Comodo Security Services" }, "has-attach": false, "internaldate": " 4-Sep-2017 02:16:09 +0200", "message-id": "mid:44", "reply-to": [], "sender": [], "size": 2332, "subject": "Comodo Domain Validation for \*.kolab.org", "to": [ { "address": "administrator@kolab.org", "name": "administrator@kolab.org" } ], "uid": "112" }, (...) ] ``HEAD /folders//objects`` ====================================== Retrieve an object count (as an ``X-Count`` response header value). ``POST /folders//search`` ===================================== Search objects in the folder specified with ``uid``. Supported search -fields: TEXT, BODY, and any header name. +fields for mail search are: TEXT, BODY, and any header name. For other +object types it depends on the object type, e.g. for contact data +you can use: firstname, surname, name, email, tags. .. NOTE:: The response includes properties that can be filtered. For example, ``POST /folders/169aad0b52725a31/search?properties=uid`` will return only the uids of objects. -.. NOTE:: - - For now only folders of type mail can be searched. - **Example** ``POST --data '{"subject":"valid"}' /folders/169aad0b52725a31/search?properties=subject,uid`` .. parsed-literal:: [ (...) { "subject": "Domain Validation", "uid": "112" }, (...) ] diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index 823cdb9..571d87d 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1239 +1,1257 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $api; public $storage; public $username; public $password; public $user; public $delimiter; protected $icache = array(); + protected $search_fields = array( + 'contact' => array('name', 'firstname', 'surname', 'email', 'type', 'tags', 'words'), + 'event' => array('tags', 'words'), + 'task' => array('tags', 'words'), + 'journal' => array('tags', 'words'), + 'note' => array('tags', 'words'), + 'file' => array('tags', 'words', 'filename'), + 'configuration' => array('tags', 'words', 'type'), + 'freebusy' => array('tags', 'words'), + ); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->api = kolab_api::get_instance(); $this->storage = $this->api->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); $this->delimiter = $this->storage->get_hierarchy_delimiter(); if ($_SESSION['user_id']) { $this->user = new rcube_user($_SESSION['user_id']); $this->api->config->set_user_prefs((array)$this->user->get_prefs()); } } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { $host = $this->select_host($username); // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->api->get_cache_shared('kolab_api_auth'); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { $auth = $this->api->plugins->exec_hook('authenticate', array( 'host' => $host, 'user' => $username, 'pass' => $password, )); if ($cache && !$auth['abort']) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error']) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } } else { $auth['pass'] = $password; } // authenticate user against the IMAP server $user_id = $auth['abort'] ? 0 : $this->login($auth['user'], $auth['pass'], $auth['host'], $error); if ($user_id) { $this->username = $auth['user']; $this->password = $auth['pass']; $this->delimiter = $this->storage->get_hierarchy_delimiter(); return true; } // IMAP server failure... send 503 error if ($error == rcube_imap_generic::ERROR_BAD) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } return false; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders (kolab_api_folder), False on backend failure */ public function folders_list($type = null) { $type_keys = array( kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY, ); // get folder unique identifiers and types $uid_data = $this->folder_uids(); $type_data = $this->storage->get_metadata('*', $type_keys); $folders = array(); if (!is_array($type_data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ($uid_data as $folder => $uid) { $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = null; if ($folders[$parent]) { $parent_id = $folders[$parent]->uid; $folders[$parent]->children++; } // parent folder does not exist add it to the list else { for ($i=0; $idelimiter, $parent_arr); if ($folders[$parent]) { $parent_id = $folders[$parent]->uid; $folders[$parent]->children++; } else { $fid = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $data = array( 'name' => array_pop($parent_arr), 'fullpath' => $parent, 'uid' => $fid, 'parent' => $parent_id, ); $folders[$parent] = new kolab_api_folder($data); $parent_id = $fid; } } } } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); // folder type reset($type_keys); foreach ($type_keys as $key) { if ($type = $type_data[$folder][$key]) { $data['type'] = $type; break; } } if (empty($data['type'])) { $data['type'] = 'mail'; } $folders[$path] = new kolab_api_folder($data); } // sort folders uksort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folder_uid2name($uid); $type = kolab_storage::folder_type($folder); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Folder info * * @param string $uid Folder UID * * @return kolab_api_folder Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folder_uid2name($uid); // get IMAP folder info $info = $this->storage->folder_info($folder); // add some more parameters (used in folders list response) $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); $path = explode($this->delimiter, $path); $info['name'] = $path[count($path)-1]; $info['fullpath'] = implode($this->delimiter, $path); $info['uid'] = $uid; $info['type'] = kolab_storage::folder_type($folder, true) ?: 'mail'; if (count($path) > 1) { array_pop($path); $parent = implode($this->delimiter, $path); $parent = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $info['parent'] = $parent; } // convert some info to be more compact if (!empty($info['rights'])) { $info['rights'] = implode('', $info['rights']); } // some info is not very interesting here ;) unset($info['attributes']); return new kolab_api_folder($info); } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * @param array $search Search criteria * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid, $search = array()) { $type = $this->folder_type($uid); $filter = $this->objects_filter($type, $search); // use IMAP to fetch mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); if ($filter) { $this->storage->search($folder, $filter, RCUBE_CHARSET); } $result = $this->storage->list_messages($folder, 1, '', 'ASC'); foreach ($result as $idx => $mail) { $result[$idx] = new kolab_api_mail($mail); } } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->select($filter); if ($result === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * @param array $search Search criteria * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid, $search = array()) { $type = $this->folder_type($uid); $filter = $this->objects_filter($type, $search); // use IMAP to count mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); if ($filter) { $this->storage->search($folder, $filter, RCUBE_CHARSET); } // @TODO: error checking requires changes in rcube_imap $result = $this->storage->count($folder); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->count($filter); } return $result; } /** * Convert search criteria into internal format */ protected function objects_filter($folder_type, $search) { // create IMAP SEARCH string for mail messages if ($folder_type === 'mail') { if (empty($search)) { return; } foreach ($search as $field => $word) { $field = strtoupper($field); $word = rcube_imap_generic::escape($word); if ($field == 'TEXT') { $subject = "TEXT $word"; } else if ($field == 'BODY') { $subject[] = "BODY $word"; } else { $subject[] = "HEADER $field $word"; } } if (empty($subject)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "Unsupported search criteria"); } $filter = trim(str_repeat(' OR', count($subject) - 1) . implode($subject)); } // otherwise we use kolab_storage else { // Make sure for contact folders we take also // distribution-lists into account (see also #5209) if ($folder_type == 'contact') { $filter = array(array('type', '=', array('contact', 'distribution-list'))); } if (empty($search)) { return $filter; } - // TODO: search criteria - throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "Searching not supported for folders of type $folder_type"); + foreach ($search as $field => $word) { + $field = strtolower($field); + if (in_array($field, $this->search_fields[$folder_type])) { + $filter[] = array($field, '~', $word); + } + } + + if (empty($filter)) { + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "Unsupported search criteria"); + } } return $filter; } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $type = $this->folder_type($uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); } // delete all if ($set === "*") { if ($is_mail) { $result = $this->storage->clear_folder($folder); } else { $result = $folder->delete_all(); } } else { if ($is_mail) { $result = $this->storage->delete_message($set, $folder); } else { foreach ($set as $uid) { $result = $folder->delete($uid); if ($result === false) { break; } } } } // @TODO: should we throw exception when deleting non-existing object? if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Move objects into another folder * * @param string $uid Folder unique identifier * @param string $target_uid Target folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_move($uid, $target_uid, $set) { $type = $this->folder_type($uid); $target_type = $this->folder_type($target_uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); $target = $this->folder_uid2name($target_uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $target = $this->folder_get_by_uid($target_uid, $target_type); } if ($is_mail) { if ($set === "*") { $set = '1:*'; } $result = $this->storage->move_messages($set, $target, $folder); } else { if ($set === "*") { $set = $folder->get_uids(); } foreach ($set as $uid) { $result = $folder->move($uid, $target); if ($result === false) { break; } } } if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return kolab_api_mail|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $type = $this->folder_type($folder_uid); if ($type === 'mail') { $folder = $this->folder_uid2name($folder_uid); $object = new rcube_message($uid, $folder); if (!$object || empty($object->headers)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object = new kolab_api_mail($object); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object = $folder->get_object($uid); if (!$object) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($type != 'configuration') { // get object categories (tag-relations) $object['categories'] = $this->get_tags($object, $object['categories']); } } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (an array or kolab_api_mail) * @param string $type Object type * * @return string Object UID * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype !== 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); return $data->save($folder); } // otherwise use kolab_storage else { if ($type != 'configuration') { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data, $data['_type'])) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!empty($categories)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (array or kolab_api_mail) * @param string $type Object type * * @return string Object UID (it can change) * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype != 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); return $data->save($folder); } // otherwise use kolab_storage else { if ($type != 'configuration') { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data, $data['_type'])) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (array_key_exists('categories', $data)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $fp=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { // object is a mail message if ($object instanceof kolab_api_mail) { return $object->get_part_body($part_id, false, 0, $mode); } // otherwise use kolab_storage else { $this->storage->set_folder($object['_mailbox']); return $this->storage->get_message_part($object['_msguid'], $part_id, null, $mode === -1, is_resource($mode) ? $mode : null, true, 0, false); } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_delete($object, $id) { // object is a mail message if (is_object($object)) { return $object->attachment_delete($id); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Create an attachment and add to a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attach Attachment data * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_create($object, $attach) { // object is a mail message if (is_object($object)) { return $object->attachment_add($attach); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object['_attachments'][] = array( 'name' => $attach->filename, 'mimetype' => $attach->mimetype, 'path' => $attach->path, 'size' => $attach->size, 'content' => $attach->data, ); if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Update an attachment in a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attach Attachment data * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_update($object, $attach) { // object is a mail message if (is_object($object)) { return $object->attachment_update($attach); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $attach->mime_id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object['_attachments'][] = array( 'name' => $attach->filename, 'mimetype' => $attach->mimetype, 'path' => $attach->path, 'size' => $attach->size, 'content' => $attach->data, ); if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Returns IMAP folder name * * @param string $uid Folder identifier * * @return string Folder name (UTF7-IMAP) */ public function folder_uid2name($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // we store last folder in-memory if (isset($this->icache["folder:$uid"])) { return $this->icache["folder:$uid"]; } $uids = $this->folder_uids(); foreach ($uids as $folder => $_uid) { if ($uid === $_uid) { return $this->icache["folder:$uid"] = $folder; } } // slowest method, but we need to try it, the full folders list // might contain non-existing folder (not in folder_uids() result) foreach ($this->folders_list() as $folder) { if ($folder->uid === $uid) { return rcube_charset::convert($folder->fullpath, RCUBE_CHARSET, 'UTF7-IMAP'); } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Helper method to get folder UID * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder's UID */ public function folder_name2uid($folder) { $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array(kolab_storage::UID_KEY_SHARED); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ if (!empty($metadata[$folder])) { foreach ($uid_keys as $key) { if ($uid = $metadata[$folder][$key]) { return $uid; } } } return md5($folder); /* // @TODO: // make sure folder exists // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($folder . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if ($this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_SHARED => $uid))) { return $uid; } // create hash from folder name if we can't write the UID metadata return md5($folder . $this->get_owner()); */ } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1); $path2 = explode($this->delimiter, $str2); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Return UIDs of all folders * * @return array Folder name to UID map */ protected function folder_uids() { $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array(kolab_storage::UID_KEY_SHARED); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ $lambda = function(&$item, $key, $keys) { reset($keys); foreach ($keys as $key) { $item = $item[$key]; return; } }; array_walk($metadata, $lambda, $uid_keys); return $metadata; } /** * Get folder by UID (use only for non-mail folders) * * @param string $uid Folder UID * @param string $type Folder type * * @return kolab_storage_folder Folder object * @throws kolab_api_exception */ protected function folder_get_by_uid($uid, $type = null) { $folder = $this->folder_uid2name($uid); $folder = kolab_storage::get_folder($folder, $type); if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // Check the given storage folder instance for validity and throw // the right exceptions according to the error state. if (!$folder->valid || ($error = $folder->get_error())) { if ($error === kolab_storage::ERROR_IMAP_CONN) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_CACHE_DB) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new kolab_api_exception(kolab_api_exception::FORBIDDEN); } else if ($error === kolab_storage::ERROR_INVALID_FOLDER) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $folder; } /** * Storage host selection */ protected function select_host($username) { // Get IMAP host $host = $this->api->config->get('default_host', 'localhost'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($default_host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ protected function login($username, $password, $host, &$error = null) { if (empty($username)) { return null; } $login_lc = $this->api->config->get('login_lc'); $default_port = $this->api->config->get('default_port', 143); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$this->storage->connect($host, $username, $password, $port, $ssl)) { $error = $this->storage->get_error_code(); return null; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->api->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->api->config->set_user_prefs((array)$this->user->get_prefs()); $_SESSION['user_id'] = $this->user->ID; $_SESSION['username'] = $this->user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $this->api->encrypt($password); $_SESSION['login_time'] = time(); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); return $user->ID; } /** * Returns list of tag-relation names assigned to Kolab object or mail message * * @param array|kolab_api_mail $object Object or message * @param array $categories Old categories to merge with */ public function get_tags($object, $categories = null) { // Kolab object if (is_array($object)) { $ident = $object['uid']; } // Mail message else if (is_object($object)) { // support only messages with message-id $ident = $object->{'message-id'}; $folder = $object->folder; $uid = $object->uid; } if (empty($ident)) { return array(); } $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($ident, 100); $delta = 300; // resolve members if it wasn't done recently if ($uid) { foreach ($tags as $idx => $tag) { $force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta; $members = $config->resolve_members($tag, $force); if (empty($members[$folder]) || !in_array($uid, $members[$folder])) { unset($tags[$idx]); } if ($force) { $this->tag_rts[$tag['uid']] = time(); } } // make sure current folder is set correctly again $this->storage->set_folder($folder); } $tags = array_filter(array_map(function($v) { return $v['name']; }, $tags)); // merge result with old categories if (!empty($categories)) { $tags = array_unique(array_merge($tags, (array) $categories)); } return $tags; } /** * Set tag-relations to kolab object */ public function set_tags($uid, $tags) { $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } }