diff --git a/doc/folders.rst b/doc/folders.rst index f55d2c4..08d78d4 100644 --- a/doc/folders.rst +++ b/doc/folders.rst @@ -1,208 +1,239 @@ ============ ``/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 Result** +**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 Result** +**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//folders`` -====================================== - -Retrieve the number of sub-folders (as an ``X-Count`` response header value). - ``HEAD /folders//objects`` ====================================== Retrieve an object count (as an ``X-Count`` response header value). -``POST /folders`` -================= - -Create a folder. - -**Example Result** - -``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 Result** - -``POST /folders/169aad0b52725a31/deleteobjects`` with ``[112,113]`` will delete objects 112 and 113. +``POST /folders//search`` +===================================== -``POST /folders//empty`` -==================================== +Search objects in the folder specified with ``uid``. Supported search +fields: TEXT, BODY, and any header name. -Clear out the contents of the folder. +.. NOTE:: -``POST /folders//move/`` -================================================ + The response includes properties that can be filtered. For example, + ``POST /folders/169aad0b52725a31/search?properties=uid`` will return only the uids of + objects. -Move objects to another folder. The first folder uid is the source, the second folder uid the target. +.. NOTE:: -Supply a list of object uids to move. + For now only folders of type mail can be searched. -``HEAD /folders/`` -============================== +**Example** -Verify the folder exists. +``POST --data '{"subject":"valid"}' /folders/169aad0b52725a31/search?properties=subject,uid`` -``PUT /folders/`` -============================= +.. parsed-literal:: -Update the folder properties. + [ + (...) + { + "subject": "Domain Validation", + "uid": "112" + }, + (...) + ] diff --git a/lib/api/folders.php b/lib/api/folders.php index e093687..d149a71 100644 --- a/lib/api/folders.php +++ b/lib/api/folders.php @@ -1,294 +1,319 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_folders extends kolab_api { protected $model = 'folder'; public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; $request_length = count($path); if (!$request_length && $method == 'POST') { $this->api_folder_create(); } else if (!$request_length && $method == 'GET') { $this->api_folder_list_folders(); } else if ($request_length >= 1) { switch (strtolower((string) $path[1])) { case 'objects': if ($method == 'HEAD') { $this->api_folder_count_objects(); } else if ($method == 'GET') { $this->api_folder_list_objects(); } break; case 'folders': if ($method == 'HEAD') { $this->api_folder_count_folders(); } else if ($method == 'GET') { $this->api_folder_list_folders(); } break; + case 'search': + if ($method == 'POST') { + $this->api_folder_search(); + } + break; + case 'empty': if ($method == 'POST') { $this->api_folder_empty(); } break; case 'deleteobjects': if ($method == 'POST') { $this->api_folder_delete_objects(); } break; case 'move': if ($method == 'POST') { $this->api_folder_move_objects(); } break; case '': if ($request_length == 1) { if ($method == 'GET') { $this->api_folder_info(); } else if ($method == 'PUT') { $this->api_folder_update(); } else if ($method == 'HEAD') { $this->api_folder_exists(); } else if ($method == 'DELETE') { $this->api_folder_delete(); } } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Returns list of folders (all or subfolders of specified folder) */ protected function api_folder_list_folders() { $root = $this->input->path[0]; $list = $this->backend->folders_list(); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; // filter by parent if ($root) { $this->filter_folders($list, $root); } $this->output->send($list, $this->model . '-list', null, $props); } /** * Returns count of folders (all or subfolders of specified folder) */ protected function api_folder_count_folders() { $root = $this->input->path[0]; $list = $this->backend->folders_list(); // filter by parent if ($root) { $this->filter_folders($list, $root); } $this->output->headers(array('X-Count' => count($list))); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Returns folders info */ protected function api_folder_info() { $folder = $this->input->path[0]; $info = $this->backend->folder_info($folder); $context = array('object' => $info); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $this->output->send($info, $this->model, $context, $props); } /** * Checks if folder exists */ protected function api_folder_exists() { $folder = $this->input->path[0]; $folder = $this->backend->folder_info($folder); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Delete a folder */ protected function api_folder_delete() { $folder = $this->input->path[0]; $folder = $this->backend->folder_info($folder); $folder->delete(); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Update specified folder (rename or set folder type, or state) */ protected function api_folder_update() { $folder = $this->input->path[0]; $info = $this->backend->folder_info($folder); $input = $this->input->input($this->model, false, $info); $input->save(); $this->output->send(array('uid' => $folder), $this->model, null, array('uid')); } /** * Create a folder */ protected function api_folder_create() { $input = $this->input->input($this->model); $uid = $input->save(); $this->output->send(array('uid' => $uid), $this->model, null, array('uid')); } /** * Returns list of objects (not folders) in specified folder */ protected function api_folder_list_objects() { $folder = $this->input->path[0]; $type = $this->backend->folder_type($folder); $list = $this->backend->objects_list($folder); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array('folder_uid' => $folder); $this->output->send($list, $type . '-list', $context, $props); } /** * Returns count of objects (not folders) in specified folder */ protected function api_folder_count_objects() { $folder = $this->input->path[0]; $count = $this->backend->objects_count($folder); $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } + /** + * Search objects in specified folder + */ + protected function api_folder_search() + { + $search = $this->input->input('search'); + if (empty($search)) { + throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "Missing search criteria"); + } + + $folder = $this->input->path[0]; + $type = $this->backend->folder_type($folder); + $list = $this->backend->objects_list($folder, $search); + $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; + $context = array('folder_uid' => $folder); + + $this->output->send($list, $type . '-list', $context, $props); + } + /** * Delete objects in specified folder */ protected function api_folder_delete_objects() { $folder = $this->input->path[0]; $set = $this->input->input(); if (empty($set)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $this->backend->objects_delete($folder, $set); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Move objects into specified folder */ protected function api_folder_move_objects() { $folder = $this->input->path[0]; $target = $this->input->path[2]; $set = $this->input->input(); if (empty($set)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if ($target === null || $target === '' || $target === $folder) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $this->backend->objects_move($folder, $target, $set); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove all objects in specified folder */ protected function api_folder_empty() { $folder = $this->input->path[0]; $this->backend->objects_delete($folder, '*'); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Filter folders by parent */ protected function filter_folders(&$list, $parent) { // filter by parent if ($parent && ($parent = $this->backend->folder_info($parent))) { // we'll compare with 'fullpath' which is in UTF-8 $path = $parent->fullpath . $this->backend->delimiter; foreach ($list as $idx => $folder) { if (strpos($folder->fullpath, $path) !== 0) { unset($list[$idx]); } } } $list = array_values($list); } } diff --git a/lib/input/json.php b/lib/input/json.php index efb81f9..3c0a5db 100644 --- a/lib/input/json.php +++ b/lib/input/json.php @@ -1,309 +1,312 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json extends kolab_api_input { /** * Get request data (JSON) * * @param string Expected object type * @param bool Disable filters application * @param array Original object data (set on update requests) * * @return array Request data */ public function input($type = null, $disable_filters = false, $original = null) { if ($this->input_body === null) { $data = file_get_contents('php://input'); $data = trim($data); if (!empty($data) && $this->api->config->get('kolab_api_debug')) { rcube::console($data); } $this->input_body = $data = json_decode($data, true); } if (!$disable_filters) { if ($this->filter) { if (!empty($original)) { // convert object data into API format $data = $this->api->get_object_data($original, $type); } $this->filter->input_body($this->input_body, $type, $data); } // convert input to internal kolab_storage format if ($type) { $class = "kolab_api_input_json_$type"; - $model = new $class; - $model->input($this->input_body, $original); + + if (class_exists($class)) { + $model = new $class; + $model->input($this->input_body, $original); + } } } return $this->input_body; } /** * Convert xCard/xCal date and date-time into internal DateTime * * @param array|string Date or Date-Time * * @return DateTime */ public static function to_datetime($input) { if (empty($input)) { return; } if (is_array($input)) { if ($input['date-time']) { if ($input['parameters']['tzid']) { $tzid = str_replace('/kolab.org/', '', $input['parameters']['tzid']); } else { $tzid = 'UTC'; } $datetime = $input['date-time']; try { $timezone = new DateTimeZone($tzid); } catch (Exception $e) {} } else if ($input['timestamp']) { $datetime = $input['timestamp']; } else if ($input['date']) { $datetime = $input['date']; $is_date = true; } else { return; } } else { $datetime = $input; $is_date = preg_match('/^[0-9]{4}-?[0-9]{2}-?[0-9]{2}$/', $input); } try { $dt = new DateTime($datetime, $timezone ?: new DateTimeZone('UTC')); } catch (Exception $e) { return; } // Horde_Date (via libcalendaring) doesn't like some timezones // @TODO: don't use Horde_Date or fix it there if ($dt->getTimezone()->getName() == 'Z') { $dt->setTimezone(new DateTimeZone('UTC')); } if ($is_date) { $dt->_dateonly = true; $dt->setTime(0, 0, 0); } return $dt; } /** * Convert PHP DateTime into xCard/xCal date or date-time * * @param DateTime $datetime DateTime object * @param string $timezone Timezone identifier * * @return array */ public static function from_datetime($datetime, $timezone = null) { $format = $datetime->_dateonly ? 'Y-m-d' : 'Y-m-d\TH:i:s'; $type = $datetime->_dateonly ? 'date' : 'date-time'; if ($timezone && $timezone != 'UTC') { try { $tz = new DateTimeZone($timezone); $datetime->setTimezone($tz); $result['parameters'] = array( 'tzid' => '/kolab.org/' . $timezone ); } catch (Exception $e) { } } else if (!$datetime->_dateonly) { $format .= '\Z'; } $result[$type] = $datetime->format($format); return $result; } /** * Add x-custom fields to the result */ public static function add_x_custom($data, &$result) { if (array_key_exists('x-custom', (array) $data)) { $value = (array) $data['x-custom']; foreach ((array) $value as $idx => $v) { if ($v['identifier'] && $v['value'] !== null) { $value[$idx] = array($v['identifier'], $v['value']); } else { unset($value[$idx]); } } $result['x-custom'] = $value; } } /** * Parse mailto URI, e.g. attendee/cal-address property * * @param string $uri Mailto: uri * @param string $params Element parameters * * @return string E-mail address */ public static function parse_mailto_uri($uri, &$params = array()) { if (strpos($uri, 'mailto:') === 0) { $uri = substr($uri, 7); $uri = rawurldecode($uri); $emails = rcube_mime::decode_address_list($uri, 1, true, null, false); $email = $emails[1]; if (!empty($email['mailto'])) { if (empty($params['cn']) && !empty($email['name'])) { $params['cn'] = $email['name']; } return $email['mailto']; } } } /** * Parse attendees property input * * @param array $attendees Attendees list * * @return array Attendees list in kolab_format_xcal format */ public static function parse_attendees($attendees) { foreach ((array) $attendees as $idx => $attendee) { $params = $attendee['parameters']; $email = kolab_api_input_json::parse_mailto_uri($attendee['cal-address'], $params); foreach (array('to', 'from') as $val) { foreach ((array) $params['delegated-' . $val] as $del) { if ($del_email = kolab_api_input_json::parse_mailto_uri($del, $params)) { $delegated[$val][] = $del_email; } } } if ($email) { $attendees[$idx] = array_filter(array( 'email' => $email, 'name' => $params['cn'], 'status' => $params['partstat'], 'role' => $params['role'], 'rsvp' => (bool) $params['rsvp'] || strtoupper($params['rsvp']) === 'TRUE', 'cutype' => $params['cutype'], 'dir' => $params['dir'], 'delegated-to' => $delegated['to'], 'delegated-from' => $delegated['from'], )); } else { unset($attendees[$idx]); } } return $attendees; } /** * Handle recurrence rule input * * @param array $data Input data * @param array $result Result data */ public static function parse_recurrence($data, &$result) { if (array_key_exists('rrule', $data)) { $result['recurrence'] = array(); $recur_keys = array( 'freq', 'interval', 'count', 'bymonth', 'bymonthday', 'byday', 'byyearday', 'bysetpos', 'byhour', 'byminute', 'bysecond', 'wkst', ); foreach ($recur_keys as $key) { if ($data['rrule']['recur'][$key]) { $result['recurrence'][strtoupper($key)] = $data['rrule']['recur'][$key]; } } if ($data['rrule']['recur']['until']) { $result['recurrence']['UNTIL'] = self::to_datetime($data['rrule']['recur']['until']); } } // Recurrence: deleted exceptions (EXDATE) if (array_key_exists('exdate', $data)) { $result['recurrence']['EXDATE'] = array(); if (!empty($data['exdate']['date'])) { $result['recurrence']['EXDATE'] = (array) $data['exdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $result['recurrence']['EXDATE'] = (array) $data['exdate']['date-time']; } } // Recurrence (RDATE) if (array_key_exists('rdate', $data)) { $result['recurrence']['RDATE'] = array(); if (!empty($data['rdate']['date'])) { $result['recurrence']['RDATE'] = (array) $data['rdate']['date']; } else if (!empty($data['exdate']['date-time'])) { $result['recurrence']['RDATE'] = (array) $data['rdate']['date-time']; } } } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index 3c4701b..823cdb9 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1180 +1,1239 @@ | +--------------------------------------------------------------------------+ | 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(); /** * 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 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) + public function objects_list($uid, $search = array()) { - $type = $this->folder_type($uid); + $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 { - // Make sure for contact folders we take also - // distribution-lists into account (see also #5209) - if ($type == 'contact') { - $filter = array(array('type', '=', array('contact', 'distribution-list'))); - } - $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 string $uid Folder unique identifier + * @param array $search Search criteria * * @return int Objects count * @throws kolab_api_exception */ - public function objects_count($uid) + public function objects_count($uid, $search = array()) { - $type = $this->folder_type($uid); + $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, 'ALL'); + $result = $this->storage->count($folder); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); - $result = $folder->count(); + $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"); + } + + 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); } } diff --git a/lib/kolab_api_exception.php b/lib/kolab_api_exception.php index 2ac19bb..1fce538 100644 --- a/lib/kolab_api_exception.php +++ b/lib/kolab_api_exception.php @@ -1,83 +1,83 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Main exception class for Kolab REST API responses */ class kolab_api_exception extends Exception { const UNAUTHORIZED = 401; const FORBIDDEN = 403; const NOT_FOUND = 404; const TIMEOUT = 408; const INVALID_REQUEST = 422; const SERVER_ERROR = 500; const NOT_IMPLEMENTED = 501; const UNAVAILABLE = 503; private $messages = array( self::UNAUTHORIZED => 'Unauthorized', self::FORBIDDEN => 'Forbidden', self::NOT_FOUND => 'Not found', self::TIMEOUT => 'Request timeout', self::INVALID_REQUEST => 'Invalid request', self::SERVER_ERROR => 'Internal server error', self::NOT_IMPLEMENTED => 'Not implemented', self::UNAVAILABLE => 'Service unavailable', ); /** * Constructor * * @param int HTTP error code (default 500) * @param array Optional error info to log * @param string Optional error message */ function __construct($code = 0, $error = array(), $message = null) { if (!$message) { $message = $this->messages[$code]; } if (!$message) { $code = self::SERVER_ERROR; $message = $this->messages[self::SERVER_ERROR]; } if (!empty($error)) { rcube::raise_error($error, true, false); } else { $api = kolab_api::get_instance(); if ($api->config->get('kolab_api_debug')) { - rcube::console('Error: ' . $code); + rcube::console("Error: $code $message"); } } parent::__construct($message, $code); } }