diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index f748df9..3e77020 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,613 +1,613 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { protected $input; protected $api; protected $attrs_filter; /** * Modify request path * * @param array (Exploded) request path */ public function path(&$path) { // handle differences between OpenChange API and Kolab API // here we do only very basic modifications, just to be able // to select apprioprate api action class if ($path[0] == 'calendars') { $path[0] = 'events'; } } /** * Executed before every api action * * @param kolab_api_input Request */ public function input(&$input) { // handle differences between OpenChange API and Kolab API $this->input = $input; $this->api = $input->api; $this->common_action = !in_array($input->action, array('folders', 'info')); // convert / to // or /// if ($this->common_action && ($uid = $input->path[0])) { list($folder, $msg, $attach) = self::uid_decode($uid); $path = array($folder, $msg); if ($attach) { $path[] = $attach; } array_splice($input->path, 0, 1, $path); } // convert parent_id into path on object create request if ($input->method == 'POST' && $this->common_action && !count($input->path)) { $data = $input->input(null, true); if ($data['parent_id']) { $input->path[0] = $data['parent_id']; } else { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } } // convert parent_id into path on object update request else if ($input->method == 'PUT' && $folder && count($input->path) == 2) { $data = $input->input(null, true); if ($data['parent_id'] && $data['parent_id'] != $folder) { $this->parent_change_handler($data); } } // handle actions on contact photo attachments switch ($input->action) { case 'attachments': $this->attachment_actions_handler(); break; case 'folders': $this->folder_actions_handler(); break; case 'notes': // Notes do not have attachments in Exchange if ($input->path[1] === 'attachments' || count($this->path) > 2) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } break; } } /** * Executed when parsing request body * * @param string Request data * @param string Expected object type * @param string Original object data (set on update requests) */ public function input_body(&$data, $type = null, $original_object = null) { // handle differences between OpenChange API and Kolab API // Note: input->path is already modified by input() and path() above switch ($this->input->action) { case 'folders': // folders//deletemessages input if ($this->input->path[1] == 'deleteobjects') { // Kolab API expects just a list of identifiers, I.e.: // [{"id": "1"}, {"id": "2"}] => ["1", "2"] foreach ((array) $data as $idx => $element) { $data[$idx] = $element['id']; } } break; } switch ($type) { case 'attachment': case 'event': case 'note': case 'task': case 'contact': case 'mail': case 'folder': $model = $this->get_model_class($type); $data = $model->input($data, $original_object); break; } } /** * Apply filter on output data * * @param array Result data * @param string Object type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function output(&$result, $type, $context = null, $attrs_filter = array()) { // handle differences between OpenChange API and Kolab API $model = $this->get_model_class($type); if (!empty($this->attrs_filter)) { $attrs_filter = array_combine($this->attrs_filter, $this->attrs_filter); } else if (!empty($attrs_filter)) { $attrs_filter = $this->attributes_filter($attrs_filter, $type, true); $attrs_filter = array_combine($attrs_filter, $attrs_filter); } // Add contact photo to attachments list if ($this->input->action == 'contacts' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) ) { if ($attachment = kolab_api_filter_mapistore_contact::photo_attachment($context['object'])) { $result[] = $attachment; } } foreach ($result as $idx => $data) { if ($filtered = $model->output($data, $context)) { // apply properties filter (again) if (!empty($attrs_filter)) { $filtered = array_intersect_key($filtered, $attrs_filter); } $result[$idx] = $filtered; } else { unset($result[$idx]); $unset = true; } } if ($unset) { $result = array_values($result); } // cleanup unset($_SESSION['uploads']['MAPIATTACH']); } /** * Executed for response headers * * @param array Response headers * @param array Context (folder_uid, object_uid, object) */ public function headers(&$headers, $context = null) { // handle differences between OpenChange API and Kolab API foreach ($headers as $name => $value) { switch ($name) { case 'X-Count': // Add contact photo to attachments count if ($this->input->action == 'contacts' && $this->input->path[2] == 'attachments' && $context && !empty($context['object']) && kolab_api_filter_mapistore_contact::photo_attachment($context['object']) ) { $value += 1; } $headers['X-mapistore-rowcount'] = $value; unset($headers[$name]); break; } } } /** * Executed for empty response status * * @param int Status code */ public function send_status(&$status) { // handle differences between OpenChange API and Kolab API if ($this->input->method == 'PUT' && !in_array($input->action, array('info'))) { // Mapistore expects 204 on object updates // however, we'd like to send modified UID of the object sometimes // $status = kolab_api_output::STATUS_EMPTY; } } /** * Converts kolab identifiers describind the object into * MAPI identifier that can be easily used in URL. * * @param string Folder UID * @param string Object UID * @param string Optional attachment identifier * * @return string Object identifier */ public static function uid_encode($folder_uid, $msg_uid, $attach_id = null) { $result = array($folder_uid, $msg_uid); if ($attach_id) { $result[] = $attach_id; } $result = array_map(array('kolab_api_filter_mapistore', 'uid_encode_item'), $result); return implode('.', $result); } /** * Converts back the MAPI identifier into kolab folder/object/attachment IDs * * @param string Object identifier * * @return array Object identifiers */ public static function uid_decode($uid) { $result = explode('.', $uid); $result = array_map(array('kolab_api_filter_mapistore', 'uid_decode_item'), $result); return $result; } /** * Encodes UID element */ protected static function uid_encode_item($str) { $fn = function($match) { return '_' . ord($match[1]); }; $str = preg_replace_callback('/([^0-9a-zA-Z-])/', $fn, $str); return $str; } /** * Decodes UID element */ protected static function uid_decode_item($str) { $fn = function($match) { return chr($match[1]); }; $str = preg_replace_callback('/_([0-9]{2})/', $fn, $str); return $str; } /** * Filter property names */ protected function attributes_filter($attrs, $type = null, $reverse = false) { $model = $this->get_model_class($type); return $model->attributes_filter($attrs, $reverse); } /** * Return instance of model class object */ protected function get_model_class($type) { $class = "kolab_api_filter_mapistore_$type"; return new $class($this); } /** * Handles object parent modification (move) */ protected function parent_change_handler($data) { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $target = $data['parent_id']; $api = kolab_api::get_instance(); // move the object $api->backend->objects_move($folder, $target, array($uid)); // replace folder uid in input arguments $this->input->path[0] = $target; // exit if the rest of input is empty if (count($data) < 2) { $api->output->send_status(kolab_api_output::STATUS_EMPTY); } } /** * Overwrite attachment actions for contact photos */ protected function attachment_actions_handler() { if ($this->input->path[2] == kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->api->backend->object_get($folder, $object_uid); $attachment = kolab_api_filter_mapistore_contact::photo_attachment($object); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); if (!$attachment) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // fetch photo info/body if ($this->input->method == 'GET') { $this->api->output->send($attachment, 'attachment', $context); } // photo existence check else if ($this->input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } // photo delete else if ($this->input->method == 'DELETE') { - unset($object['photo']); + $object['photo'] = ''; $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } // photo update else if ($this->input->method == 'PUT') { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); if (empty($data) || empty($data['PidTagAttachDataBinary'])) { $error = "Invalid input for contact photo update request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } } // add photo to contact? else if ($this->input->path[0] && $this->input->path[1] && $this->input->method == 'POST') { $data = $this->input->input(null, true); if ($data['PidTagAttachmentContactPhoto']) { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $object = $this->api->backend->object_get($folder, $object_uid); if (empty($data['PidTagAttachDataBinary'])) { $error = "Invalid input for contact photo create request"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } $object['photo'] = base64_decode($data['PidTagAttachDataBinary']); $this->api->backend->object_update($folder, $object, 'contact'); $this->api->output->send_status(kolab_api_output::STATUS_OK); } } } /** * Overwrite folders actions */ protected function folder_actions_handler() { $input = $this->input; // in OpenChange folders/1/folders means get folders of the IPM Subtree if ($input->method == 'GET' && $input->path[0] === '1' && $input->path[1] == 'folders') { $input->path = array(); $type = 'folder'; } // in OpenChange folders/0/folders means get the hierarchy of the NON-IPM Subtree else if ($input->method == 'GET' && $input->path[0] === '0' && $input->path[1] == 'folders') { $list = $this->get_builtin_folder_list(0); $this->api->output->send($list, 'folder-list', null); } else if ($input->path[1] == 'messages') { $input->path[1] = 'objects'; if ($input->args['properties'] && !$this->is_builtin_folder($this->path[0])) { $type = $this->api->backend->folder_type($input->path[0]); list($type, ) = explode('.', $type); } } else if ($input->path[1] == 'deletemessages') { $input->path[1] = 'deleteobjects'; } // properties filter, map MAPI attribute names to Kolab attributes if ($type && $input->args['properties']) { $this->attrs_filter = explode(',', $this->input->args['properties']); $properties = $this->attributes_filter($this->attrs_filter, $type); $input->args['properties'] = implode(',', $properties); } // request for built-in folder if ($this->is_builtin_folder($input->path[0])) { $folder = $this->get_builtin_folder($input->path[0]); if (count($input->path) == 1) { // folder info if ($input->method == 'GET') { $this->api->output->send($folder, 'folder', null); } // folder exists else if ($input->method == 'HEAD') { $this->api->output->send_status(kolab_api_output::STATUS_OK); } throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } else { switch (strtolower((string) $input->path[1])) { case 'objects': if ($input->method == 'HEAD') { $this->builtin_folder_count_objects(); } else if ($input->method == 'GET') { $this->builtin_folder_list_objects(); } break; case 'folders': if ($input->method == 'HEAD') { $this->builtin_folder_count_folders(); } else if ($input->method == 'GET') { $this->builtin_folder_list_folders(); } break; case 'empty': if ($input->method == 'POST') { $this->builtin_folder_empty(); } break; case 'deleteobjects': if ($input->method == 'POST') { $this->builtin_folder_delete_objects(); } break; } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } } /** * Returns list of built-in folders (NON-IPM subtree) */ protected function get_builtin_folder_list($parent = null) { $folders = kolab_api_filter_mapistore_folder::$builtin_folders; foreach ($folders as $idx => $folder) { if ($parent !== null && $parent != $folder['parent']) { unset($folders[$idx]); continue; } $folders[$idx] = array_merge(array( 'comment' => $folder['name'], 'uid' => $idx, 'hidden' => true, 'role' => 10, 'system_idx' => $idx, ), $folder); } return $folders; } /** * Returns built-in folder information */ protected function get_builtin_folder($uid) { $list = $this->get_builtin_folder_list(); $result = $list[$uid]; if (!$result) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $result; } /** * Check if specified uid is an uid of builtin folder */ protected function is_builtin_folder($uid) { return is_numeric($uid) && strlen($uid) < 4; } /** * Coun objects in built-in folder */ protected function builtin_folder_count_objects() { // @TODO $this->api->output->headers(array('X-Count' => 0)); $this->api->output->send_status(kolab_api_output::STATUS_OK); } /** * List objects in built-in folder */ protected function builtin_folder_list_objects() { // @TODO $this->api->output->send(array(), 'folder-list'); } /** * Count sub-folders of built-in folder */ protected function builtin_folder_count_folders() { // @TODO $this->api->output->headers(array('X-Count' => 0)); $this->api->output->send_status(kolab_api_output::STATUS_OK); } /** * List sub-folders in built-in folder */ protected function builtin_folder_list_folders() { // @TODO $this->api->output->send(array(), 'folder-list'); } /** * Delete all objects in built-in folder */ protected function builtin_folder_empty() { throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } /** * Delete objects in built0in folder */ protected function builtin_folder_delete_objects() { throw new kolab_api_exception(kolab_api_exception::NOT_IMPLEMENTED); } } diff --git a/lib/filter/mapistore/info.php b/lib/filter/mapistore/info.php index b966e19..fc37025 100644 --- a/lib/filter/mapistore/info.php +++ b/lib/filter/mapistore/info.php @@ -1,138 +1,133 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_info { protected $model = 'info'; protected $map = array( 'name' => 'name', 'version' => 'version', 'capabilities' => 'capabilities', // @TODO ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = $data[$kolab_idx]; if ($value === null) { continue; } $result[$mapi_idx] = $value; } $result['contexts'] = $this->contexts_list(); $result = array_filter($result, function($v) { return $v !== null; }); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { return null; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } /** * Get 'contexts' property */ protected function contexts_list() { - $api = kolab_api::get_instance(); - - if (!$api->backend) { - - } - + $api = kolab_api::get_instance(); $builtins = kolab_api_filter_mapistore_folder::$builtin_folders; - $folders = $api->backend->folders_list(); + $folders = $api->backend ? $api->backend->folders_list() : array(); $contexts = array(); foreach ($builtins as $idx => $folder) { if ($folder['parent'] == kolab_api_filter_mapistore_folder::FOLDER_MSGROOT) { // find real folder if ($folder['type']) { reset($folders); foreach ($folders as $f) { if ($folder['name'] == 'INBOX' && $f['name'] == 'INBOX') { $idx = $f['uid']; break; } else if ($folder['type'] == $f['type']) { $idx = $f['uid']; $folder['name'] = $f['name']; } } } $url = $folder['url'] ?: "/folders/$idx/"; $contexts[] = array( 'main_folder' => true, 'name' => $folder['name'], 'role' => $folder['role'], 'system_idx' => $idx, 'url' => $url, ); } } return $contexts; } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index 57eb4d7..a08d079 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1341 +1,1326 @@ | +--------------------------------------------------------------------------+ | 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, 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']; } // 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']; } else { $fid = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $folders[$parent] = array( 'name' => array_pop($parent_arr), 'fullpath' => $parent, 'uid' => $fid, 'parent' => $parent_id, ); $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] = $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; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid) { $type = $this->folder_type($uid); // use IMAP to fetch mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); $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->get_objects(); 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 * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $type = $this->folder_type($uid); // use IMAP to count mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); // @TODO: error checking requires changes in rcube_imap $result = $this->storage->count($folder, 'ALL'); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->count(); } return $result; } /** * 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); } - $old_categories = $object['categories']; - } - - // @TODO: Use relations also for events - if ($type != 'configuration' && $type != 'event') { - // get object categories (tag-relations) - $categories = $this->get_tags($object, $old_categories); - - if ($type === 'mail') { - $object->categories = $categories; - } - else { - $object['categories'] = $categories; + 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); - // @TODO: categories - return $data->save($folder); } // otherwise use kolab_storage else { - // @TODO: Use relations also for events - if (!preg_match('/^(event|configuration)/', $type)) { + 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)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } - // @TODO: Use relations also for events 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 { - // @TODO: Use relations also for events - if (!preg_match('/^(event|configuration)/', $type)) { + 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)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } - // @TODO: Use relations also for events 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']; } } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $name = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP'); if ($parent) { $parent = $this->folder_uid2name($parent); $name = $parent . $this->delimiter . $name; } if ($this->storage->folder_exists($name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $created = kolab_storage::folder_create($name, $type, false, false); if ($created) { $created = $this->folder_name2uid($name); } return $created; } /** * Subscribes a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folder_uid2name($uid); if (isset($updates['type'])) { $result = kolab_storage::set_folder_type($folder, $updates['type']); if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } if (isset($updates['subscribed'])) { if ($updates['subscribed']) { $result = $this->storage->subscribe($folder); } else { $result = $this->storage->unsubscribe($folder); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } // @TODO: active state } /** * Renames a folder * * @param string $old_name Folder name (UTF-8) * @param string $new_name New folder name (UTF-8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_name = rcube_charset::convert($old_name, RCUBE_CHARSET, 'UTF7-IMAP'); $new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP'); if (!strlen($old_name) || !strlen($new_name) || $old_name === $new_name) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if ($this->storage->folder_exists($new_name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $type = kolab_storage::folder_type($old_name); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { $result = $this->storage->rename_folder($old_name, $new_name); } else { $result = kolab_storage::folder_rename($old_name, $new_name); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folder_uid2name($uid); $type = $this->folder_type($uid); // don't use kolab_storage for mail folders if ($type === 'mail') { $status = $this->storage->delete_folder($folder); } else { $status = kolab_storage::folder_delete($folder); } if (!$status) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Folder info * * @param string $uid Folder UID * * @return array 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); // get IMAP folder data $data = $this->storage->folder_data($folder); $info['exists'] = $data['EXISTS']; $info['unseen'] = $data['UNSEEN']; $info['modseq'] = $data['HIGHESTMODSEQ']; // 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']); } // @TODO: subscription status, active state // some info is not very interesting here ;) unset($info['attributes']); return $info; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { $folder = $this->folder_uid2name($uid); return strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); } /** * Returns IMAP folder name * * @param string $uid Folder identifier * * @return string Folder name (UTF7-IMAP) */ protected 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 */ protected 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 + * 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 */ - protected function get_tags($object, $categories = null) + 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); + $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 */ - protected function set_tags($uid, $tags) + public function set_tags($uid, $tags) { - // @TODO: set_tags() for email - $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } } diff --git a/lib/kolab_api_mail.php b/lib/kolab_api_mail.php index bb41454..aac85c0 100644 --- a/lib/kolab_api_mail.php +++ b/lib/kolab_api_mail.php @@ -1,1241 +1,1339 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_mail { /** * List of supported header fields * * @var array */ public static $header_fields = array( 'uid', 'subject', 'from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'message-id', 'references', 'date', 'internaldate', 'content-type', 'priority', 'size', 'flags', 'categories', 'has-attach', ); /** * Original message * * @var rcube_message */ protected $message; /** * Modified properties * * @var array */ protected $data = array(); /** * Validity status * * @var bool */ protected $valid = true; /** * Headers-only mode flag * * @var bool */ protected $is_header = false; /** * Line separator * * @var string */ protected $endln = "\r\n"; protected $body_text; protected $body_html; protected $boundary; protected $attach_data = array(); protected $recipients = array(); protected $from_address; /** * Object constructor * * @param rcube_message|rcube_message_header Original message */ public function __construct($message = null) { $this->message = $message; $this->is_header = $this->message instanceof rcube_message_header; } /** * Properties setter * * @param string $name Property name * @param mixed $value Property value */ public function __set($name, $value) { switch ($name) { case 'flags': $value = (array) $value; break; case 'priority': $value = (int) $value; /* values: 1, 2, 4, 5 */ break; case 'date': case 'subject': case 'from': case 'sender': case 'to': case 'cc': case 'bcc': case 'reply-to': case 'in-reply-to': case 'references': case 'categories': case 'message-id': case 'text': case 'html': // make sure the value is utf-8 if ($value !== null && $value !== '' && (!is_array($value) || !empty($value))) { // make sure we have utf-8 here $value = rcube_charset::clean($value); } break; case 'uid': case 'internaldate': case 'content-type': case 'size': // ignore return; default: // unsupported property, log error? return; } if (!$changed && in_array($name, self::$header_fields)) { $changed = $this->{$name} !== $value; } else { $changed = true; } if ($changed) { $this->data[$name] = $value; } } /** * Properties getter * * @param string $name Property name * * @param mixed Property value */ public function __get($name) { if (array_key_exists($name, $this->data)) { return $this->data[$name]; } if (empty($this->message)) { return; } $headers = $this->is_header ? $this->message : $this->message->headers; $value = null; switch ($name) { case 'uid': return (string) $headers->uid; case 'folder': return $headers->folder; case 'priority': case 'size': if (isset($headers->{$name})) { $value = (int) $headers->{$name}; } break; case 'content-type': $value = $headers->ctype; break; case 'date': case 'internaldate': $value = $headers->{$name}; break; case 'subject': $value = trim(rcube_mime::decode_header($headers->subject, $headers->charset)); break; case 'flags': $value = array_change_key_case((array) $headers->flags); $value = array_filter($value); $value = array_keys($value); break; case 'from': case 'sender': case 'to': case 'cc': case 'bcc': case 'reply-to': $addresses = $headers->{$name == 'reply-to' ? 'replyto' : $name}; $addresses = rcube_mime::decode_address_list($addresses, null, true, $headers->charset); $value = array(); foreach ((array) $addresses as $addr) { $idx = count($value); if ($addr['mailto']) { $value[$idx]['address'] = $addr['mailto']; } if ($addr['name']) { $value[$idx]['name'] = $addr['name']; } } if ($name == 'from' && !empty($value)) { $value = $value[0]; } break; case 'categories': - $value = (array) $headers->categories; + $api = kolab_api::get_instance(); + $value = (array) $api->backend->get_tags($this); break; case 'references': case 'in-reply-to': case 'message-id': $value = $headers->get($name); break; case 'text': case 'html': $value = $this->body($name == 'html'); break; case 'has-attach': if (isset($this->message->attachments)) { $value = !empty($this->message->attachments); } else if (isset($headers->attachments)) { $value = !empty($headers->attachments); } else { // We don't have the whole structure, // we can only make a guess based on message mimetype $regex = '/^(application\/|multipart\/(m|signed|report))/i'; $value = (bool) preg_match($regex, $headers->ctype); } break; case 'attachments': if ($message = $this->get_message()) { return $message->attachments; } return array(); } // add the value to the result if ($value !== null && $value !== '' && (!is_array($value) || !empty($value))) { // make sure we have utf-8 here $value = rcube_charset::clean($value); } return $value; } /** * Return message data as an array * * @param array $filetr Optional properties filter * * @return array Message/headers data */ public function data($filter = array()) { $result = array(); $fields = self::$header_fields; if (!empty($filter)) { $fields = array_intersect($fields, $filter); } foreach ($fields as $field) { $value = $this->{$field}; // add the value to the result if ($value !== null && $value !== '') { $result[$field] = $value; } } // complete rcube_message object, we can set more props, e.g. body content if (!$this->is_header) { foreach (array('text', 'html') as $prop) { if ($value = $this->{$prop}) { $result[$prop] = $value; } } } return $result; } /** * Check if the original message has been modified * * @return bool True if the message has been modified * since the object creation */ public function changed() { return !empty($this->data); } /** * Check object validity * * @return bool True if the object is valid */ public function valid() { if (empty($this->message)) { // @TODO: check required properties of a new message? } return $this->valid; } /** * Get content of a specific part of this message * * @param string $mime_id Part ID * @param boolean $formatted Enables formatting of text/* parts bodies * @param int $max_bytes Only return/read this number of bytes * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string|bool Part content or operation status */ public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null) { if ($message = $this->get_message()) { return $message->get_part_body($mime_id, $formatted, $max_bytes, $mode); } return false; } /** * Save the message in specified folder * * @param string $folder IMAP folder name * * @return string New message UID * @throws kolab_api_exception */ public function save($folder = null) { if (empty($this->data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Nothing to save. Did you use kolab_api_mail::changed()?' )); } $message = $this->get_message(); - $api = kolab_api::get_instance(); if (empty($message) && !strlen($folder)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Folder not specified' )); } // Create message content - $stream = $this->create_message_stream(); - - // Save the message - $uid = $this->save_message($stream, $folder ?: $message->folder); - - // IMAP flags change requested - if (array_key_exists('flags', $this->data)) { - $old_flags = $this->flags; - - // set new flags - foreach ((array) $this->data['flags'] as $flag) { - if (($key = array_search($flag, $old_flags)) !== false) { - unset($old_flags[$key]); - } - else { - $flag = strtoupper($flag); - $api->backend->storage->set_flag($uid, $flag, $message->folder); - } - } - - // unset remaining old flags - foreach ($old_flags as $flag) { - $flag = 'UN' . strtoupper($flag); - $api->backend->storage->set_flag($uid, $flag, $message->folder); - } + if ($stream = $this->create_message_stream()) { + // Save the message + $uid = $this->save_message($stream, $folder ?: $message->folder); + } + // it is a message update without its source change + // e.g. only flags/categories changed + else { + $uid = $this->uid; } + // Update IMAP flags and categories/tags if needed + $this->update_flags($uid, $folder); + $this->update_categories($uid, $folder, $message); + return $uid; } /** * Send the message * * @return bool True on success * @throws kolab_api_exception */ public function send() { // Create message content $stream = $this->create_message_stream(true); if (empty($this->from_address)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "No sender found"); } if (empty($this->recipients)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "No recipients found"); } $this->send_message_stream($stream); return true; } /** * Add an attachment to the message * * @param rcube_message_part $attachment Attachment data * * @return string New message UID on success * @throws kolab_api_exception */ public function attachment_add($attachment) { if (!($message = $this->get_message())) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->attach_data['create'][] = $attachment; // Create message content $stream = $this->create_message_stream(); // Save the message return $this->save_message($stream, $message->folder); } /** * Remove attachment from the message * * @param string $id Attachment id * * @return string New message UID on success * @throws kolab_api_exception */ public function attachment_delete($id) { if (!($message = $this->get_message())) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ((array) $message->attachments as $attach) { if ($attach->mime_id == $id) { $attachment = $attach; } } if (empty($attachment)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $this->attach_data['delete'][] = $attachment; // Create message content $stream = $this->create_message_stream(); // Save the message return $this->save_message($stream, $message->folder); } /** * Update specified attachment in the message * * @param rcube_message_part $attachment Attachment data * * @return string New message UID on success * @throws kolab_api_exception */ public function attachment_update($attachment) { if (!($message = $this->get_message())) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->attach_data['update'][] = $attachment; // Create message content $stream = $this->create_message_stream(); // Save the message return $this->save_message($stream, $message->folder); } /** * Create message stream */ protected function create_message_stream($send_mode = false) { $api = kolab_api::get_instance(); $message = $this->get_message(); $specials = array('flags', 'categories'); $diff = array_diff($this->data, $specials); $headers = array(); $endln = $this->endln; $body_mod = $message && (!empty($diff) || !empty($this->attach_data)); // header change requested, get old headers if ($body_mod) { $api->backend->storage->set_folder($message->folder); $headers = $api->backend->storage->get_raw_headers($message->uid); $headers = self::parse_headers($headers); } foreach ($this->data as $name => $value) { $normalized = self::normalize_header_name($name); unset($headers[$normalized]); switch ($name) { case 'priority': unset($headers['X-Priority']); $priority = intval($value); $priorities = array(1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest'); if ($str_priority = $priorities[$priority]) { $headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority)); } break; case 'date': // @TODO: date-time format $headers['Date'] = $value; break; case 'from': if (!empty($value)) { if (empty($value['address']) || !strpos($value['address'], '@')) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $value = format_email_recipient($value['address'], $value['name']); } $headers[$normalized] = $value; break; case 'to': case 'cc': case 'bcc': case 'reply-to': $recipients = array(); foreach ((array) $value as $adr) { if (!is_array($adr) || empty($adr['address']) || !strpos($adr['address'], '@')) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $recipients[] = format_email_recipient($adr['address'], $adr['name']); } $headers[$normalized] = implode(',', $recipients); break; case 'references': case 'in-reply-to': case 'message-id': case 'subject': if ($value) { $headers[$normalized] = $value; } break; } } // Prepare message body $body_mod = $this->prepare_body($headers); // We're going to send this message, we need more data/checks if ($send_mode) { if (empty($headers['From'])) { // get From: address from default identity of the user? if ($identity = $api->backend->user->get_identity()) { $headers['From'] = format_email_recipient( format_email($identity['email']), $identity['name']); } } $addresses = rcube_mime::decode_address_list($headers['From'], null, false, null, true); $this->from_address = array_shift($addresses); // extract mail recipients foreach (array('To', 'Cc', 'Bcc') as $idx) { if ($headers[$idx]) { $addresses = rcube_mime::decode_address_list($headers[$idx], null, false, null, true); $this->recipients = array_merge($this->recipients, $addresses); } } $this->recipients = array_unique($this->recipients); unset($headers['Bcc']); $headers['Date'] = $api->user_date(); /* if ($mdn_enabled) { $headers['Return-Receipt-To'] = $this->from_address; $headers['Disposition-Notification-To'] = $this->from_address; } */ } // Write message headers to the stream if (!empty($headers) || empty($message) || $body_mod) { // Place Received: headers at the beginning of the message // Spam detectors often flag messages with it after the Subject: as spam if (!empty($headers['Received'])) { $received = $headers['Received']; unset($headers['Received']); $headers = array('Received' => $received) + $headers; } if (empty($headers['MIME-Version'])) { $headers['MIME-Version'] = '1.0'; } // always add User-Agent header if (empty($headers['User-Agent'])) { $headers['User-Agent'] .= kolab_api::APP_NAME . ' ' . kolab_api::VERSION; if ($agent = $api->config->get('useragent')) { $headers['User-Agent'] .= '/' . $agent; } } if (empty($headers['Message-ID'])) { - $headers['Message-ID'] = $this->gen_message_id(); + $headers['Message-ID'] = $this->data['message-id'] = $this->gen_message_id(); } // create new message header if ($stream = fopen('php://temp/maxmemory:10240000', 'r+')) { foreach ($headers as $header_name => $header_value) { if (strlen($header_value)) { $header_value = $this->encode_header($header_name, $header_value); fwrite($stream, $header_name . ": " . $header_value . $endln); } } fwrite($stream, $endln); } else { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Failed to open file stream for mail message' )); } } // Save/update the message body into the stream - $this->write_body($stream, $headers); + if ($stream) { + $this->write_body($stream, $headers); + } return $stream; } /** * Send the message stream using configured method */ protected function send_message_stream($stream) { $api = kolab_api::get_instance(); // send thru SMTP server using custom SMTP library if ($api->config->get('smtp_server')) { // send message if (!is_object($api->smtp)) { $api->smtp_init(true); } rewind($stream); $headers = null; $sent = $api->smtp->send_mail( $this->from_address, $this->recipients, $headers, $stream, $smtp_opts); // log error if (!$sent) { $smtp_response = $api->smtp->get_response(); // $smtp_error = $api->smtp->get_error(); throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'code' => 800, 'type' => 'smtp', 'message' => "SMTP error: " . join("\n", $smtp_response), )); } } else { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'code' => 800, 'type' => 'smtp', 'message' => "SMTP server not configured. Really need smtp_server option to be set.", )); } // $api->plugins->exec_hook('message_sent', array('headers' => array(), 'body' => $stream)); if ($api->config->get('smtp_log')) { rcube::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s", $api->get_user_name(), $_SERVER['REMOTE_ADDR'], implode(',', $this->recipients), !empty($smtp_response) ? join('; ', $smtp_response) : '')); } return true; } /** * Prepare message body and content headers */ protected function prepare_body(&$headers) { $message = $this->get_message(); $body_mod = array_key_exists('text', $this->data) || array_key_exists('html', $this->data) || !empty($this->attach_data); if (!$body_mod) { return false; } if (!empty($this->attach_data['create']) || !empty($this->attach_data['update'])) { $ctype = 'multipart/mixed'; } else { $ctype = $this->data['html'] ? 'multipart/alternative' : 'text/plain'; } // Get/set Content-Type header of the modified message if ($old_ctype = $headers['Content-Type']) { if (preg_match('/boundary="?([a-z0-9-\'\(\)+_\,\.\/:=\? ]+)"?/i', $old_ctype, $matches)) { $boundary = $matches[1]; } if ($pos = strpos($old_ctype, ';')) { $old_ctype = substr($old_ctype, 0, $pos); } if ($old_ctype == 'multipart/mixed') { // replace first part (if it is text/plain or multipart/alternative) $ctype = $old_ctype; } } $headers['Content-Type'] = $ctype; if ($ctype == 'text/plain') { $headers['Content-Type'] .= '; charset=' . RCUBE_CHARSET; } else if (!$boundary) { $boundary = '_' . md5(rand() . microtime()); } if ($boundary) { $headers['Content-Type'] .= ';' . $this->endln . " boundary=\"$boundary\""; } // create message body if ($html = $this->data['html']) { $text = $this->data['text']; if ($text === null) { $h2t = new rcube_html2text($html); $text = $h2t->get_text(); } $this->body_text = quoted_printable_encode($text); $this->body_html = quoted_printable_encode($html); } else if ($text = $this->data['text']) { $headers['Content-Transfer-Encoding'] = 'quoted-printable'; $this->body_text = quoted_printable_encode($text); } $this->boundary = $boundary; // make sure all line endings are CRLF $this->body_text = preg_replace('/\r?\n/', $this->endln, $this->body_text); $this->body_html = preg_replace('/\r?\n/', $this->endln, $this->body_html); return true; } /** * Write message body to the stream */ protected function write_body($stream, $headers) { $api = kolab_api::get_instance(); $endln = $this->endln; $message = $this->get_message(); $body_mod = array_key_exists('text', $this->data) || array_key_exists('html', $this->data); $modified = $body_mod || !empty($this->attach_data); // @TODO: related parts for inline images // nothing changed in the modified message body... if (!$modified && !empty($message)) { // just copy the content to the output stream $api->backend->storage->get_raw_body($message->uid, $stream, 'TEXT'); } // new message creation, or the message does not have any attachments else if (empty($message) || ($message->headers->ctype != 'multipart/mixed' && empty($this->attach_data))) { // Here we do not have attachments yet, so we only have two // simple options: multipart/alternative or text/plain $this->write_body_content($stream, $this->boundary); } // body changed or attachments added, non-multipart message else if ($message->headers->ctype != 'multipart/mixed') { fwrite($stream, '--' . $this->boundary . $endln); // write body if (array_key_exists('text', $this->data) || array_key_exists('html', $this->data)) { // new body $this->write_body_content($stream, $this->boundary); } else { // existing body, just copy old content to the stream $api->backend->storage->get_raw_body($message->uid, $stream, 'TEXT'); } fwrite($stream, $endln); // write attachments, here we can only have new attachments foreach ((array) $this->attach_data['create'] as $attachment) { fwrite($stream, '--' . $this->boundary . $endln); $this->write_attachment($stream, $attachment); } fwrite($stream, $endln . '--' . $this->boundary . '--' . $endln); } // body or attachments changed, multipart/mixed message else { // get old TEXT of the message $body_stream = fopen('php://temp/maxmemory:10240000', 'r+'); $api->backend->storage->get_raw_body($message->uid, $body_stream, 'TEXT'); rewind($body_stream); $num = 0; $ignore = false; $regexp = '/^--' . preg_quote($this->boundary, '/') . '(--|)\r?\n$/'; // Go and replace bodies... while (($line = fgets($body_stream, 4096)) !== false) { // boundary line if ($line[0] === '-' && $line[1] === '-' && preg_match($regexp, $line, $m)) { // this is the end of the message, add new attachment(s) here if ($m[1] == '--') { foreach ((array) $this->attach_data['create'] as $attachment) { fwrite($stream, '--' . $this->boundary . $endln); $this->write_attachment($stream, $attachment); } fwrite($stream, $line); break; } $num++; $ignore = false; // delete this part foreach ((array) $this->attach_data['delete'] as $attachment) { if ($attachment->mime_id == $num) { $ignore = true; continue 2; } } // update this part foreach ((array) $this->attach_data['update'] as $attachment) { if ($attachment->mime_id == $num) { fwrite($stream, $line); $this->write_attachment($stream, $attachment); $ignore = true; continue 2; } } // find the first part (expected to be the text or alternative if ($num == 1 && $body_mod) { $ignore = true; $boundary = '_' . md5(rand() . microtime()); fwrite($stream, $line); $this->write_body_content($stream, $boundary, true); continue; } } else if ($ignore) { continue; } fwrite($stream, $line); } fclose($body_stream); } } /** * Write configured text/plain or multipart/alternative * part content into message stream */ protected function write_body_content($stream, $boundary, $with_headers = false) { $endln = $this->endln; // multipart/alternative if (strlen($this->body_html)) { if ($with_headers) { fwrite($stream, 'Content-Type: multipart/alternative;' . $endln . " boundary=\"$boundary\"" . $endln . $endln); } fwrite($stream, '--' . $boundary . $endln . 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/plain; charset=UTF-8' . $endln . $endln); fwrite($stream, $this->body_text); fwrite($stream, $endln . '--' . $boundary . $endln . 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/html; charset=UTF-8' . $endln . $endln); fwrite($stream, $this->body_html); fwrite($stream, $endln . '--' . $boundary . '--' . $endln); } // text/plain else if (strlen($this->body_text)) { if ($with_headers) { fwrite($stream, 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/plain; charset=UTF-8' . $endln . $endln); } // make sure all line endings are CRLF $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart); fwrite($stream, $this->body_text); } } /** * Get rcube_message object of the assigned message */ protected function get_message() { if ($this->message && !($this->message instanceof rcube_message)) { $this->message = new rcube_message($this->message->uid, $this->message->folder); if (empty($this->message->headers)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $this->message; } /** * Parse message source with headers */ protected static function parse_headers($headers) { // Parse headers $headers = str_replace("\r\n", "\n", $headers); $headers = explode("\n", trim($headers)); $ln = 0; $lines = array(); foreach ($headers as $line) { if (ord($line[0]) <= 32) { $lines[$ln] .= (empty($lines[$ln]) ? '' : "\r\n") . $line; } else { $lines[++$ln] = trim($line); } } // Unify char-case of header names $headers = array(); foreach ($lines as $line) { list($field, $string) = explode(':', $line, 2); if ($field = self::normalize_header_name($field)) { $headers[$field] = trim($string); } } return $headers; } /** * Normalize (fix) header names */ protected static function normalize_header_name($name) { $headers_map = array( 'subject' => 'Subject', 'from' => 'From', 'to' => 'To', 'cc' => 'Cc', 'bcc' => 'Bcc', 'date' => 'Date', 'reply-to' => 'Reply-To', 'in-reply-to' => 'In-Reply-To', 'x-priority' => 'X-Priority', 'message-id' => 'Message-ID', 'references' => 'References', 'content-type' => 'Content-Type', 'content-transfer-encoding' => 'Content-Transfer-Encoding', ); $name_lc = strtolower($name); return isset($headers_map[$name_lc]) ? $headers_map[$name_lc] : $name; } /** * Save the message into IMAP folder and delete the old one */ protected function save_message($stream, $folder) { $api = kolab_api::get_instance(); $message = $this->get_message(); // save the message $data = array($stream); $saved = $api->backend->storage->save_message($folder, $data); + @fclose($stream); + if (empty($saved)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Failed to save the message in storage' )); } // delete the old message if ($saved && $message && $message->uid) { $api->backend->storage->delete_message($message->uid, $folder); } return $saved; } /** * Return message body (in specified format, html or text) */ protected function body($html = true) { if ($message = $this->get_message()) { if ($html) { $html = $message->first_html_part($part, true); if ($html) { // charset was converted to UTF-8 in rcube_storage::get_message_part(), // change/add charset specification in HTML accordingly $meta = ''; // remove old meta tag and add the new one, making sure // that it is placed in the head $html = preg_replace('/]+charset=[a-z0-9-_]+[^>]*>/Ui', '', $html); $html = preg_replace('/(]*>)/Ui', '\\1' . $meta, $html, -1, $rcount); if (!$rcount) { $html = '' . $meta . '' . $html; } } return $html; } $plain = $message->first_text_part($part, true); if ($part === null && $message->body) { $plain = $message->body; } else if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') { $plain = rcube_mime::unfold_flowed($plain); } return $plain; } } /** * Encode header value */ protected function encode_header($name, $value) { $mime_part = new Mail_mimePart; return $mime_part->encodeHeader($name, $value, RCUBE_CHARSET, 'quoted-printable', $this->endln); } /** * Unique Message-ID generator. * * @return string Message-ID */ public function gen_message_id() { $api = kolab_api::get_instance(); $local_part = md5(uniqid('kolab'.mt_rand(), true)); $domain_part = $api->backend->user->get_username('domain'); // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924) if (!preg_match('/\.[a-z]+$/i', $domain_part)) { foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) { $host = preg_replace('/:[0-9]+$/', '', $host); if ($host && preg_match('/\.[a-z]+$/i', $host)) { $domain_part = $host; } } } return sprintf('<%s@%s>', $local_part, $domain_part); } /** * Write attachment body with headers into the output stream */ protected function write_attachment($stream, $attachment) { $api = kolab_api::get_instance(); $message = $this->get_message(); $temp_dir = $api->config->get('temp_dir'); // use Mail_mimePart functionality for simplicity $params = array( 'eol' => $this->endln, 'encoding' => $attachment->mimetype == 'message/rfc822' ? '8bit' : 'base64', 'content_type' => $attachment->mimetype, 'body_file' => $attachment->path, 'disposition' => 'attachment', 'filename' => $attachment->filename, 'name_encoding' => 'quoted-printable', 'headers_charset' => RCUBE_CHARSET, ); if ($attachment->content_id) { $params['cid'] = rtrim($attachment->content_id, '<>'); } if ($attachment->content_location) { $params['location'] = $attachment->content_location; } // Get attachment body if both 'path' and 'data' are NULL // this is the case when we modify only attachment metadata, e.g. filename if (empty($attachment->path) && $attachment->data === null && !empty($message) && !empty($attachment->mime_id) ) { // @TODO: do this with temp files $api->backend->storage->set_folder($message->folder); $body = $api->backend->storage->get_raw_body($message->uid, null, $attachment->mime_id . '.TEXT'); if ($attachment->encoding == 'base64') { $body = base64_decode($body); } else if ($attachment->encoding == 'quoted-printable') { $body = quoted_printable_decode($body); } $attachment->data = $body; } $mime = new Mail_mimePart($attachment->data, $params); $temp_file = tempnam($temp_dir, 'msgPart'); // @TODO: implement encodeToStream() $result = $mime->encodeToFile($temp_file); if ($result instanceof PEAR_Error) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, $result); } if ($fd = fopen($temp_file, 'r')) { stream_copy_to_stream($fd, $stream); fwrite($stream, $this->endln); fclose($fd); @unlink($temp_file); } else { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } + + /** + * Apply flag changes + */ + protected function update_flags($uid, $folder) + { + // IMAP flags change requested + if (!array_key_exists('flags', $this->data)) { + return; + } + + $api = kolab_api::get_instance(); + $old_flags = $this->flags; + $folder = $folder ?: $this->folder; + $old_folder = $this->folder ?: $folder; + + // set new flags + foreach ((array) $this->data['flags'] as $flag) { + if (($key = array_search($flag, $old_flags)) !== false) { + unset($old_flags[$key]); + } + else { + $flag = strtoupper($flag); + $api->backend->storage->set_flag($uid, $flag, $folder); + } + } + + // unset remaining old flags + foreach ($old_flags as $flag) { + $flag = 'UN' . strtoupper($flag); + $api->backend->storage->set_flag($uid, $flag, $old_folder); + } + } + + /** + * Apply categories changes + */ + protected function update_categories($uid, $folder, $message) + { + // IMAP flags change requested + if (!array_key_exists('categories', $this->data) && $uid == $this->uid) { + return; + } + + $config = kolab_storage_config::get_instance(); + $api = kolab_api::get_instance(); + $old_uid = $this->uid; + $folder = $folder ?: $this->folder; + $old_folder = $this->folder ?: $folder; + $categories = (array) $this->data['categories']; + $categories_lc = array_map('mb_strtolower', $categories); + + $delete_member = $config->build_member_url(array( + 'folder' => $old_folder, + 'uid' => $old_uid ?: $uid + )); + + $create_member = $config->build_member_url(array( + 'folder' => $folder, + 'uid' => $uid, + 'message-id' => $this->{'message-id'}, + 'date' => $this->date, + 'subject' => $this->subject, + )); + + foreach ($config->get_tags() as $tag) { + $tag_name = mb_strtolower($tag['name']); + $modified = false; + $is_new = ($i = array_search($tag_name, $categories_lc)) !== false; + + // remove a member of the relation + if (!$is_new || ($old_uid && $old_uid != $uid)) { + foreach ($tag['members'] as $idx => $member) { + if ($member == $delete_member || strpos($member, $delete_member . '?') === 0) { + unset($tag['members'][$idx]); + $modified = true; + } + } + } + + // add a member to the relation + if ($is_new) { + unset($categories[$i]); + + if ($modified || $old_uid != $uid) { + $tag['members'][] = $create_member; + $modified = true; + } + } + + // update tag/relation object + if ($modified) { + $config->save($tag, 'relation', $tag['uid']); + } + } + + // add non-existing categories + foreach ($categories as $cat) { + $tag = array( + 'name' => $cat, + 'category' => 'tag', + 'members' => array($create_member), + ); + + $config->save($tag, 'relation'); + } + } } diff --git a/lib/output/json.php b/lib/output/json.php index f4c2544..8a12d0f 100644 --- a/lib/output/json.php +++ b/lib/output/json.php @@ -1,271 +1,279 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json extends kolab_api_output { /** * Send successful response * * @param mixed Response data * @param string Data type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function send($data, $type, $context = null, $attrs_filter = array()) { // Set output type $this->headers(array('Content-Type' => "application/json; charset=utf-8")); list($type, $mode) = explode('-', $type); if ($mode != 'list') { $data = array($data); } $class = "kolab_api_output_json_$type"; $model = new $class($this); $result = array(); $debug = $this->api->config->get('kolab_api_debug'); foreach ($data as $idx => $item) { if ($element = $model->element($item, $attrs_filter)) { $result[] = $element; } else { unset($data[$idx]); } } // apply output filter if ($this->api->filter) { $this->api->filter->output($result, $type, $context, $attrs_filter); } // generate JSON output $opts = $debug && defined('JSON_PRETTY_PRINT') ? JSON_PRETTY_PRINT : 0; $result = json_encode($result, $opts); if ($mode != 'list') { $result = trim($result, '[]'); } if ($debug) { rcube::console($result); } $this->send_status(kolab_api_output::STATUS_OK, false); // send JSON output echo $result; exit; } /** * Convert object data into JSON API format * * @param array Object data * @param string Object type * * @return array Object data in JSON API format */ public function convert($data, $type) { $class = "kolab_api_output_json_$type"; $model = new $class($this); return $model->element($data); } /** * Convert (part of) kolab_format object into an array * * @param array Kolab object * @param string Object type * @param string Data element name * @param array Optional list of return properties * * @return array Object data */ public function object_to_array($object, $type, $element, $properties = array(), $array_elements = array()) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) { $format = $object['_formatobj']; } // create new kolab_format instance if (!$format) { $format = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) { return; } $format->set($object); } $xml = $format->write(kolab_storage::$version); if (empty($xml) || !$format->is_valid() || !$format->uid) { return; } // The simplest way of "normalizing object properties // is to use its XML representation $doc = new DOMDocument(); // LIBXML_NOBLANKS is required for xml_to_array() below $doc->loadXML($xml, LIBXML_NOBLANKS); $node = $doc->getElementsByTagName($element)->item(0); $node = $this->xml_to_array($node); $node = array_filter($node); unset($node['prodid']); // faked 'categories' property (we need this for unit-tests // @TODO: find a better way - if (!empty($object['categories'])) { - $node['categories'] = $object['categories']; + if (array_key_exists('categories', $object)) { + if ($node['properties']) { + $node['properties']['categories'] = $object['categories']; + } + else { + $node['categories'] = $object['categories']; + } } if (!empty($properties)) { $node = array_intersect_key($node, array_combine($properties, $properties)); } // force some elements to be arrays if (!empty($array_elements)) { self::parse_array_result($node, $array_elements); } return $node; } /** * Convert XML element into an array * This is intended to use with Kolab XML format * * @param DOMElement XML element * * @return mixed Conversion result */ public function xml_to_array($node) { $children = $node->childNodes; if (!$children->length) { return; } if ($children->length == 1) { if ($node->firstChild->nodeType == XML_TEXT_NODE || !$node->firstChild->childNodes->length ) { return (string) $node->textContent; } if ($node->firstChild->nodeType == XML_ELEMENT_NODE && $node->firstChild->childNodes->length == 1 && $node->firstChild->firstChild->nodeType == XML_TEXT_NODE ) { switch ($node->firstChild->nodeName) { case 'integer': return (int) $node->textContent; case 'boolean': return strtoupper($node->textContent) == 'TRUE'; case 'date-time': case 'timestamp': case 'date': case 'text': case 'uri': case 'sex': return (string) $node->textContent; } } } $result = array(); foreach ($children as $child) { $value = $child->nodeType == XML_TEXT_NODE ? $child->nodeValue : $this->xml_to_array($child); if (!isset($result[$child->nodeName])) { $result[$child->nodeName] = $value; } else { if (!is_array($result[$child->nodeName]) || !isset($result[$child->nodeName][0])) { $result[$child->nodeName] = array($result[$child->nodeName]); } $result[$child->nodeName][] = $value; } } if (is_array($result['text']) && count($result) == 1) { $result = $result['text']; } return $result; } public static function parse_array_result(&$data, $array_elements = array()) { foreach ($array_elements as $key) { $items = explode('/', $key); if (count($items) > 1 && !empty($data[$items[0]])) { $key = array_shift($items); self::parse_array_result($data[$key], array(implode('/', $items))); } else if (!empty($data[$key]) && (!is_array($data[$key]) || !array_key_exists(0, $data[$key]))) { $data[$key] = array($data[$key]); } + else if (empty($data[$key])) { + unset($data[$key]); + } } } /** * Makes sure exdate/rdate output is consistent/unified */ public static function parse_recurrence(&$data) { foreach (array('exdate', 'rdate') as $key) { if ($data[$key]) { if (is_string($data[$key])) { $idx = strlen($data[$key]) > 10 ? 'date-time' : 'date'; $data[$key] = array($idx => array($data[$key])); } else if (array_key_exists('date', $data[$key]) && !is_array($data[$key]['date'])) { $data[$key]['date'] = (array) $data[$key]['date']; } else if (array_key_exists('date-time', $data[$key]) && !is_array($data[$key]['date-time'])) { $data[$key]['date-time'] = (array) $data[$key]['date-time']; } } } } } diff --git a/lib/output/xml.php b/lib/output/xml.php index 2c5bff3..16197ac 100644 --- a/lib/output/xml.php +++ b/lib/output/xml.php @@ -1,148 +1,148 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml extends kolab_api_output { public static $prod_id; public static $version; public static $x_version; /** * Object constructor * * @param kolab_api The API */ public function __construct($api) { $this->api = $api; self::$prod_id = 'Kolab REST API ' . kolab_api::VERSION; self::$version = '2.0'; self::$x_version = '3.1.0'; } /** * Send successful response * * @param mixed Response data * @param string Data type * @param array Context (folder_uid, object_uid, object) * @param array Optional attributes filter */ public function send($data, $type, $context = null, $attrs_filter = array()) { // Set output type // @TODO: object specific content-type, e.g. application/calendar+xml? $this->headers(array('Content-Type' => "application/xml; charset=utf-8")); list($type, $mode) = explode('-', $type); if ($mode != 'list') { $data = array($data); } $class = "kolab_api_output_xml_$type"; $model = new $class($this); $debug = $this->api->config->get('kolab_api_debug'); // create XML document $xml = new DOMDocument('1.0', RCUBE_CHARSET); $xml->xmlStandalone = false; $xml->formatOutput = $debug; $dom = $model->structure($xml); foreach ($data as $item) { $model->append_element($item, $dom, $attrs_filter); } // generate XML output $result = $xml->saveXML(); $xml = null; if ($debug) { rcube::console(rtrim($result)); } $this->send_status(kolab_api_output::STATUS_OK, false); // send XML output echo $result; exit; } /** * Helper to convert kolab_format object into DOMDocument * * @param kolab_format Object * * @return DOMDocument */ public function object_to_dom($object, $type = null) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) { $format = $object['_formatobj']; } // create new kolab_format instance if (!$format) { $format = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) { return; } $format->set($object); } $xml = $format->write(kolab_storage::$version); if (empty($xml) || !$format->is_valid() || !$format->uid) { return; } // parse XML $doc = new DOMDocument(); $doc->loadXML($xml); return $doc; } /** * Attributes filter - removes attributes not listed in $attrs_filter * * @param DOMElement XML element * @param array Attributes list */ public static function attrs_filter(&$node, $attrs_filter) { if (empty($attrs_filter)) { return; } - + // @TODO } } diff --git a/tests/API/Contacts.php b/tests/API/Contacts.php index 97b3c55..25f12d7 100644 --- a/tests/API/Contacts.php +++ b/tests/API/Contacts.php @@ -1,188 +1,190 @@ get('folders/' . kolab_api_tests::folder_uid('Contacts') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('a-b-c-d', $body[0]['uid']); $this->assertSame('displname', $body[0]['fn']); } /** * Test contact existence */ function test_contact_exists() { self::$api->head('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing contact self::$api->head('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test contact info */ function test_contact_info() { self::$api->get('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('a-b-c-d', $body['uid']); $this->assertSame('displname', $body['fn']); $this->assertSame(array('tag1'), $body['categories']); } /** * Test contact create */ function test_contact_create() { $post = json_encode(array( 'n' => array( 'surname' => 'lastname', ), 'note' => 'Test description', )); self::$api->post('contacts/' . kolab_api_tests::folder_uid('Contacts'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'n' => array( 'surname' => 'lastname', ), 'note' => 'Test description', )); self::$api->post('contacts/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('contacts/' . kolab_api_tests::folder_uid('Contacts'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test contact update */ function test_contact_update() { $post = json_encode(array( - 'note' => 'note1', + 'note' => 'note1', + 'categories' => array('test'), )); self::$api->put('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('a-b-c-d', $body['uid']); self::$api->get('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('note1', $body['note']); + $this->assertSame(array('test'), $body['categories']); } /** * Test contact delete */ function test_contact_delete() { // delete existing contact self::$api->delete('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/a-b-c-d'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing contact self::$api->delete('contacts/' . kolab_api_tests::folder_uid('Contacts') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test counting task attachments */ function test_count_attachments() { $this->markTestIncomplete('TODO'); } /** * Test listing task attachments */ function test_list_attachments() { $this->markTestIncomplete('TODO'); } } diff --git a/tests/API/Events.php b/tests/API/Events.php index fbae553..1ce158f 100644 --- a/tests/API/Events.php +++ b/tests/API/Events.php @@ -1,217 +1,220 @@ get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('100-100-100-100', $body[0]['uid']); $this->assertSame('Summary', $body[0]['summary']); $this->assertSame('101-101-101-101', $body[1]['uid']); $this->assertSame('PUBLIC', $body[1]['class']); } /** * Test event existence */ function test_event_exists() { self::$api->head('events/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing event self::$api->head('events/' . kolab_api_tests::folder_uid('Calendar') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test event info */ function test_event_info() { self::$api->get('events/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('100-100-100-100', $body['uid']); $this->assertSame('Summary', $body['summary']); + $this->assertSame(array('tag1', 'Work'), $body['categories']); } /** * Test event create */ function test_event_create() { $post = json_encode(array( 'summary' => 'Test description', 'dtstart' => '2015-01-01', )); self::$api->post('events/' . kolab_api_tests::folder_uid('Calendar'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'summary' => 'Test description', 'dtstart' => '2015-01-01', )); self::$api->post('events/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('events/' . kolab_api_tests::folder_uid('Calendar'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test event update */ function test_event_update() { // @TODO: test modification of all supported properties $post = json_encode(array( 'summary' => 'Modified summary (1)', 'dtstart' => '2015-01-01', + 'categories' => array('test'), )); self::$api->put('events/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('100-100-100-100', $body['uid']); self::$api->get('events/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary (1)', $body['summary']); + $this->assertSame(array('test'), $body['categories']); } /** * Test counting event attachments */ function test_count_attachments() { self::$api->head('events/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); self::$api->head('events/' . kolab_api_tests::folder_uid('Calendar') . '/101-101-101-101/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); } /** * Test listing event attachments */ function test_list_attachments() { self::$api->get('events/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame('3', $body[0]['id']); $this->assertSame('image/jpeg', $body[0]['mimetype']); $this->assertSame('photo-mini.jpg', $body[0]['filename']); $this->assertSame('attachment', $body[0]['disposition']); $this->assertSame(793, $body[0]['size']); } /** * Test event delete */ function test_event_delete() { // delete existing event self::$api->delete('events/' . kolab_api_tests::folder_uid('Calendar') . '/101-101-101-101'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing event self::$api->delete('events/' . kolab_api_tests::folder_uid('Calendar') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/API/Mails.php b/tests/API/Mails.php index a5b0979..8d45740 100644 --- a/tests/API/Mails.php +++ b/tests/API/Mails.php @@ -1,331 +1,336 @@ get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(5, count($body)); $this->assertSame(kolab_api_tests::msg_uid('1'), $body[0]['uid']); $this->assertSame('"test" wurde aktualisiert', $body[0]['subject']); $this->assertSame(kolab_api_tests::msg_uid('2'), $body[1]['uid']); $this->assertSame('Re: dsda', $body[1]['subject']); } /** * Test mail existence check */ function test_mail_exists() { self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing mail self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test mail info */ function test_mail_info() { self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::msg_uid('1'), $body['uid']); $this->assertSame('"test" wurde aktualisiert', $body['subject']); $this->assertSame(624, $body['size']); + $this->assertSame(array('tag1'), $body['categories']); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::msg_uid('6'), $body['uid']); } /** * Test counting mail attachments */ function test_count_attachments() { self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('2') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(2, (int) $count); } /** * Test listing mail attachments */ function test_list_attachments() { self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('2') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body); $this->assertSame('2', $body[0]['id']); $this->assertSame('text/plain', $body[0]['mimetype']); $this->assertSame('test.txt', $body[0]['filename']); $this->assertSame('attachment', $body[0]['disposition']); $this->assertSame(4, $body[0]['size']); } /** * Test mail create */ function test_mail_create() { $post = json_encode(array( - 'subject' => 'Test summary', - 'text' => 'This is the body.', + 'subject' => 'Test summary', + 'text' => 'This is the body.', + 'categories' => array('tag1'), )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); self::$created = $body['uid']; self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . self::$created); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('Test summary', $body['subject']); $this->assertSame('This is the body.', $body['text']); + $this->assertSame(array('tag1'), $body['categories']); // folder does not exists $post = json_encode(array( 'subject' => 'Test summary 2', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); // test HTML message creation $post = json_encode(array( 'subject' => 'HTML', 'html' => 'now it iÅ› HTML', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(!empty($body['uid'])); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('HTML', $body['subject']); $this->assertRegexp('|now it iÅ› HTML|', (string) $body['html']); } /** * Test mail update */ function test_mail_update() { $post = json_encode(array( 'subject' => 'Modified summary', 'html' => 'now it is HTML', + 'categories' => array('test'), )); self::$api->put('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . self::$created, array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(!empty($body['uid'])); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('Modified summary', $body['subject']); $this->assertRegexp('|now it is HTML|', (string) $body['html']); $this->assertSame('now it is HTML', trim($body['text'])); + $this->assertSame(array('test'), $body['categories']); // test replacing message body in multipart/mixed message $post = json_encode(array( 'html' => 'now it is HTML', 'priority' => 5, )); self::$api->put('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertRegexp('|now it is HTML|', (string) $body['html']); $this->assertSame('now it is HTML', trim($body['text'])); $this->assertSame(5, $body['priority']); } /** * Test mail submit */ function test_mail_submit() { // send the message to self $post = json_encode(array( 'subject' => 'Test summary', 'text' => 'This is the body.', 'from' => array( 'name' => "Test' user", 'address' => self::$api->username, ), 'to' => array( array( 'name' => "Test' user", 'address' => self::$api->username, ), ), )); self::$api->post('mails/submit', array(), $post); $code = self::$api->response_code(); $this->assertEquals(204, $code); // @TODO: test submitting an existing message } /** * Test mail delete */ function test_mail_delete() { // delete existing mail self::$api->delete('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing mail self::$api->delete('mails/' . kolab_api_tests::folder_uid('INBOX') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/API/Notes.php b/tests/API/Notes.php index e3be857..c5b723d 100644 --- a/tests/API/Notes.php +++ b/tests/API/Notes.php @@ -1,213 +1,214 @@ get('folders/' . kolab_api_tests::folder_uid('Notes') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(2, count($body)); $this->assertSame('1-1-1-1', $body[0]['uid']); $this->assertSame('test', $body[0]['summary']); $this->assertSame('2-2-2-2', $body[1]['uid']); $this->assertSame('wwww', $body[1]['summary']); } /** * Test note existence */ function test_note_exists() { self::$api->head('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing note self::$api->head('notes/' . kolab_api_tests::folder_uid('Notes') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test note info */ function test_note_info() { self::$api->get('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('1-1-1-1', $body['uid']); $this->assertSame('test', $body['summary']); + $this->assertSame(array('tag1'), $body['categories']); } /** * Test note create */ function test_note_create() { $post = json_encode(array( 'summary' => 'Test summary', 'description' => 'Test description' )); self::$api->post('notes/' . kolab_api_tests::folder_uid('Notes'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'summary' => 'Test summary 2', )); self::$api->post('notes/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('notes/' . kolab_api_tests::folder_uid('Notes'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test note update */ function test_note_update() { $post = json_encode(array( 'summary' => 'Modified summary', 'description' => 'Modified description', 'classification' => 'PRIVATE', 'categories' => array('test'), 'unknown' => 'test' )); self::$api->put('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('1-1-1-1', $body['uid']); self::$api->get('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary', $body['summary']); $this->assertSame('Modified description', $body['description']); $this->assertSame('PRIVATE', $body['classification']); $this->assertSame(array('test'), $body['categories']); $this->assertSame(null, $body['unknown']); // test unsetting some data $post = json_encode(array( 'description' => null, 'categories' => null, )); self::$api->put('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); self::$api->get('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame(null, $body['description']); $this->assertSame(null, $body['categories']); } /** * Test counting task attachments */ function test_count_attachments() { // @TODO } /** * Test listing task attachments */ function test_list_attachments() { // @TODO } /** * Test note delete */ function test_note_delete() { // delete existing note self::$api->delete('notes/' . kolab_api_tests::folder_uid('Notes') . '/1-1-1-1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing note self::$api->delete('notes/' . kolab_api_tests::folder_uid('Notes') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/API/Tasks.php b/tests/API/Tasks.php index 11e0bb7..20d32d0 100644 --- a/tests/API/Tasks.php +++ b/tests/API/Tasks.php @@ -1,235 +1,238 @@ get('folders/' . kolab_api_tests::folder_uid('Tasks') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(2, count($body)); $this->assertSame('10-10-10-10', $body[0]['uid']); $this->assertSame('task title', $body[0]['summary']); $this->assertSame('20-20-20-20', $body[1]['uid']); $this->assertSame('task', $body[1]['summary']); } /** * Test task existence */ function test_task_exists() { self::$api->head('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing task self::$api->head('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test task info */ function test_task_info() { self::$api->get('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('10-10-10-10', $body['uid']); $this->assertSame('task title', $body['summary']); + $this->assertSame(array('tag1'), $body['categories']); } /** * Test task create */ function test_task_create() { $post = json_encode(array( 'summary' => 'Test summary', 'description' => 'Test description' )); self::$api->post('tasks/' . kolab_api_tests::folder_uid('Tasks'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); // folder does not exists $post = json_encode(array( 'summary' => 'Test summary 2', )); self::$api->post('tasks/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('tasks/' . kolab_api_tests::folder_uid('Tasks'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test task update */ function test_task_update() { $post = json_encode(array( 'summary' => 'modified summary', 'description' => 'modified description', 'class' => 'PRIVATE', 'dtstart' => '2014-01-10', + 'categories' => array('test'), )); self::$api->put('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('10-10-10-10', $body['uid']); self::$api->get('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('modified summary', $body['summary']); $this->assertSame('modified description', $body['description']); $this->assertSame('PRIVATE', $body['class']); + $this->assertSame(array('test'), $body['categories']); // test unsetting some properties $post = json_encode(array( 'due' => null, 'related-to' => null, 'location' => null, 'unknown' => 'test', )); self::$api->put('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); self::$api->get('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10'); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('2014-01-10', $body['dtstart']); $this->assertSame(null, $body['due']); $this->assertSame(null, $body['related-to']); $this->assertSame(null, $body['location']); $this->assertSame(null, $body['unknown']); } /** * Test task delete */ function test_task_delete() { // delete existing task self::$api->delete('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/20-20-20-20'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing task self::$api->delete('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test counting task attachments */ function test_count_attachments() { self::$api->head('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); } /** * Test listing task attachments */ function test_list_attachments() { self::$api->get('tasks/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertSame('3', $body[0]['id']); $this->assertSame('text/plain', $body[0]['mimetype']); $this->assertSame('test.txt', $body[0]['filename']); $this->assertSame('attachment', $body[0]['disposition']); $this->assertSame(4, $body[0]['size']); } } diff --git a/tests/Unit/Filter/Mapistore/Contact.php b/tests/Unit/Filter/Mapistore/Contact.php index 5ee5328..1cbaede 100644 --- a/tests/Unit/Filter/Mapistore/Contact.php +++ b/tests/Unit/Filter/Mapistore/Contact.php @@ -1,433 +1,434 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts', false), $result['parent_id']); // $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), $result['PidTagLastModificationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150330', false), $result['PidTagBirthday']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('20150301', false), $result['PidTagWeddingAnniversary']); $this->assertSame(null, $body['PidTagHasAttachments']); $this->assertSame('displname', $result['PidTagDisplayName']); $this->assertSame('last', $result['PidTagSurname']); $this->assertSame('test', $result['PidTagGivenName']); $this->assertSame('middlename', $result['PidTagMiddleName']); $this->assertSame('prefx', $result['PidTagDisplayNamePrefix']); $this->assertSame('suff', $result['PidTagGeneration']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['PidTagBody']); $this->assertSame('free-busy url', $result['PidLidFreeBusyLocation']); $this->assertSame('title', $result['PidTagTitle']); $this->assertSame('Org', $result['PidTagCompanyName']); $this->assertSame('dept', $result['PidTagDepartmentName']); $this->assertSame('profeion', $result['PidTagProfession']); $this->assertSame('manager name', $result['PidTagManagerName']); $this->assertSame('assist', $result['PidTagAssistant']); $this->assertSame('website', $result['PidTagPersonalHomePage']); $this->assertSame('office street', $result['PidTagOtherAddressStreet']); $this->assertSame('office city', $result['PidTagOtherAddressCity']); $this->assertSame('office state', $result['PidTagOtherAddressStateOrProvince']); $this->assertSame('office zip', $result['PidTagOtherAddressPostalCode']); $this->assertSame('office country', $result['PidTagOtherAddressCountry']); // $this->assertSame('office pobox', $result['PidTagOtherAddressPostOfficeBox']); $this->assertSame('home street', $result['PidTagHomeAddressStreet']); $this->assertSame('home city', $result['PidTagHomeAddressCity']); $this->assertSame('home state', $result['PidTagHomeAddressStateOrProvince']); $this->assertSame('home zip', $result['PidTagHomeAddressPostalCode']); $this->assertSame('home country', $result['PidTagHomeAddressCountry']); // $this->assertSame('home pobox', $result['PidTagHomeAddressPostOfficeBox']); $this->assertSame('work street', $result['PidLidWorkAddressStreet']); $this->assertSame('work city', $result['PidLidWorkAddressCity']); $this->assertSame('work state', $result['PidLidWorkAddressState']); $this->assertSame('work zip', $result['PidLidWorkAddressPostalCode']); $this->assertSame('work country', $result['PidLidWorkAddressCountry']); // $this->assertSame('work pobox', $result['PidLidWorkAddressPostOfficeBox']); $this->assertSame(2, $result['PidLidPostalAddressId']); $this->assertSame('nick', $result['PidTagNickname']); $this->assertSame(2, $result['PidTagGender']); $this->assertSame('spouse', $result['PidTagSpouseName']); $this->assertSame(array('children', 'children2'), $result['PidTagChildrensNames']); $this->assertSame('home phone', $result['PidTagHomeTelephoneNumber']); $this->assertSame('work phone', $result['PidTagBusinessTelephoneNumber']); $this->assertSame('home fax', $result['PidTagHomeFaxNumber']); $this->assertSame('work fax', $result['PidTagBusinessFaxNumber']); $this->assertSame('mobile', $result['PidTagMobileTelephoneNumber']); $this->assertSame('pager', $result['PidTagPagerTelephoneNumber']); $this->assertSame('car phone', $result['PidTagCarTelephoneNumber']); $this->assertSame('other phone', $result['PidTagOtherTelephoneNumber']); $this->assertSame('im gg', $result['PidLidInstantMessagingAddress']); $this->assertSame('test@mail.ru', $result['PidLidEmail1EmailAddress']); $this->assertSame('work@email.pl', $result['PidLidEmail2EmailAddress']); $this->assertSame('other@email.pl', $result['PidLidEmail3EmailAddress']); $this->assertRegExp('/^cy9.*/', $result['PidTagUserX509Certificate']); $this->assertSame(true, $result['PidLidHasPicture']); + $this->assertSame(array('tag1'), $result['PidNameKeywords']); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); // $this->assertSame('individual', $result['kind']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_contact; $data = array( 'id' => kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), 'parent_id' => kolab_api_tests::folder_uid('Contacts', false), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), 'PidTagBirthday' => kolab_api_filter_mapistore_common::date_php2mapi('20150330', true), 'PidTagWeddingAnniversary' => kolab_api_filter_mapistore_common::date_php2mapi('20150301', true), 'PidTagDisplayName' => 'displname', 'PidTagSurname' => 'last', 'PidTagGivenName' => 'test', 'PidTagMiddleName' => 'middlename', 'PidTagDisplayNamePrefix' => 'prefx', 'PidTagGeneration' => 'suff', 'PidTagBody' => 'dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', 'PidLidFreeBusyLocation' => 'free-busy url', 'PidTagTitle' => 'title', 'PidTagCompanyName' => 'Org', 'PidTagDepartmentName' => 'dept', 'PidTagProfession' => 'profeion', 'PidTagManagerName' => 'manager name', 'PidTagAssistant' => 'assist', 'PidTagPersonalHomePage' => 'website', 'PidTagOtherAddressStreet' => 'office street', 'PidTagOtherAddressCity' => 'office city', 'PidTagOtherAddressStateOrProvince' => 'office state', 'PidTagOtherAddressPostalCode' => 'office zip', 'PidTagOtherAddressCountry' => 'office country', // 'PidTagOtherAddressPostOfficeBox' => 'office pobox', 'PidTagHomeAddressStreet' => 'home street', 'PidTagHomeAddressCity' => 'home city', 'PidTagHomeAddressStateOrProvince' => 'home state', 'PidTagHomeAddressPostalCode' => 'home zip', 'PidTagHomeAddressCountry' => 'home country', // 'PidTagHomeAddressPostOfficeBox' => 'home pobox', 'PidLidWorkAddressStreet' => 'work street', 'PidLidWorkAddressCity' => 'work city', 'PidLidWorkAddressState' => 'work state', 'PidLidWorkAddressPostalCode' => 'work zip', 'PidLidWorkAddressCountry' => 'work country', // 'PidLidWorkAddressPostOfficeBox' => 'work pobox', 'PidLidPostalAddressId' => 1, 'PidTagNickname' => 'nick', 'PidTagGender' => 2, 'PidTagSpouseName' => 'spouse', 'PidTagChildrensNames' => array('children', 'children2'), 'PidTagHomeTelephoneNumber' => 'home phone', 'PidTagBusinessTelephoneNumber' => 'work phone', 'PidTagHomeFaxNumber' => 'home fax', 'PidTagBusinessFaxNumber' => 'work fax', 'PidTagMobileTelephoneNumber' => 'mobile', 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagCarTelephoneNumber' => 'car phone', 'PidTagOtherTelephoneNumber' => 'other phone', 'PidLidInstantMessagingAddress' => 'im gg', 'PidLidEmail1EmailAddress' => 'test@mail.ru', 'PidLidEmail2EmailAddress' => 'work@email.pl', 'PidLidEmail3EmailAddress' => 'other@email.pl', 'PidTagUserX509Certificate' => '1234567890', 'PidTagInitials' => 'initials', ); $result = $api->input($data); // $this->assertSame('a-b-c-d', $result['uid']); // $this->assertSame('20150420T141533Z', $result['rev']); // $this->assertSame('individual', $result['kind']); $this->assertSame('displname', $result['fn']); $this->assertSame('last', $result['n']['surname']); $this->assertSame('test', $result['n']['given']); $this->assertSame('middlename', $result['n']['additional']); $this->assertSame('prefx', $result['n']['prefix']); $this->assertSame('suff', $result['n']['suffix']); $this->assertSame('dsfsdfsdfsdf sdfsdfsdf sdfsdfsfd', $result['note']); $this->assertSame('free-busy url', $result['fburl']); $this->assertSame('title', $result['title']); $this->assertSame('Org', $result['group']['org'][0]); $this->assertSame('dept', $result['group']['org'][1]); $this->assertSame('profeion', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist', $result['group']['related'][1]['text']); // $this->assertSame('', $result['group']['adr']['pobox']); $this->assertSame('office street', $result['group']['adr']['street']); $this->assertSame('office city', $result['group']['adr']['locality']); $this->assertSame('office state', $result['group']['adr']['region']); $this->assertSame('office zip', $result['group']['adr']['code']); $this->assertSame('office country', $result['group']['adr']['country']); $this->assertSame(array('website'), $result['url']); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame(1, $result['adr'][0]['parameters']['pref']); $this->assertSame('home street', $result['adr'][0]['street']); $this->assertSame('home city', $result['adr'][0]['locality']); $this->assertSame('home state', $result['adr'][0]['region']); $this->assertSame('home zip', $result['adr'][0]['code']); $this->assertSame('home country', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame('work street', $result['adr'][1]['street']); $this->assertSame('work city', $result['adr'][1]['locality']); $this->assertSame('work state', $result['adr'][1]['region']); $this->assertSame('work zip', $result['adr'][1]['code']); $this->assertSame('work country', $result['adr'][1]['country']); $this->assertSame('nick', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children2', $result['related'][2]['text']); $this->assertSame('2015-03-30', $result['bday']); // ? $this->assertSame('2015-03-01', $result['anniversary']); // ? $this->assertSame('M', $result['gender']); $this->assertSame(array('im gg'), $result['impp']); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame('other', $result['email'][2]['parameters']['type']); $this->assertSame('other@email.pl', $result['email'][2]['text']); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][1]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('MAPI:PidTagInitials', $result['x-custom'][0]['identifier']); $this->assertSame('initials', $result['x-custom'][0]['value']); $phones = array( 'home' => 'home phone', 'work' => 'work phone', 'faxhome' => 'home fax', 'faxwork' => 'work fax', 'cell' => 'mobile', 'pager' => 'pager', 'x-car' => 'car phone', 'textphone' => 'other phone', ); foreach ($result['tel'] as $tel) { $type = implode('', (array)$tel['parameters']['type']); $text = $tel['text']; if (!empty($phones[$type]) && $phones[$type] == $text) { unset($phones[$type]); } } $this->assertCount(8, $result['tel']); $this->assertCount(0, $phones); self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_contact; $data = array( 'id' => kolab_api_tests::mapi_uid('Contacts', false, 'a-b-c-d'), 'parent_id' => kolab_api_tests::folder_uid('Contacts', false), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('20150421T145607Z'), 'PidTagBirthday' => kolab_api_filter_mapistore_common::date_php2mapi('20150430', true), 'PidTagWeddingAnniversary' => kolab_api_filter_mapistore_common::date_php2mapi('20150401', true), 'PidTagDisplayName' => 'displname1', 'PidTagSurname' => 'last1', 'PidTagGivenName' => 'test1', 'PidTagMiddleName' => 'middlename1', 'PidTagDisplayNamePrefix' => 'prefx1', 'PidTagGeneration' => 'suff1', 'PidTagBody' => 'body1', 'PidLidFreeBusyLocation' => 'free-busy url1', 'PidTagTitle' => 'title1', 'PidTagCompanyName' => 'Org1', 'PidTagDepartmentName' => 'dept1', 'PidTagProfession' => 'profeion1', 'PidTagManagerName' => 'manager name1', 'PidTagAssistant' => 'assist1', 'PidTagPersonalHomePage' => 'website1', 'PidTagOtherAddressStreet' => 'office street1', 'PidTagOtherAddressCity' => 'office city1', 'PidTagOtherAddressStateOrProvince' => 'office state1', 'PidTagOtherAddressPostalCode' => 'office zip1', 'PidTagOtherAddressCountry' => 'office country1', 'PidTagHomeAddressStreet' => 'home street1', 'PidTagHomeAddressCity' => 'home city1', 'PidTagHomeAddressStateOrProvince' => 'home state1', 'PidTagHomeAddressPostalCode' => 'home zip1', 'PidTagHomeAddressCountry' => 'home country1', 'PidLidWorkAddressStreet' => 'work street1', 'PidLidWorkAddressCity' => 'work city1', 'PidLidWorkAddressState' => 'work state1', 'PidLidWorkAddressPostalCode' => 'work zip1', 'PidLidWorkAddressCountry' => 'work country1', 'PidTagNickname' => 'nick1', 'PidTagGender' => 1, 'PidTagSpouseName' => 'spouse1', 'PidTagChildrensNames' => array('children10', 'children20'), 'PidTagHomeTelephoneNumber' => 'home phone1', 'PidTagBusinessTelephoneNumber' => null, 'PidTagHomeFaxNumber' => 'home fax1', 'PidTagBusinessFaxNumber' => 'work fax1', 'PidTagMobileTelephoneNumber' => 'mobile1', 'PidTagPagerTelephoneNumber' => 'pager1', 'PidTagOtherTelephoneNumber' => 'other phone1', 'PidLidInstantMessagingAddress' => 'im gg1', 'PidLidEmail1EmailAddress' => 'test@mail.ru', 'PidLidEmail2EmailAddress' => 'work@email.pl', 'PidTagUserX509Certificate' => '12345678901', 'PidTagInitials' => 'initials1', 'PidNameKeywords' => array('work1'), ); $result = $api->input($data, self::$original); // $this->assertSame('a-b-c-d', $result['uid']); // $this->assertSame('20150420T141533Z', $result['rev']); // $this->assertSame('individual', $result['kind']); $this->assertSame('displname1', $result['fn']); $this->assertSame('last1', $result['n']['surname']); $this->assertSame('test1', $result['n']['given']); $this->assertSame('middlename1', $result['n']['additional']); $this->assertSame('prefx1', $result['n']['prefix']); $this->assertSame('suff1', $result['n']['suffix']); $this->assertSame('body1', $result['note']); $this->assertSame('free-busy url1', $result['fburl']); $this->assertSame('title1', $result['title']); $this->assertSame('Org1', $result['group']['org'][0]); $this->assertSame('dept1', $result['group']['org'][1]); $this->assertSame('profeion1', $result['group']['role']); $this->assertSame('x-manager', $result['group']['related'][0]['parameters']['type']); $this->assertSame('manager name1', $result['group']['related'][0]['text']); $this->assertSame('x-assistant', $result['group']['related'][1]['parameters']['type']); $this->assertSame('assist1', $result['group']['related'][1]['text']); $this->assertSame('office street1', $result['group']['adr']['street']); $this->assertSame('office city1', $result['group']['adr']['locality']); $this->assertSame('office state1', $result['group']['adr']['region']); $this->assertSame('office zip1', $result['group']['adr']['code']); $this->assertSame('office country1', $result['group']['adr']['country']); $this->assertSame(array('website1'), $result['url']); $this->assertSame('home', $result['adr'][0]['parameters']['type']); $this->assertSame('home street1', $result['adr'][0]['street']); $this->assertSame('home city1', $result['adr'][0]['locality']); $this->assertSame('home state1', $result['adr'][0]['region']); $this->assertSame('home zip1', $result['adr'][0]['code']); $this->assertSame('home country1', $result['adr'][0]['country']); $this->assertSame('work', $result['adr'][1]['parameters']['type']); $this->assertSame('work street1', $result['adr'][1]['street']); $this->assertSame('work city1', $result['adr'][1]['locality']); $this->assertSame('work state1', $result['adr'][1]['region']); $this->assertSame('work zip1', $result['adr'][1]['code']); $this->assertSame('work country1', $result['adr'][1]['country']); $this->assertSame('nick1', $result['nickname']); $this->assertSame('spouse', $result['related'][0]['parameters']['type']); $this->assertSame('spouse1', $result['related'][0]['text']); $this->assertSame('child', $result['related'][1]['parameters']['type']); $this->assertSame('children10', $result['related'][1]['text']); $this->assertSame('child', $result['related'][2]['parameters']['type']); $this->assertSame('children20', $result['related'][2]['text']); $this->assertSame('2015-04-30', $result['bday']); // ? $this->assertSame('2015-04-01', $result['anniversary']); // ? $this->assertSame('F', $result['gender']); $this->assertSame(array('im gg1'), $result['impp']); $this->assertSame('home', $result['email'][0]['parameters']['type']); $this->assertSame('test@mail.ru', $result['email'][0]['text']); $this->assertSame('work', $result['email'][1]['parameters']['type']); $this->assertSame('work@email.pl', $result['email'][1]['text']); $this->assertSame(null, $result['email'][2]); $this->assertRegExp('|^data:application/pkcs7-mime;base64,|', $result['key'][0]); // $this->assertRegExp('|^data:application/pgp-keys;base64,|', $result['key'][1]); // $this->assertRegExp('|^data:image/jpeg;base64,|', $result['photo']); $this->assertSame('MAPI:PidTagInitials', $result['x-custom'][0]['identifier']); $this->assertSame('initials1', $result['x-custom'][0]['value']); $this->assertSame(array('work1'), $result['categories']); $phones = array( 'home' => 'home phone1', 'faxhome' => 'home fax1', 'faxwork' => 'work fax1', 'cell' => 'mobile1', 'pager' => 'pager1', 'x-car' => 'car phone', 'textphone' => 'other phone1', ); foreach ($result['tel'] as $tel) { $type = implode('', (array)$tel['parameters']['type']); $text = $tel['text']; if (!empty($phones[$type]) && $phones[$type] == $text) { unset($phones[$type]); } } $this->assertCount(7, $result['tel']); $this->assertCount(0, $phones); // @TODO: updating some deep items (e.g. adr); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_contact; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } /** * Test photo_attachment method */ function test_photo_attachment() { $contact = array(); $result = kolab_api_filter_mapistore_contact::photo_attachment($contact); $this->assertSame(null, $result); $contact['photo'] = base64_decode('R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7'); $result = kolab_api_filter_mapistore_contact::photo_attachment($contact); $this->assertSame(true, $result['is_photo']); $this->assertSame(54, $result['size']); $this->assertSame('ContactPicture.gif', $result['filename']); $this->assertSame('image/gif', $result['mimetype']); $this->assertSame($contact['photo'], $result['content']); $this->assertSame(kolab_api_filter_mapistore_contact::PHOTO_ATTACHMENT_ID, $result['id']); } } diff --git a/tests/Unit/Filter/Mapistore/Event.php b/tests/Unit/Filter/Mapistore/Event.php index 472d7f5..69724f8 100644 --- a/tests/Unit/Filter/Mapistore/Event.php +++ b/tests/Unit/Filter/Mapistore/Event.php @@ -1,268 +1,268 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '100-100-100-100'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), $result['PidTagLastModificationTime']); $this->assertSame(2, $result['PidLidAppointmentSequence']); $this->assertSame(3, $result['PidTagSensitivity']); $this->assertSame(true, $result['PidTagHasAttachments']); - $this->assertSame('Work', $result['PidNameKeywords'][0]); + $this->assertSame(array('tag1'), $result['PidNameKeywords']); /* $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('https://some.url', $result['url']); */ $this->assertSame('Summary', $result['PidTagSubject']); $this->assertSame('Description', $result['PidTagBody']); $this->assertSame(2, $result['PidTagImportance']); $this->assertSame('Location', $result['PidLidLocation']); $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame(3, $result['recipients'][0]['PidTagRecipientFlags']); $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); $this->assertSame(0, $result['recipients'][1]['PidTagRecipientTrackStatus']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientFlags']); $this->assertSame(15, $result['PidLidReminderDelta']); $this->assertSame(true, $result['PidLidReminderSet']); $this->assertTrue($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'] == $result['PidLidAppointmentTimeZoneDefinitionStartDisplay']); // PidLidTimeZoneDefinition $tzd = new kolab_api_filter_mapistore_structure_timezonedefinition; $tzd->input($result['PidLidAppointmentTimeZoneDefinitionStartDisplay'], true); $this->assertSame('Europe/Berlin', $tzd->KeyName); $this->assertCount(1, $tzd->TZRules); $this->assertSame(2015, $tzd->TZRules[0]->Year); $this->assertSame(-60, $tzd->TZRules[0]->Bias); $data = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', false, '101-101-101-101'), $result['id']); $this->assertSame('calendars', $result['collection']); $this->assertSame('IPM.Appointment', $result['PidTagMessageClass']); $this->assertSame(0, $result['PidTagSensitivity']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentStartWhole']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T00:00:00Z'), $result['PidLidAppointmentEndWhole']); $this->assertSame(1, $result['PidLidAppointmentSubType']); $this->assertSame(null, $result['PidTagHasAttachments']); // EXDATE $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(1, $arp->RecurrencePattern->Period); $this->assertSame(0x200B, $arp->RecurrencePattern->RecurFrequency); $this->assertSame(1, $arp->RecurrencePattern->PatternType); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); // RDATE $data = kolab_api_tests::get_data('102-102-102-102', 'Calendar', 'event', 'json', $context); $result = $api->output($data, $context); // recurrence $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern; $arp->input($result['PidLidAppointmentRecur'], true); $this->assertSame(2, $arp->RecurrencePattern->DeletedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->DeletedInstanceDates); $this->assertSame(2, $arp->RecurrencePattern->ModifiedInstanceCount); $this->assertCount(2, $arp->RecurrencePattern->ModifiedInstanceDates); $this->assertSame(2, $arp->ExceptionCount); $this->assertCount(2, $arp->ExceptionInfo); $this->assertCount(2, $arp->ExtendedException); // PidLidTimeZoneStruct $tz = new kolab_api_filter_mapistore_structure_timezonestruct; $tz->input($result['PidLidTimeZoneStruct'], true); $this->assertSame(-60, $tz->Bias); $this->assertSame(0, $tz->StandardYear); $this->assertSame(10, $tz->StandardDate->Month); $this->assertSame('(GMT+01:00) Europe/Berlin', $result['PidLidTimeZoneDescription']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_event; $tzs = new kolab_api_filter_mapistore_structure_timezonestruct(array( 'Bias' => -60, 'StandardBias' => 0, 'DaylightBias' => -60, 'StandardDate' => new kolab_api_filter_mapistore_structure_systemtime(array( 'Month' => 10, 'DayOfWeek' => 0, 'Day' => 5, 'Hour' => 3, )), 'DaylightDate' => new kolab_api_filter_mapistore_structure_systemtime(array( 'Month' => 3, 'Day' => 5, 'DayOfWeek' => 0, 'Hour' => 2, )), )); $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 10, 'PidTagSensitivity' => 3, 'PidNameKeywords' => array('work'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidTagImportance' => 2, 'PidLidLocation' => 'location', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T16:00:00Z'), 'PidLidReminderDelta' => 15, 'PidLidReminderSet' => true, 'PidLidTimeZoneStruct' => $tzs->output(true), 'recipients' => array( array( 'PidTagDisplayName' => 'German, Mark', 'PidTagEmailAddress' => 'mark.german@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientFlags' => 3, ), array( 'PidTagDisplayName' => 'Manager, Jane', 'PidTagEmailAddress' => 'manager@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientTrackStatus' => 2, ), ), ); $result = $api->input($data); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(10, $result['sequence']); $this->assertSame('confidential', $result['class']); $this->assertSame(array('work'), $result['categories']); $this->assertSame('location', $result['location']); $this->assertSame(1, $result['priority']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-14T15:03:33', $result['dtstart']['date-time']); $this->assertSame('2015-05-14T18:00:00', $result['dtend']['date-time']); $this->assertRegexp('/kolab.org/', $result['dtstart']['parameters']['tzid']); $this->assertRegexp('/kolab.org/', $result['dtend']['parameters']['tzid']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Reminder', $result['valarm'][0]['properties']['description']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); // $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); self::$original = $result; $tzdef = base64_encode(pack("H*", '0201300002001500' . '500061006300690066006900630020005300740061006E0064006100720064002000540069006D006500' . '0100' . '02013E000000D6070000000000000000000000000000E001000000000000C4FFFFFF00000A0000000500020000000000000000000400000001000200000000000000' )); $data = array( 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T05:00:00Z'), 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => $tzdef, 'PidLidReminderSet' => false, // @TODO: recurrence, exceptions, alarms ); $result = $api->input($data); $this->assertSame('2015-05-13T22:00:00', $result['dtstart']['date-time']); $this->assertSame('2015-05-14T05:00:00Z', $result['dtend']['date-time']); $this->assertSame(array(), $result['valarm']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_event; $data = array( // 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:03:33Z'), // 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-14T13:50:18Z'), 'PidLidAppointmentSequence' => 20, 'PidTagSensitivity' => 2, 'PidNameKeywords' => array('work1'), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidTagImportance' => 1, 'PidLidLocation' => 'location1', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T13:03:33Z'), 'PidLidAppointmentEndWhole' => kolab_api_filter_mapistore_common::date_php2mapi('2015-05-15T16:00:00Z'), 'PidLidReminderDelta' => 25, 'PidLidReminderSet' => true, ); $result = $api->input($data, self::$original); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(20, $result['sequence']); $this->assertSame('private', $result['class']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('location1', $result['location']); $this->assertSame(5, $result['priority']); // $this->assertSame('2015-05-14T13:03:33Z', $result['created']); // $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame('2015-05-15T13:03:33Z', $result['dtstart']['date-time']); $this->assertSame('2015-05-15T16:00:00Z', $result['dtend']['date-time']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Reminder', $result['valarm'][0]['properties']['description']); $this->assertSame('-PT25M', $result['valarm'][0]['properties']['trigger']['duration']); // @TODO: recurrence, exceptions, attendees } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_event; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Info.php b/tests/Unit/Filter/Mapistore/Info.php index e6209c6..884513e 100644 --- a/tests/Unit/Filter/Mapistore/Info.php +++ b/tests/Unit/Filter/Mapistore/Info.php @@ -1,66 +1,63 @@ 'test1', 'version' => 'version1', ); $result = $api->output($data, $context); $this->assertSame('test1', $result['name']); $this->assertSame('version1', $result['version']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_info; $data = array( 'name' => 'test1', 'version' => 'version1', ); $result = $api->input($data); $this->assertSame(null, $result); } /** * Test input method with merge */ function test_input2() { // there's nothing to test here } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_info; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Mail.php b/tests/Unit/Filter/Mapistore/Mail.php index ca2d7aa..df432fa 100644 --- a/tests/Unit/Filter/Mapistore/Mail.php +++ b/tests/Unit/Filter/Mapistore/Mail.php @@ -1,157 +1,158 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', false, '1'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX', false), $result['parent_id']); $this->assertSame('mails', $result['collection']); $this->assertSame('IPM.Note', $result['PidTagMessageClass']); $this->assertSame('"test" wurde aktualisiert', $result['PidTagSubject']); $this->assertSame(624, $result['PidTagMessageSize']); $this->assertCount(1, $result['recipients']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagSmtpAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame('text/plain', $result['PidNameContentType']); $this->assertSame('20140722140159.420B4A105A@kolab32.example.org', $result['PidTagInternetMessageId']); + $this->assertSame(array('tag1'), $result['PidNameKeywords']); $data = kolab_api_tests::get_data('2', 'INBOX', 'mail', 'json', $context); $result = $api->output($data, $context); $this->assertSame('IPM.Note', $result['PidTagMessageClass']); $this->assertSame('Re: dsda', $result['PidTagSubject']); $this->assertSame(849, $result['PidTagMessageSize']); $this->assertCount(2, $result['recipients']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagSmtpAddress']); $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame('brad.pitt@example.org', $result['recipients'][1]['PidTagSmtpAddress']); $this->assertSame('Pitt, Brad', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(2, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame(null, $result['PidTagHtml']); $this->assertSame('text/plain', $result['PidNameContentType']); $this->assertSame('5071c053c207e5916123b0aa36959f03@example.org', $result['PidTagInternetMessageId']); $this->assertSame('c0e5147fc63045dedfe668877876c478@example.org', $result['PidTagInReplyToId']); $data = kolab_api_tests::get_data('5', 'INBOX', 'mail', 'json', $context); $result = $api->output($data, $context); $this->assertRegexp('/TEST CONTENT/', $result['PidTagBody']); $this->assertRegexp('/test content/', base64_decode($result['PidTagHtml'])); $this->assertSame('multipart/alternative', $result['PidNameContentType']); $this->assertSame(null, $result['PidTagHasAttachments']); // @TODO: sender/from $data = kolab_api_tests::get_data('6', 'INBOX', 'mail', 'json', $context); $result = $api->output($data, $context); $this->assertSame(true, $result['PidTagHasAttachments']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_mail; $data = array( 'id' => kolab_api_tests::mapi_uid('INBOX', false, '1000'), 'parent_id' => kolab_api_tests::folder_uid('INBOX', false), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidTagHtml' => base64_encode('html'), 'PidTagPriority' => 0xFFFFFFFF, 'PidTagInternetMessageId' => 'test-id', 'PidNameKeywords' => array('work1'), 'recipients' => array( array( 'PidTagRecipientType' => 1, 'PidTagDisplayName' => 'test-to', 'PidTagSmtpAddress' => 'test-to@domain.tld', ), array( 'PidTagRecipientType' => 2, 'PidTagDisplayName' => 'test-cc', 'PidTagSmtpAddress' => 'test-cc@domain.tld', ), ), ); $result = $api->input($data); // $this->assertSame(kolab_api_tests::mapi_uid('INBOX', false, '1000'), $result['uid']); // $this->assertSame(kolab_api_tests::folder_uid('INBOX', false), $result['parent']); $this->assertSame('subject', $result['subject']); $this->assertSame('body', $result['text']); $this->assertSame('html', $result['html']); $this->assertSame(5, $result['priority']); $this->assertSame('', $result['message-id']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('test-to', $result['to'][0]['name']); $this->assertSame('test-to@domain.tld', $result['to'][0]['address']); $this->assertSame('test-cc', $result['cc'][0]['name']); $this->assertSame('test-cc@domain.tld', $result['cc'][0]['address']); // @TODO: PidTagRtfCompressed, sender/from self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_mail; $data = array( 'recipients' => array( array( 'PidTagRecipientType' => 1, 'PidTagDisplayName' => 'test-to2', 'PidTagSmtpAddress' => 'test-to2@domain.tld', ), array( 'PidTagRecipientType' => 2, 'PidTagDisplayName' => 'test-cc2', 'PidTagSmtpAddress' => 'test-cc2@domain.tld', ), ), ); $result = $api->input($data, self::$original); $this->assertCount(1, $result['to']); $this->assertCount(1, $result['cc']); $this->assertSame('test-to2', $result['to'][0]['name']); $this->assertSame('test-to2@domain.tld', $result['to'][0]['address']); $this->assertSame('test-cc2', $result['cc'][0]['name']); $this->assertSame('test-cc2@domain.tld', $result['cc'][0]['address']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_mail; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Note.php b/tests/Unit/Filter/Mapistore/Note.php index 8453372..cd40339 100644 --- a/tests/Unit/Filter/Mapistore/Note.php +++ b/tests/Unit/Filter/Mapistore/Note.php @@ -1,131 +1,132 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Notes', false), $result['parent_id']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T11:44:59Z'), $result['PidTagCreationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T11:30:17Z'), $result['PidTagLastModificationTime']); // $this->assertSame('PUBLIC', $result['classification']); $this->assertSame('test', $result['PidTagSubject']); $this->assertRegexp('//', $result['PidTagBody']); $this->assertSame(100, $result['PidLidNoteX']); $this->assertSame(200, $result['PidLidNoteY']); - $this->assertSame(null, $body['PidTagHasAttachments']); + $this->assertSame(null, $result['PidTagHasAttachments']); + $this->assertSame(array('tag1'), $result['PidNameKeywords']); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_note; $data = array( 'id' => kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), 'parent_id' => kolab_api_tests::folder_uid('Notes', false), 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T11:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T11:30:17Z'), 'PidTagSubject' => 'subject', 'PidTagBody' => 'body', 'PidLidNoteColor' => 1, 'PidLidNoteHeight' => 100, 'PidLidNoteWidth' => 200, 'PidLidNoteX' => 300, 'PidLidNoteY' => 400, 'PidNameKeywords' => array('work1'), ); $result = $api->input($data); // $this->assertSame(kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), $result['uid']); // $this->assertSame(kolab_api_tests::folder_uid('Notes', false), $result['parent']); $this->assertSame('2015-01-20T11:44:59Z', $result['creation-date']); $this->assertSame('2015-01-22T11:30:17Z', $result['last-modification-date']); $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame('MAPI:PidLidNoteColor', $result['x-custom'][0]['identifier']); $this->assertSame(1, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidNoteHeight', $result['x-custom'][1]['identifier']); $this->assertSame(100, $result['x-custom'][1]['value']); $this->assertSame('MAPI:PidLidNoteWidth', $result['x-custom'][2]['identifier']); $this->assertSame(200, $result['x-custom'][2]['value']); $this->assertSame('MAPI:PidLidNoteX', $result['x-custom'][3]['identifier']); $this->assertSame(300, $result['x-custom'][3]['value']); $this->assertSame('MAPI:PidLidNoteY', $result['x-custom'][4]['identifier']); $this->assertSame(400, $result['x-custom'][4]['value']); $this->assertSame(array('work1'), $result['categories']); self::$original = $result; } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_note; $data = array( 'id' => kolab_api_tests::mapi_uid('Notes', false, '1-1-1-1'), 'parent_id' => kolab_api_tests::folder_uid('Notes', false), 'PidTagSubject' => 'subject1', 'PidTagBody' => 'body1', 'PidLidNoteX' => 250, 'PidLidNoteColor' => null, ); $result = $api->input($data, self::$original); // $this->assertSame('2015-01-20T11:44:59Z', $result['creation-date']); // $this->assertSame('2015-01-22T11:30:17Z', $result['last-modification-date']); $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame('MAPI:PidLidNoteHeight', $result['x-custom'][0]['identifier']); $this->assertSame(100, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidNoteWidth', $result['x-custom'][1]['identifier']); $this->assertSame(200, $result['x-custom'][1]['value']); $this->assertSame('MAPI:PidLidNoteX', $result['x-custom'][2]['identifier']); $this->assertSame(250, $result['x-custom'][2]['value']); $this->assertSame('MAPI:PidLidNoteY', $result['x-custom'][3]['identifier']); $this->assertSame(400, $result['x-custom'][3]['value']); $this->assertCount(4, $result['x-custom']); // test unsetting values $api = new kolab_api_filter_mapistore_note; $data = array( 'PidTagSubject' => '', ); $result = $api->input($data, self::$original); $this->assertSame('', $result['summary']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_note; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Filter/Mapistore/Task.php b/tests/Unit/Filter/Mapistore/Task.php index e41cdeb..971664a 100644 --- a/tests/Unit/Filter/Mapistore/Task.php +++ b/tests/Unit/Filter/Mapistore/Task.php @@ -1,184 +1,185 @@ output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', false, '10-10-10-10'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks', false), $result['parent_id']); $this->assertSame('IPM.Task', $result['PidTagMessageClass']); $this->assertSame('tasks', $result['collection']); $this->assertSame('task title', $result['PidTagSubject']); $this->assertSame("task description\nsecond line", $result['PidTagBody']); $this->assertSame(0.56, $result['PidLidPercentComplete']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20T14:22:18Z', true), $result['PidTagLastModificationTime']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20T14:22:18Z', true), $result['PidTagCreationTime']); $this->assertSame(8, $result['PidLidTaskActualEffort']); $this->assertSame(true, $result['PidTagHasAttachments']); + $this->assertSame(array('tag1'), $result['PidNameKeywords']); $data = kolab_api_tests::get_data('20-20-20-20', 'Tasks', 'task', 'json', $context); $result = $api->output($data, $context); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', false, '20-20-20-20'), $result['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks', false), $result['parent_id']); $this->assertSame('IPM.Task', $result['PidTagMessageClass']); $this->assertSame('tasks', $result['collection']); $this->assertSame('task', $result['PidTagSubject']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20', true), $result['PidLidTaskStartDate']); $this->assertSame(kolab_api_filter_mapistore_common::date_php2mapi('2015-04-27', true), $result['PidLidTaskDueDate']); $this->assertSame(null, $result['PidTagHasAttachments']); // organizer/attendees $this->assertSame('German, Mark', $result['recipients'][0]['PidTagDisplayName']); $this->assertSame('mark.german@example.org', $result['recipients'][0]['PidTagEmailAddress']); $this->assertSame(1, $result['recipients'][0]['PidTagRecipientType']); $this->assertSame('Manager, Jane', $result['recipients'][1]['PidTagDisplayName']); $this->assertSame(1, $result['recipients'][1]['PidTagRecipientType']); $this->assertSame('jane.manager@example.org', $result['recipients'][1]['PidTagEmailAddress']); // recurrence $rp = new kolab_api_filter_mapistore_structure_recurrencepattern; $rp->input($result['PidLidTaskRecurrence'], true); $this->assertSame(true, $result['PidLidTaskFRecurring']); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY, $rp->PatternType); $this->assertSame(kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY, $rp->RecurFrequency); } /** * Test input method */ function test_input() { $api = new kolab_api_filter_mapistore_task; $data = array( 'id' => kolab_api_tests::mapi_uid('Tasks', false, '10-10-10-10'), 'parent_id' => kolab_api_tests::folder_uid('Tasks', false), 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T11:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T11:30:17Z'), 'PidTagMessageClass' => 'IPM.Task', 'PidTagSubject' => 'subject', 'PidLidPercentComplete' => 0.56, 'PidTagBody' => 'body', 'PidLidTaskStartDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20', true), 'PidLidTaskDueDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-27', true), 'PidLidTaskActualEffort' => 16, 'PidLidTaskEstimatedEffort' => 20, 'PidNameKeywords' => array('work1'), 'recipients' => array( array( 'PidTagDisplayName' => 'German, Mark', 'PidTagEmailAddress' => 'mark.german@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientFlags' => 3, ), array( 'PidTagDisplayName' => 'Manager, Jane', 'PidTagEmailAddress' => 'manager@example.org', 'PidTagRecipientType' => 1, 'PidTagRecipientTrackStatus' => 2, ), ), ); $result = $api->input($data); self::$original = $result; $this->assertSame('subject', $result['summary']); $this->assertSame('body', $result['description']); $this->assertSame(56, $result['percent-complete']); $this->assertSame('2015-01-20T11:44:59Z', $result['created']); $this->assertSame('2015-01-22T11:30:17Z', $result['dtstamp']); $this->assertSame('2015-04-20', $result['dtstart']); $this->assertSame('2015-04-27', $result['due']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame(16, $result['x-custom'][0]['value']); $this->assertSame('MAPI:PidLidTaskEstimatedEffort', $result['x-custom'][1]['identifier']); $this->assertSame(20, $result['x-custom'][1]['value']); $this->assertSame(array('work1'), $result['categories']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('TENTATIVE', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); // $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:manager%40example.org', $result['attendee'][0]['cal-address']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:mark.german%40example.org', $result['organizer']['cal-address']); $data = array( 'PidLidTaskComplete' => true, 'PidLidTaskDateCompleted' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-20', true), 'PidLidTaskActualEffort' => 100, 'PidLidTaskEstimatedEffort' => 100, // @TODO: recurrence ); $result = $api->input($data); $this->assertSame('COMPLETED', $result['status']); $this->assertSame('MAPI:PidLidTaskDateCompleted', $result['x-custom'][0]['identifier']); $this->assertSame(13073961600.0, $result['x-custom'][0]['value']); } /** * Test input method with merge */ function test_input2() { $api = new kolab_api_filter_mapistore_task; $data = array( 'PidTagCreationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-20T12:44:59Z'), 'PidTagLastModificationTime' => kolab_api_filter_mapistore_common::date_php2mapi('2015-01-22T12:30:17Z'), // 'PidTagMessageClass' => 'IPM.Task', 'PidTagSubject' => 'subject1', 'PidLidPercentComplete' => 0.66, 'PidTagBody' => 'body1', 'PidLidTaskStartDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-21', true), 'PidLidTaskDueDate' => kolab_api_filter_mapistore_common::date_php2mapi('2015-04-28', true), 'PidLidTaskActualEffort' => 21, 'PidLidTaskEstimatedEffort' => null, ); $result = $api->input($data, self::$original); self::$original = $result; $this->assertSame('subject1', $result['summary']); $this->assertSame('body1', $result['description']); $this->assertSame(66, $result['percent-complete']); $this->assertSame('2015-01-20T12:44:59Z', $result['created']); $this->assertSame('2015-01-22T12:30:17Z', $result['dtstamp']); $this->assertSame('2015-04-21', $result['dtstart']); $this->assertSame('2015-04-28', $result['due']); $this->assertSame('MAPI:PidLidTaskActualEffort', $result['x-custom'][0]['identifier']); $this->assertSame(21, $result['x-custom'][0]['value']); $this->assertCount(1, $result['x-custom']); } /** * Test map method */ function test_map() { $api = new kolab_api_filter_mapistore_task; $map = $api->map(); $this->assertInternalType('array', $map); $this->assertTrue(!empty($map)); } } diff --git a/tests/Unit/Output/Json/Event.php b/tests/Unit/Output/Json/Event.php index 34ff68c..4db0705 100644 --- a/tests/Unit/Output/Json/Event.php +++ b/tests/Unit/Output/Json/Event.php @@ -1,69 +1,69 @@ element($object); $this->assertSame('100-100-100-100', $result['uid']); $this->assertSame('2015-05-14T13:03:33Z', $result['created']); $this->assertSame('2015-05-14T13:50:18Z', $result['dtstamp']); $this->assertSame(2, $result['sequence']); $this->assertSame('CONFIDENTIAL', $result['class']); - $this->assertSame('Work', $result['categories'][0]); + $this->assertSame('tag1', $result['categories'][0]); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtstart']['parameters']['tzid']); $this->assertSame('2015-05-15T10:00:00', $result['dtstart']['date-time']); $this->assertSame('/kolab.org/Europe/Berlin', $result['dtend']['parameters']['tzid']); $this->assertSame('2015-05-15T10:30:00', $result['dtend']['date-time']); $this->assertSame('Summary', $result['summary']); $this->assertSame('Description', $result['description']); $this->assertSame(1, $result['priority']); $this->assertSame('Location', $result['location']); $this->assertSame('German, Mark', $result['organizer']['parameters']['cn']); $this->assertSame('mailto:%3Cmark.german%40example.org%3E', $result['organizer']['cal-address']); $this->assertSame('https://some.url', $result['url']); $this->assertSame('Manager, Jane', $result['attendee'][0]['parameters']['cn']); $this->assertSame('NEEDS-ACTION', $result['attendee'][0]['parameters']['partstat']); $this->assertSame('REQ-PARTICIPANT', $result['attendee'][0]['parameters']['role']); $this->assertSame(true, $result['attendee'][0]['parameters']['rsvp']); $this->assertSame('mailto:%3Cjane.manager%40example.org%3E', $result['attendee'][0]['cal-address']); $this->assertSame('image/jpeg', $result['attach'][0]['parameters']['fmttype']); $this->assertSame('photo-mini.jpg', $result['attach'][0]['parameters']['x-label']); $this->assertSame('cid:photo-mini.1431611291.28810.jpg', $result['attach'][0]['uri']); $this->assertSame('DISPLAY', $result['valarm'][0]['properties']['action']); $this->assertSame('Summary', $result['valarm'][0]['properties']['description']); $this->assertSame('START', $result['valarm'][0]['properties']['trigger']['parameters']['related']); $this->assertSame('-PT15M', $result['valarm'][0]['properties']['trigger']['duration']); $object = kolab_api_tests::get_data('101-101-101-101', 'Calendar', 'event', null, $context); $result = $output->element($object); $this->assertSame('101-101-101-101', $result['uid']); $this->assertSame('PUBLIC', $result['class']); $this->assertSame('2015-05-15', $result['dtstart']); $this->assertSame('2015-05-15', $result['dtend']); $this->assertSame('WEEKLY', $result['rrule']['recur']['freq']); $this->assertSame('MO', $result['rrule']['recur']['byday']); $this->assertSame('2015-06-05', $result['exdate']['date'][0]); $this->assertSame('2015-06-12', $result['exdate']['date'][1]); $object = kolab_api_tests::get_data('102-102-102-102', 'Calendar', 'event', null, $context); $result = $output->element($object); $this->assertSame('102-102-102-102', $result['uid']); $this->assertSame('2015-06-25', $result['rdate']['date'][0]); $this->assertSame('2015-06-28', $result['rdate']['date'][1]); } } diff --git a/tests/data/data.json b/tests/data/data.json index f051640..22fbdd6 100644 --- a/tests/data/data.json +++ b/tests/data/data.json @@ -1,72 +1,72 @@ { "folders": { "INBOX": { "type": "mail.inbox", "items": ["1","2","5","6","7"] }, "Trash": { "type": "mail.wastebasket", "items": [] }, "Drafts": { "type": "mail.drafts" }, "Sent": { "type": "mail.sentitems" }, "Junk": { "type": "mail.junkemail" }, "Calendar": { "type": "event.default", "items": ["100-100-100-100","101-101-101-101"] }, "Calendar/Personal Calendar": { "type": "event" }, "Contacts": { "type": "contact.default", "items": ["a-b-c-d","e-f-g-h"] }, "Files": { "type": "file.default" }, "Files2": { "type": "file" }, "Notes": { "type": "note.default", "items": ["1-1-1-1","2-2-2-2"] }, "Tasks": { "type": "task.default", "items":["10-10-10-10","20-20-20-20"] }, "Configuration": { "type": "configuration.default", "items": ["98-98-98-98","99-99-99-99"] }, "Mail-Test": { "type": "mail" }, "Mail-Test2": { "type": "mail" } }, "tags": { "tag1": { - "members": ["1", "10-10-10-10", "1-1-1-1", "a-b-c-d"] + "members": ["1", "10-10-10-10", "1-1-1-1", "a-b-c-d", "100-100-100-100"] }, "tag2": { } } } diff --git a/tests/lib/kolab_api_backend.php b/tests/lib/kolab_api_backend.php index 107292c..ece169c 100644 --- a/tests/lib/kolab_api_backend.php +++ b/tests/lib/kolab_api_backend.php @@ -1,1028 +1,1034 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $delimiter = '/'; public $username = 'user@example.org'; public $storage; public $user; public $db = array(); protected $folder = array(); protected $data = 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() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; if (file_exists($db_file)) { $db = file_get_contents($db_file); $this->db = unserialize($db); } $json = file_get_contents(__DIR__ . '/../data/data.json'); $this->data = json_decode($json, true); $this->folders = $this->parse_folders_list($this->data['folders']); if (!array_key_exists('tags', $this->db)) { $this->db['tags'] = $this->data['tags']; } $this->user = new kolab_api_user; $this->storage = $this; } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { return true; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { return array_values($this->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->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $type = $folder['type'] ?: 'mail'; if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type kolab_api_mail or array) * @throws kolab_api_exception */ public function objects_list($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $result = array(); $is_mail = empty($folder['type']) || preg_match('/^mail/', $folder['type']); foreach ((array) $folder['items'] as $id) { $object = $this->object_get($uid, $id); if ($is_mail) { $object = new kolab_api_message($object->headers, array('is_header' => true)); } $result[] = $object; } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return count($folder['items']); } /** * 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) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === '*') { foreach ((array) $this->folders[$uid]['items'] as $i) { unset($this->db['messages'][$i]); } $this->folders[$uid]['items'] = array(); $this->db['items'][$uid] = array(); } else { $this->folders[$uid]['items'] = array_values(array_diff($this->folders[$uid]['items'], $set)); foreach ($set as $i) { unset($this->db['items'][$uid][$i]); unset($this->db['messages'][$i]); } } $this->db['folders'][$uid]['items'] = $this->folders[$uid]['items']; $this->save_db(); } /** * 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) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $target = $this->folders[$target_uid]; if (!$target) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === "*") { $set = $this->folders[$uid]['items']; } // @TODO: we should check if all objects from the set exist $diff = array_values(array_diff($this->folders[$uid]['items'], $set)); $this->folders[$uid]['items'] = $diff; $this->db['folders'][$uid]['items'] = $diff; $diff = array_values(array_merge((array) $this->folders[$target_uid]['items'], $set)); $this->folders[$target_uid]['items'] = $diff; $this->db['folders'][$target_uid]['items'] = $diff; foreach ($set as $i) { if ($this->db['items'][$uid][$i]) { $this->db['items'][$target_uid][$i] = $this->db['items'][$uid][$i]; unset($this->db['items'][$uid][$i]); } } $this->save_db(); } /** * 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) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!in_array($uid, (array) $folder['items'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($data = $this->db['items'][$folder_uid][$uid]) { return $data; } list($type,) = explode('.', $folder['type']); $file = $this->get_file_content($uid, $type); if (empty($file)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // get message content and parse it $file = str_replace("\r?\n", "\r\n", $file); $params = array('uid' => $uid, 'folder' => $folder_uid); $object = new kolab_api_message($file, $params); // get assigned tag-relations $tags = array(); foreach ($this->db['tags'] as $tag_name => $tag) { if (in_array($uid, (array) $tag['members'])) { $tags[] = $tag_name; } } if ($type != 'mail') { $object = $object->to_array($type); - $object['categories'] = $tags; + $object['categories'] = array_unique(array_merge($tags, (array) $object['categories'])); } else { $object = new kolab_api_message($object); - $object->categories = $tags; + $object->set_categories($tags); } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ $uid = microtime(true); if (is_array($data)) { $categories = $data['categories']; $data['uid'] = $uid; $this->db['items'][$folder_uid][$uid] = $data; } else { $categories = $data->categories; $uid = $data->save($folder['fullpath']); } if (!empty($categories)) { foreach ($categories as $cat) { if (!$this->db['tags'][$cat]) { $this->db['tags'][$cat] = array(); } if (!in_array($uid, (array) $this->db['tags'][$cat]['members'])) { $this->db['tags'][$cat]['members'][] = $uid; } } - } $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); return $uid; } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ + // kolab object if (is_array($data)) { - $uid = $data['uid']; - - if (array_key_exists('categories', $data)) { - foreach ($this->db['tags'] as $tag_name => $tag) { - if (($idx = array_search($tag_name, (array) $data['categories'])) !== false) { - unset($data['categories'][$idx]); - continue; - } - - $this->db['tags'][$tag_name]['members'] = array_diff((array)$this->db['tags'][$tag_name]['members'], array($data['uid'])); - } - foreach ((array) $data['categories'] as $cat) { - $this->db['tags'][$cat] = array('members' => array($data['uid'])); - } - } + $uid = $data['uid']; + $categories = $data['categories']; // remove _formatobj which is problematic in serialize/unserialize unset($data['_formatobj']); $this->db['items'][$folder_uid][$uid] = $data; - $this->save_db(); } + // email message else { - $uid = $data->save($folder['fullpath']); + $old_uid = $data->uid; + $categories = $data->categories; + $uid = $data->save($folder['fullpath']); $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; - $this->save_db(); } + // remove old tag assignments + foreach ($this->db['tags'] as $tag_name => $tag) { + if (($idx = array_search($old_uid ?: $uid, (array) $this->db['tags'][$tag_name]['members'])) !== false) { + unset($this->db['tags'][$tag_name]['members'][$idx]); + } + } + + // assign new tags + foreach ($categories as $tag) { + if (!$this->db['tags'][$tag]) { + $this->db['tags'][$tag] = array(); + } + + $this->db['tags'][$tag]['members'][] = $uid; + } + + $this->save_db(); + return $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 $mode=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } $body = $object->get_part_body($part_id); if (!$mode) { return $body; } else if ($mode === -1) { echo $body; } } /** * 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) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; $key = $msg_uid . ":" . $part_id; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } if ($object->get_part_body($id) === null) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $uid = $object->attachment_delete($id); // change UID only for mail messages if (!is_numeric($msg_uid)) { $this->db['messages'][$msg_uid] = $this->db['messages'][$uid]; unset($this->db['messages'][$uid]); $this->save_db(); $uid = $msg_uid; } if ($msg_uid != $uid) { $folder_uid = $object->folder; $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Create an attachment and add to a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attachment Attachment data * * @return string Message/object UID * @throws kolab_api_exception */ public function attachment_create($object, $attachment) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } $uid = $object->attachment_add($attachment); $folder_uid = $object->folder; // change UID only for mail messages if (!is_numeric($msg_uid)) { $this->db['messages'][$msg_uid] = $this->db['messages'][$uid]; unset($this->db['messages'][$uid]); $params = array('uid' => $msg_uid, 'folder' => $folder_uid); $object = new kolab_api_message(base64_decode($this->db['messages'][$msg_uid]), $params); $object = $object->to_array($this->folders[$folder_uid]['type']); // $object['categories'] = $tags; unset($object['_formatobj']); $this->db['items'][$folder_uid][$msg_uid] = $object; $this->save_db(); $uid = $msg_uid; } if ($msg_uid != $uid) { $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Update an attachment in a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attachment Attachment data * * @return string Message/object UID * @throws kolab_api_exception */ public function attachment_update($object, $attachment) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } $uid = $object->attachment_update($attachment); $folder_uid = $object->folder; // change UID only for mail messages if (!is_numeric($msg_uid)) { $this->db['messages'][$msg_uid] = $this->db['messages'][$uid]; unset($this->db['messages'][$uid]); $params = array('uid' => $msg_uid, 'folder' => $folder_uid); $object = new kolab_api_message(base64_decode($this->db['messages'][$msg_uid]), $params); $object = $object->to_array($this->folders[$folder_uid]['type']); // $object['categories'] = $tags; unset($object['_formatobj']); $this->db['items'][$folder_uid][$msg_uid] = $object; $this->save_db(); $uid = $msg_uid; } if ($msg_uid != $uid) { $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $folder = $name; if ($parent) { $parent_folder = $this->folders[$parent]; if (!$parent_folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $folder = $parent_folder['fullpath'] . $this->delimiter . $folder; } $uid = kolab_api_tests::folder_uid($folder, false); // check if folder exists if ($this->folders[$uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->folders[$uid] = array( 'name' => $name, 'fullpath' => $folder, 'parent' => $parent ? kolab_api_tests::folder_uid($parent, false) : null, 'uid' => $uid, 'type' => $type ? $type : 'mail', ); $this->db['folders'][$uid] = $this->folders[$uid]; $this->save_db(); return $uid; } /** * Updates a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } foreach ($updates as $idx => $value) { $this->db['folders'][$uid][$idx] = $value; $this->folders[$uid][$idx] = $value; } $this->save_db(); } /** * Renames/moves a folder * * @param string $old_name Folder name (UTF8) * @param string $new_name New folder name (UTF8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_uid = kolab_api_tests::folder_uid($old_name, false); $new_uid = kolab_api_tests::folder_uid($new_name, false); $folder = $this->folders[$old_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($this->folders[$new_uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $path = explode($this->delimiter, $new_name); $folder['fullpath'] = $new_name; $folder['name'] = array_pop($path); unset($this->folders[$old_uid]); $this->folders[$new_uid] = $folder; $this->db['folders'][$new_uid] = $folder; $this->db['deleted'][] = $old_uid; $this->save_db(); } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } unset($this->folders[$uid]); $this->db['deleted'][] = $uid; $this->save_db(); } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // some info is not very interesting here ;) unset($folder['items']); return $folder; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $folder['fullpath']; } /** * Parse folders list into API format */ protected function parse_folders_list($list) { $folders = array(); foreach ($list as $path => $folder) { $uid = kolab_api_tests::folder_uid($path, false); if (!empty($this->db['deleted']) && in_array($uid, $this->db['deleted'])) { continue; } if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = kolab_api_tests::folder_uid($parent, false); } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); if (!empty($this->db['folders']) && !empty($this->db['folders'][$uid])) { $data = array_merge($data, $this->db['folders'][$uid]); } $folders[$uid] = array_merge($folder, $data); } foreach ((array) $this->db['folders'] as $uid => $folder) { if (!$folders[$uid]) { $folders[$uid] = $folder; } } // sort folders uasort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1['fullpath']); $path2 = explode($this->delimiter, $str2['fullpath']); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Save current database state */ public function save_db() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; $db = serialize($this->db); file_put_contents($db_file, $db); } /** * Wrapper for rcube_imap::set_flag() */ public function set_flag($uid, $flag) { $flag = strtoupper($flag); $folder_uid = $this->folder_uid($folder); $flags = (array) $this->db['flags'][$uid]; if (strpos($flag, 'UN') === 0) { $flag = substr($flag, 3); $flags = array_values(array_diff($flags, array($flag))); } else { $flags[] = $flag; $flags = array_unique($flags); } $this->db['flags'][$uid] = $flags; $this->save_db(); return true; } /** * Wrapper for rcube_imap::save_message() */ public function save_message($folder, $streams) { $folder_uid = $this->folder_uid($folder); $uid = '3' . count($this->db['messages']) . preg_replace('/^[0-9]+\./', '', microtime(true)); $content = ''; foreach ($streams as $stream) { rewind($stream); $content .= stream_get_contents($stream); } $this->db['messages'][$uid] = base64_encode($content); $this->save_db(); return $uid; } /** * Wrapper for rcube_imap::delete_message() */ public function delete_message($uid, $folder) { $folder_uid = $this->folder_uid($folder); $this->folders[$folder_uid]['items'] = array_values(array_diff((array)$this->folders[$folder_uid]['items'], array($uid))); unset($this->db['items'][$folder_uid][$uid]); unset($this->db['messages'][$uid]); $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); return true; } /** * Wrapper for rcube_imap::get_raw_body */ public function get_raw_body($uid, $fp = null, $part = null) { $file = $this->get_file_content($uid); $file = explode("\r\n\r\n", $file, 2); if (stripos($part, 'TEXT') !== false) { $body = $file[1]; if (preg_match('/^([0-9]+)/', $part, $m)) { if (preg_match('/boundary="?([^"]+)"?/', $file[0], $mm)) { $parts = explode('--' . $mm[1], $body); $parts = explode("\r\n\r\n", $parts[$m[1]], 2); $body = $parts[1]; } else { $body = ''; } } } if ($fp) { fwrite($fp, $body); return true; } else { return $body; } } /** * Wrapper for rcube_imap::get_raw_headers */ public function get_raw_headers($uid) { $file = $this->get_file_content($uid); $file = explode("\r\n\r\n", $file, 2); return $file[0]; } /** * Wrapper for rcube_imap::set_folder */ public function set_folder($folder) { // do nothing } /** * Find folder UID by its name */ protected function folder_uid($name) { foreach ($this->folders as $uid => $folder) { if ($folder['fullpath'] == $name) { return $uid; } } } /** * Get sample message from tests/data dir */ protected function get_file_content($uid, $type = null) { if ($file = $this->db['messages'][$uid]) { $file = base64_decode($file); } else { if (empty($type)) { foreach (array('mail', 'event', 'task', 'note', 'contact') as $t) { $file = __DIR__ . '/../data/' . $t . '/' . $uid; if (file_exists($file)) { $type = $t; break; } } } $file = file_get_contents(__DIR__ . '/../data/' . $type . '/' . $uid); } return $file; } } /** * Dummy class imitating rcube_user */ class kolab_api_user { public function get_username($type) { $api = kolab_api_backend::get_instance(); list($local, $domain) = explode('@', $api->username); if ($type == 'domain') { return $domain; } else if ($type == 'local') { return $local; } return $api->username; } public function get_user_id() { return 10; } public function get_identity() { return array( 'email' => 'user@example.org', 'name' => 'Test User', ); } } diff --git a/tests/lib/kolab_api_message.php b/tests/lib/kolab_api_message.php index 047025d..b0cee7e 100644 --- a/tests/lib/kolab_api_message.php +++ b/tests/lib/kolab_api_message.php @@ -1,256 +1,292 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Mock class emulating rcube_message and kolab_api_mail * With some additional functionality for testing */ class kolab_api_message extends kolab_api_mail { public $attachments = array(); public $parts = array(); public $mime_parts = array(); public $folder; public $uid; public $headers; + protected $_categories = array(); + /** * Class initialization */ public function __construct($content = null, $params = array()) { // kolab_api_mail mode if (is_object($content)) { $this->message = $content; $this->headers = $content->headers; $this->mime_parts = $content->mime_parts; $params['folder'] = $content->folder ?: $content->headers->folder; $params['uid'] = $content->uid ?: $content->headers->uid; } // rcube_message mode else if ($content) { $this->message = rcube_mime::parse_message($content); $this->headers = rcube_message_header::from_array($this->message->headers); $this->message->headers = $this->headers; $this->headers->ctype = $this->message->mimetype; foreach ((array) $params as $idx => $val) { $this->headers->{$idx} = $val; } $this->headers->size = strlen($content); $this->set_mime_parts($this->message); } if ($this->message) { foreach ((array) $this->message->parts as $part) { if ($part->filename || $part->disposition == 'attachment') { $this->attachments[] = $part; } $this->parts[$part->mime_id] = $part; } $this->message->attachments = $this->attachments; } foreach ((array) $params as $idx => $val) { $this->{$idx} = $val; $this->message->{$idx} = $val; } } /** * Returns body of the message part */ public function get_part_body($id, $formatted = false, $max_bytes = 0, $mode = null) { if (!$id) { $body = $this->message->body; } else { $body = $this->mime_parts[$id]->body; } if (is_resource($mode)) { fwrite($mode, $body); } else { return $body; } } /** * Convert message into Kolab object * * @return array Kolab object */ public function to_array() { $object_type = kolab_format::mime2object_type($this->headers->others['x-kolab-type']); $content_type = kolab_format::KTYPE_PREFIX . $object_type; $attachments = array(); // get XML part foreach ((array)$this->attachments as $part) { if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) { $xml = $this->get_part_body($part->mime_id); } else if ($part->filename || $part->content_id || $part->disposition == 'attachment') { $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename; $size = null; // Use Content-Disposition 'size' as for the Kolab Format spec. if (isset($part->d_parameters['size'])) { $size = $part->d_parameters['size']; } // we can trust part size only if it's not encoded else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') { $size = $part->size; } // looks like MimeDecode does not support d_parameters (?) else if (preg_match('/size=([0-9]+)/', $part->headers['content-disposition'], $m)) { $size = $m[1]; } if (!$key) { $key = $part->mime_id; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'encoding' => $part->encoding, 'size' => $size, ); } } // check kolab format version $format_version = $this->headers->others['x-kolab-mime-version']; if (empty($format_version)) { list($xmltype, $subtype) = explode('.', $object_type); $xmlhead = substr($xml, 0, 512); // detect old Kolab 2.0 format if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false) $format_version = '2.0'; else $format_version = '3.0'; // assume 3.0 } // get Kolab format handler for the given type $format = kolab_format::factory($object_type, $format_version, $xml); if (is_a($format, 'PEAR_Error')) { return false; } // load Kolab object from XML part $format->load($xml); if ($format->is_valid()) { $object = $format->to_array(array('_attachments' => $attachments)); $object['_formatobj'] = $format; $object['_type'] = $object_type; $object['_attachments'] = $attachments; $object['_message'] = $this; // $object['_msguid'] = $msguid; // $object['_mailbox'] = $this->name; return $object; } return false; } /** * Send the message stream using configured method */ protected function send_message_stream($stream) { return true; } /** * Get rcube_message object of the assigned message */ protected function get_message() { return $this->message; } /** * rcube_message::first_html_part() emulation. */ public function first_html_part(&$part = null, $enriched = false) { foreach ((array) $this->mime_parts as $part) { if (!$part->filename && $part->mimetype == 'text/html') { return $this->get_part_body($part->mime_id, true); } } $part = null; } /** * rcube_message::first_text_part() emulation. */ public function first_text_part(&$part = null, $strict = false) { // no message structure, return complete body if (empty($this->mime_parts)) { return $this->message->body; } foreach ((array) $this->mime_parts as $part) { if (!$part->filename && $part->mimetype == 'text/plain') { return $this->get_part_body($part->mime_id, true); } } $part = null; } + /** + * Properties getter + * + * @param string $name Property name + * + * @param mixed Property value + */ + public function __get($name) + { + if ($name == 'categories' && !array_key_exists($name, $this->data)) { + return (array) $this->_categories; + } + + return parent::__get($name); + } + + /** + * Categories setter + */ + public function set_categories($categories) + { + $this->_categories = $categories; + } + + protected function update_flags($uid) + { + // @TODO + } + + protected function update_categories($uid) + { + // empty, handled by kolab_api_backend of tests + } + /** * Fill aflat array with references to all parts, indexed by part numbers */ private function set_mime_parts(&$part) { if (strlen($part->mime_id)) { $this->mime_parts[$part->mime_id] = &$part; } if (is_array($part->parts)) { for ($i = 0; $i < count($part->parts); $i++) { $this->set_mime_parts($part->parts[$i]); } } } } diff --git a/tests/lib/kolab_api_tests.php b/tests/lib/kolab_api_tests.php index 8f608fe..2603212 100644 --- a/tests/lib/kolab_api_tests.php +++ b/tests/lib/kolab_api_tests.php @@ -1,405 +1,418 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_tests { static $items_map; static $folders_map; + static $db; /** * Reset backend state */ public static function reset_backend() { $rcube = rcube::get_instance(); $temp_dir = $rcube->config->get('temp_dir'); $filename = $temp_dir . '/tests.db'; if (file_exists($filename)) { unlink($filename); } $username = $rcube->config->get('tests_username'); $password = $rcube->config->get('tests_password'); if (!$username) { return; } $authenticated = self::login($username, $password); if (!$authenticated) { throw new Exception("IMAP login failed for user $username"); } // get all existing folders $imap = $rcube->get_storage(); $old_folders = $imap->list_folders('', '*'); $old_subscribed = $imap->list_folders_subscribed('', '*'); // get configured folders $json = file_get_contents(__DIR__ . '/../data/data.json'); $data = json_decode($json, true); $items = array(); $uids = array(); // initialize/update content in existing folders // create configured folders if they do not exists foreach ($data['folders'] as $folder_name => $folder) { if (($idx = array_search($folder_name, $old_folders)) !== false) { // cleanup messages in the folder $imap->delete_message('*', $folder_name); unset($old_folders[$idx]); // make sure it's subscribed if (!in_array($folder_name, $old_subscribed)) { $imap->subscribe($folder_name); } } else { // create the folder $imap->create_folder($folder_name, true); } // set folder type kolab_storage::set_folder_type($folder_name, $folder['type']); list($type, ) = explode('.', $folder['type']); // append messages foreach ((array) $folder['items'] as $uid) { $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); - $res = $imap->save_message($folder_name, $file); + + // replace member message references + if ($uid == '99-99-99-99') { + $repl = urlencode($username) . '/INBOX/' . $items[1]; + $file = str_replace('mark.german%40example.org/INBOX/1', $repl, $file); + } + + $res = $imap->save_message($folder_name, $file); if (is_numeric($uid)) { $items[$uid] = $res; } } } // remove extra folders $deleted = array(); foreach ($old_folders as $folder) { // ...but only personal if ($imap->folder_namespace($folder) == 'personal') { $path = explode('/', $folder); while (array_pop($path) !== null) { if (in_array(implode('/', $path), $deleted)) { $deleted[] = $folder; continue 2; } } if (!$imap->delete_folder($folder)) { throw new Exception("Failed removing '$folder'"); } $deleted[] = $folder; } else { } } // get folder UIDs map $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $imap->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new Exception("Failed to get folders metadata"); } foreach ($metadata as $folder => $meta) { $uids[$folder] = $meta[kolab_storage::UID_KEY_CYRUS]; } self::$items_map = $items; self::$folders_map = $uids; } /** * Initialize testing environment */ public static function init() { $rcube = rcube::get_instance(); // If tests_username is set we use real Kolab server // otherwise use dummy backend class which emulates a real server if (!$rcube->config->get('tests_username')) { // Load backend wrappers for tests // @TODO: maybe we could replace kolab_storage and rcube_imap instead? require_once __DIR__ . '/kolab_api_backend.php'; - } - // Message wrapper for unit tests - require_once __DIR__ . '/kolab_api_message.php'; + // Message wrapper for unit tests + require_once __DIR__ . '/kolab_api_message.php'; + + // extend include path with kolab_format/kolab_storage classes + $include_path = __DIR__ . '/../../lib/ext/plugins/libkolab/lib' . PATH_SEPARATOR . ini_get('include_path'); + set_include_path($include_path); + } // load HTTP_Request2 wrapper for functional/integration tests require_once __DIR__ . '/kolab_api_request.php'; - - // extend include path with kolab_format/kolab_storage classes - $include_path = __DIR__ . '/../../lib/ext/plugins/libkolab/lib' . PATH_SEPARATOR . ini_get('include_path'); - set_include_path($include_path); } /** * Initializes kolab_api_request object * * @param string Accepted response type (xml|json) * * @return kolab_api_request Request object */ public static function get_request($type, $suffix = '') { $rcube = rcube::get_instance(); $base_uri = $rcube->config->get('tests_uri', 'http://localhost/copenhagen-tests'); $username = $rcube->config->get('tests_username', 'test@example.org'); $password = $rcube->config->get('tests_password', 'test@example.org'); if ($suffix) { $base_uri .= $suffix; } $request = new kolab_api_request($base_uri, $username, $password); // set expected response type $request->set_header('Accept', $type == 'xml' ? 'application/xml' : 'application/json'); return $request; } /** * Get data object */ public static function get_data($uid, $folder_name, $type, $format = '', &$context = null) { + require_once __DIR__ . '/kolab_api_message.php'; + $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); $folder_uid = self::folder_uid($folder_name, false); // get message content and parse it $file = str_replace("\r?\n", "\r\n", $file); $params = array('uid' => $uid, 'folder' => $folder_uid); $object = new kolab_api_message($file, $params); + if (empty(self::$db)) { + $json = file_get_contents(__DIR__ . '/../data/data.json'); + self::$db = json_decode($json, true); + } + + // get assigned tag-relations + $tags = array(); + foreach (self::$db['tags'] as $tag_name => $tag) { + if (in_array($uid, (array) $tag['members'])) { + $tags[] = $tag_name; + } + } + if ($type != 'mail') { $object = $object->to_array($type); + $object['categories'] = $tags; } else { $object = new kolab_api_message($object); + $object->set_categories($tags); } $context = array( 'object' => $object, 'folder_uid' => $folder_uid, 'object_uid' => $uid, ); if ($format) { $model = self::get_output_class($format, $type); $object = $model->element($object); } return $object; } public static function get_output_class($format, $type) { // fake GET request to have proper API class in kolab_api::get_instance $_GET['request'] = "{$type}s"; $output = "kolab_api_output_{$format}"; $class = "{$output}_{$type}"; $output = new $output(kolab_api::get_instance()); $model = new $class($output); return $model; } /** * Get folder UID by name */ public static function folder_uid($name, $api_test = true) { if ($api_test && !empty(self::$folders_map)) { if (self::$folders_map[$name]) { return self::$folders_map[$name]; } // it maybe is a newly created folder? check the metadata again $rcube = rcube::get_instance(); $imap = $rcube->get_storage(); $uid_keys = array(kolab_storage::UID_KEY_CYRUS); $metadata = $imap->get_metadata($name, $uid_keys); if ($uid = $metadata[$name][kolab_storage::UID_KEY_CYRUS]) { return self::$folders_map[$name] = $uid; } } return md5($name); } /** * Get message UID */ public static function msg_uid($uid, $api_test = true) { if ($uid && $api_test && !empty(self::$items_map)) { if (self::$items_map[$uid]) { return self::$items_map[$uid]; } } return $uid; } /** * Build MAPI object identifier */ public static function mapi_uid($folder_name, $api_test, $msg_uid, $attachment_uid = null) { $folder_uid = self::folder_uid($folder_name, $api_test); $msg_uid = self::msg_uid($msg_uid, $api_test); return kolab_api_filter_mapistore::uid_encode($folder_uid, $msg_uid, $attachment_uid); } protected static function login($username, $password) { $rcube = rcube::get_instance(); $login_lc = $rcube->config->get('login_lc'); $host = $rcube->config->get('default_host'); $default_port = $rcube->config->get('default_port', 143); $rcube->storage = null; $storage = $rcube->get_storage(); // 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 (!$storage->connect($host, $username, $password, $port, $ssl)) { throw new Exception("Unable to connect to IMAP"); } // No user in database, but IMAP auth works if (!is_object($user)) { if ($rcube->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { throw new Exception("Failed to create a user record"); } } else { throw new Exception("Access denied for new user $username. 'auto_create_user' is disabled"); } } // overwrite config with user preferences $rcube->user = $user; $rcube->config->set_user_prefs((array)$user->get_prefs()); /* $_SESSION['user_id'] = $user->ID; $_SESSION['username'] = $user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $rcube->encrypt($password); $_SESSION['login_time'] = time(); */ setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); // clear the cache $storage->clear_cache('mailboxes', true); // to clear correctly the cache index in testing environments // (where we call self::reset_backend() many times in one go) // we need to also close() the cache if ($ctype = $rcube->config->get('imap_cache')) { $cache = $rcube->get_cache('IMAP', $ctype, $rcube->config->get('imap_cache_ttl', '10d')); $cache->close(); } // clear also libkolab cache $db = $rcube->get_dbh(); $db->query('DELETE FROM `kolab_folders`'); return true; } - - /** - * Initialize backend class, some unit-tests require it - */ - public static function init_backend() - { - $api = kolab_api::get_instance(); - - if (!$api->backend) { - $api->backend = kolab_api_backend::get_instance(); - } - } }