diff --git a/lib/api/attachments.php b/lib/api/attachments.php index dd7dfd5..c6eed65 100644 --- a/lib/api/attachments.php +++ b/lib/api/attachments.php @@ -1,197 +1,198 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_attachments extends kolab_api { protected $model = 'attachment'; public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if ($path[0] && $path[1] && $method == 'POST') { $this->api_attachment_create(); } else if ($path[2]) { if ($method == 'GET') { if ($path[3] === 'get') { $this->api_attachment_body(); } else { $this->api_attachment_info(); } } else if ($method == 'PUT') { $this->api_attachment_update(); } else if ($method == 'HEAD') { $this->api_attachment_exists(); } else if ($method == 'DELETE') { $this->api_attachment_delete(); } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Fetch attachment info */ protected function api_attachment_info() { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->backend->object_get($folder, $object_uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); // get attachment part from the object $attachment = $this->get_attachment($object, $attach_uid); $this->output->send($attachment, $this->model, $context); } /** * Create a attachment */ protected function api_attachment_create() { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $object_uid); $attachment = $this->input->input('attachment'); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); // @TODO // $uid = $this->backend->attachment_add($folder, $object, $attachment); // $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * Update specified attachment */ protected function api_attachment_update() { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $object_uid); $attachment = $this->input->input('attachment'); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); // @TODO // merge old object's data with new properties // $note = $this->merge_data($note, $object); // $this->backend->object_update($folder, $note, 'note'); // $this->output->send(array('id' => $uid), $this->model, $folder); // $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Check if specified attachment exists */ protected function api_attachment_exists() { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->backend->object_get($folder, $object_uid); // get attachment part from the object $attachment = $this->get_attachment($object, $attach_uid); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove specified attachment */ protected function api_attachment_delete() { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->backend->object_get($folder, $object_uid); $this->backend->attachment_delete($object, $attach_uid); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Fetch attachment body */ protected function api_attachment_body() { $folder = $this->input->path[0]; $object_uid = $this->input->path[1]; $attach_uid = $this->input->path[2]; $object = $this->backend->object_get($folder, $object_uid); // get attachment part from the object $attachment = $this->get_attachment($object, $attach_uid); // @TODO: set headers // print attachment body $this->backend->attachment_get($object, $attach_uid, -1); exit; } /** * Find attachment in an object/message */ protected function get_attachment($object, $id) { if ($object) { $attachments = (array) $this->get_object_attachments($object); foreach ($attachments as $attachment) { if ($attachment->mime_id == $id) { return $attachment; } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } } diff --git a/lib/api/contacts.php b/lib/api/contacts.php index 0804c7b..dbd211e 100644 --- a/lib/api/contacts.php +++ b/lib/api/contacts.php @@ -1,28 +1,29 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_contacts extends kolab_api { protected $model = 'contact'; } diff --git a/lib/api/events.php b/lib/api/events.php index 4054212..b7f0e5e 100644 --- a/lib/api/events.php +++ b/lib/api/events.php @@ -1,28 +1,29 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_events extends kolab_api { protected $model = 'event'; } diff --git a/lib/api/folders.php b/lib/api/folders.php index 374b7b3..910362b 100644 --- a/lib/api/folders.php +++ b/lib/api/folders.php @@ -1,375 +1,376 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_folders extends kolab_api { protected $model = 'folder'; public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; $request_length = count($path); if (!$request_length && $method == 'POST') { $this->api_folder_create(); } else if (!$request_length && $method == 'GET') { $this->api_folder_list_folders(); } else if ($request_length >= 1) { switch (strtolower((string) $path[1])) { case 'objects': if ($method == 'HEAD') { $this->api_folder_count_objects(); } else if ($method == 'GET') { $this->api_folder_list_objects(); } break; case 'folders': if ($method == 'HEAD') { $this->api_folder_count_folders(); } else if ($method == 'GET') { $this->api_folder_list_folders(); } break; case 'empty': if ($method == 'POST') { $this->api_folder_empty(); } break; case 'deleteobjects': if ($method == 'POST') { $this->api_folder_delete_objects(); } break; case 'move': if ($method == 'POST') { $this->api_folder_move_objects(); } break; case '': if ($request_length == 1) { if ($method == 'GET') { $this->api_folder_info(); } else if ($method == 'PUT') { $this->api_folder_update(); } else if ($method == 'HEAD') { $this->api_folder_exists(); } else if ($method == 'DELETE') { $this->api_folder_delete(); } } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Returns list of folders (all or subfolders of specified folder) */ protected function api_folder_list_folders() { $root = $this->input->path[0]; $list = $this->backend->folders_list(); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; // filter by parent if ($root) { $this->filter_folders($list, $root); } $list = array_map(array($this, 'folder_props_filter'), $list); $this->output->send($list, $this->model . '-list', null, $props); } /** * Returns count of folders (all or subfolders of specified folder) */ protected function api_folder_count_folders() { $root = $this->input->path[0]; $list = $this->backend->folders_list(); // filter by parent if ($root) { $this->filter_folders($list, $root); } $this->output->headers(array('X-Count' => count($list))); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Returns folders info */ protected function api_folder_info() { $folder = $this->input->path[0]; $info = $this->backend->folder_info($folder); $info = self::folder_props_filter($info); $this->output->send($info, $this->model); } /** * Checks if folder exists */ protected function api_folder_exists() { $folder = $this->input->path[0]; $folder = $this->backend->folder_uid2path($folder); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Delete a folder */ protected function api_folder_delete() { $folder = $this->input->path[0]; $this->backend->folder_delete($folder); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Update specified folder (rename or set folder type, or state) */ protected function api_folder_update() { $folder = $this->input->path[0]; $request = $this->input->input($this->model); $info = $this->backend->folder_info($folder); // need rename? $parent_change = isset($request['parent']) && $request['parent'] != $info['parent']; $name_change = isset($request['name']) && $request['name'] !== $info['name']; $fullpath = $info['fullpath']; $updates = array(); // rename first if ($parent_change || $name_change) { if ($name_change) { if (!strlen($request['name'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $path = explode($this->backend->delimiter, $info['fullpath']); array_pop($path); $path = implode($this->backend->delimiter, $path); $info['name'] = $request['name']; $info['fullpath'] = (strlen($path) ? $path . $this->backend->delimiter : '') . $request['name']; } if ($parent_change) { if ($request['parent']) { $parent = $this->backend->folder_uid2path($request['parent']); $info['fullpath'] = $parent . $this->backend->delimiter . $info['name']; } else { $info['fullpath'] = $info['name']; } } $this->backend->folder_rename($fullpath, $info['fullpath']); } // type change? if (isset($request['type']) && $request['type'] != $info['type']) { // @TODO: don't allow type changes on subfolders or non-empty folders? // @TODO: check input validity $updates['type'] = $request['type']; } // subscription change? if (isset($request['subscribed']) && $request['subscribed'] != $info['subscribed']) { $updates['subscribed'] = (bool) $request['subscribed']; } if (!empty($updates)) { $this->backend->folder_update($folder, $updates); } - $this->output->send_status(kolab_api_output::STATUS_EMPTY); + $this->output->send(array('uid' => $folder), $this->model); } /** * Create a folder */ protected function api_folder_create() { $folder = $this->input->input($this->model); if (!isset($folder['name']) || !strlen($folder['name'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $uid = $this->backend->folder_create($folder['name'], $folder['parent'], $folder['type']); // @TODO: folder subscribe, activate $this->output->send(array('uid' => $uid), $this->model, null, array('uid')); } /** * Returns list of objects (not folders) in specified folder */ protected function api_folder_list_objects() { $folder = $this->input->path[0]; $type = $this->backend->folder_type($folder); $list = $this->backend->objects_list($folder); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array('folder_uid' => $folder); $this->output->send($list, $type . '-list', $context, $props); } /** * Returns count of objects (not folders) in specified folder */ protected function api_folder_count_objects() { $folder = $this->input->path[0]; $count = $this->backend->objects_count($folder); $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Delete objects in specified folder */ protected function api_folder_delete_objects() { $folder = $this->input->path[0]; $set = $this->input->input(); if (empty($set)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $this->backend->objects_delete($folder, $set); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Move objects into specified folder */ protected function api_folder_move_objects() { $folder = $this->input->path[0]; $target = $this->input->path[2]; $set = $this->input->input(); if (empty($set)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if ($target === null || $target === '' || $target === $folder) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $this->backend->objects_move($folder, $target, $set); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove all objects in specified folder */ protected function api_folder_empty() { $folder = $this->input->path[0]; $this->backend->objects_delete($folder, '*'); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Filter folders by parent */ protected function filter_folders(&$list, $parent) { // filter by parent if ($parent && ($parent = $this->backend->folder_uid2path($parent))) { // we'll compare with 'fullpath' which is in UTF-8 $parent .= $this->backend->delimiter; foreach ($list as $idx => $folder) { if (strpos($folder['fullpath'], $parent) !== 0) { unset($list[$idx]); } } } $list = array_values($list); } /** * Filter folder data and return only supported properties */ public static function folder_props_filter($data) { static $props; if ($props === null) { $props = array( 'uid', 'parent', 'name', 'fullpath', 'type', 'rights', 'namespace', 'exists', 'unseen', 'modseq', ); $props = array_combine($props, $props); } return array_intersect_key($data, $props); } } diff --git a/lib/api/info.php b/lib/api/info.php index 1e4b72a..0d6ff41 100644 --- a/lib/api/info.php +++ b/lib/api/info.php @@ -1,44 +1,45 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_info extends kolab_api { public function run() { $this->initialize_handler(); if ($this->input->method == 'GET' && empty($this->input->path)) { // @TODO: It's just a sample $info = array( 'name' => kolab_api::APP_NAME, 'version' => kolab_api::VERSION, 'capabilities' => array(), ); $this->output->send($info, 'info'); } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } } diff --git a/lib/api/mails.php b/lib/api/mails.php index f474bb6..cff2f51 100644 --- a/lib/api/mails.php +++ b/lib/api/mails.php @@ -1,129 +1,117 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_mails extends kolab_api { protected $model = 'mail'; public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if ($path[0] === 'submit' && $method == 'POST') { $this->api_message_submit(); } else if (!$path[1] && $path[0] && $method == 'POST') { $this->api_object_create(); } else if ($path[1]) { switch (strtolower($path[2])) { case 'attachments': if ($method == 'HEAD') { $this->api_message_count_attachments(); } else if ($method == 'GET') { $this->api_message_list_attachments(); } break; case '': if ($method == 'GET') { $this->api_object_info(); } else if ($method == 'PUT') { - $this->api_message_update(); + $this->api_object_update(); } else if ($method == 'HEAD') { $this->api_object_exists(); } else if ($method == 'DELETE') { $this->api_object_delete(); } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Submit a message object into SMTP server */ protected function api_message_submit() { - // @TODO - } - - /** - * Update specified message object - */ - protected function api_message_update() - { -/* - $folder = $this->input->path[0]; - $uid = $this->input->path[1]; - $email = $this->input->input($this->model); - $object = $this->backend->object_get($folder, $uid); + // parse input and merge with current data (returns kolab_api_mail) + $input = $this->input->input($this->model, false); - // merge old object's data with new properties - $email = $this->merge_data($email, $object); + // send the message + $uid = $input->send(); - $this->backend->object_update($folder, $email, $this->model); + // @TODO: option to save in Sent -// $this->output->send(array('id' => $uid), $this->model, $folder); - $this->output->send_status(kolab_api_output::STATUS_EMPTY); -*/ + $this->output->send(array('id' => $uid), $this->model, $folder); } /** * Count message attachments */ protected function api_message_count_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $count = count($object->attachments); $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * List message attachments */ protected function api_message_list_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array('folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object); $list = $object->attachments; $this->output->send($list, 'attachment-list', $context, $props); } } diff --git a/lib/api/notes.php b/lib/api/notes.php index 38e943c..000f721 100644 --- a/lib/api/notes.php +++ b/lib/api/notes.php @@ -1,28 +1,29 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_notes extends kolab_api { protected $model = 'note'; } diff --git a/lib/api/tasks.php b/lib/api/tasks.php index f2285ff..e99936f 100644 --- a/lib/api/tasks.php +++ b/lib/api/tasks.php @@ -1,28 +1,29 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_tasks extends kolab_api { protected $model = 'task'; } diff --git a/lib/filter/mapistore.php b/lib/filter/mapistore.php index ca0c73e..a0f214e 100644 --- a/lib/filter/mapistore.php +++ b/lib/filter/mapistore.php @@ -1,624 +1,629 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore extends kolab_api_filter { protected $input; protected $attrs_filter; // Common properties [MS-OXCMSG] protected static $map = array( // 'PidTagAccess' => '', // 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify // 'PidTagChangeKey' => '', 'PidTagCreationTime' => 'creation-date', // PtypTime, UTC 'PidTagLastModificationTime' => 'last-modification-date', // PtypTime, UTC // 'PidTagLastModifierName' => '', // 'PidTagObjectType' => '', // @TODO // 'PidTagHasAttachments' => '', // @TODO // 'PidTagRecordKey' => '', // 'PidTagSearchKey' => '', 'PidNameKeywords' => 'categories', ); /** * 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) { - $this->input = $input; + $this->input = $input; + $this->common_action = !in_array($input->action, array('folders', 'info')); // handle differences between OpenChange API and Kolab API switch ($input->action) { case 'folders': // in OpenChange folders/1/folders means get all folders 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 // we should ignore/send empty request else if ($input->method == 'GET' && $input->path[0] === '0' && $input->path[1] == 'folders') { // @TODO throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } else if ($input->path[1] == 'messages') { $input->path[1] = 'objects'; if ($input->args['properties']) { $type = $input->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); } 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; } - $common_action = !in_array($input->action, array('folders', 'info')); - // convert / to // or /// - if ($common_action && ($uid = $input->path[0])) { + 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' && $common_action && !count($input->path)) { + 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); } } } /** * 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) { $input = $this->input; // handle differences between OpenChange API and Kolab API // Note: input->path is already modified by input() and path() above switch ($input->action) { case 'folders': // folders//deletemessages input if ($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); } 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); } } /** * Executed for response headers * * @param array Response headers */ public function headers(&$headers) { // handle differences between OpenChange API and Kolab API foreach ($headers as $name => $value) { switch ($name) { case 'X-Count': $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; + } } /** * Extracts data from kolab data array */ public static function get_kolab_value($data, $name) { $name_items = explode('.', $name); $count = count($name_items); $value = $data[$name_items[0]]; // special handling of x-custom properties if ($name_items[0] === 'x-custom') { foreach ((array) $value as $custom) { if ($custom['identifier'] === $name_items[1]) { return $custom['value']; } } return null; } for ($i = 1; $i < $count; $i++) { if (!is_array($value)) { return null; } list($key, $num) = explode(':', $name_items[$i]); $value = $value[$key]; if ($num !== null && $value !== null) { $value = is_array($value) ? $value[$num] : null; } } return $value; } /** * Sets specified kolab data item */ public static function set_kolab_value(&$data, $name, $value) { $name_items = explode('.', $name); $count = count($name_items); $element = &$data; // x-custom properties if ($name_items[0] === 'x-custom') { // this is supposed to be converted later by parse_common_props() $data[$name] = $value; return; } if ($count > 1) { for ($i = 0; $i < $count - 1; $i++) { $key = $name_items[$i]; if (!array_key_exists($key, $element)) { $element[$key] = array(); } $element = &$element[$key]; } } $element[$name_items[$count - 1]] = $value; } /** * 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; } /** * Parse common properties in object data (convert into MAPI format) */ public static function parse_common_props(&$result, $data, $context = array()) { if (empty($context)) { // @TODO: throw exception? return; } if ($data['uid'] && $context['folder_uid']) { $result['id'] = self::uid_encode($context['folder_uid'], $data['uid']); } if ($context['folder_uid']) { $result['parent_id'] = $context['folder_uid']; } foreach (self::$map as $mapi_idx => $kolab_idx) { if (!isset($result[$mapi_idx]) && ($value = $data[$kolab_idx]) !== null) { switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': $result[$mapi_idx] = self::date_php2mapi($value, true); break; case 'PidNameKeywords': $result[$mapi_idx] = self::parse_categories((array) $value); break; } } } } /** * Convert common properties into kolab format */ public static function convert_common_props(&$result, $data, $original) { // @TODO: id, parent_id? foreach (self::$map as $mapi_idx => $kolab_idx) { if (array_key_exists($mapi_idx, $data) && !array_key_exists($kolab_idx, $result)) { $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $dt = self::date_mapi2php($value); $result[$kolab_idx] = $dt->format('Y-m-d\TH:i:s\Z'); } break; default: if ($value) { $result[$kolab_idx] = $value; } break; } } } // Handle x-custom fields foreach ((array) $result as $key => $value) { if (strpos($key, 'x-custom.') === 0) { unset($result[$key]); $key = substr($key, 9); foreach ((array) $original['x-custom'] as $idx => $custom) { if ($custom['identifier'] == $key) { if ($value) { $original['x-custom'][$idx]['value'] = $value; } else { unset($original['x-custom'][$idx]); } $x_custom_update = true; continue 2; } } if ($value) { $original['x-custom'][] = array( 'identifier' => $key, 'value' => $value, ); } $x_custom_update = true; } } if ($x_custom_update) { $result['x-custom'] = array_values($original['x-custom']); } } /** * Filter property names */ public function attributes_filter($attrs, $type = null, $reverse = false) { $map = self::$map; $result = array(); if ($type) { $model = $this->get_model_class($type); $map = array_merge($map, $model->map()); } // add some special common attributes $map['PidTagMessageClass'] = 'PidTagMessageClass'; $map['collection'] = 'collection'; $map['id'] = 'uid'; foreach ($attrs as $attr) { if ($reverse) { if ($name = array_search($attr, $map)) { $result[] = $name; } } else if ($name = $map[$attr]) { $result[] = $name; } } return $result; } /** * Return instance of model class object */ protected function get_model_class($type) { $class = "kolab_api_filter_mapistore_$type"; return new $class($this); } /** * Convert DateTime object to MAPI date format */ public function date_php2mapi($date, $utc = true, $time = null) { // convert string to DateTime if (!is_object($date) && !empty($date)) { // convert date to datetime on 00:00:00 if (preg_match('/^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$/', $date, $m)) { $date = $m[1] . '-' . $m[2] . '-' . $m[3] . 'T00:00:00+00:00'; } $date = new DateTime($date); } if (!is_object($date)) { return; } if ($utc) { $date->setTimezone(new DateTimeZone('UTC')); } if (!empty($time)) { $date->setTime((int) $time['hour'], (int) $time['minute'], (int) $time['second']); } // MAPI PTypTime is 64-bit integer representing the number // of 100-nanosecond intervals since January 1, 1601. // Note: probably does not work on 32-bit systems return ($date->format('U') + 11644473600) * 10000000; } /** * Convert date-time from MAPI format to DateTime */ public function date_mapi2php($date) { // Note: probably does not work on 32-bit systems $seconds = intval($date / 10000000) - 11644473600; // assumes we're working with dates after 1970-01-01 return new DateTime('@' . $seconds); } /** * Parse categories according to [MS-OXCICAL 2.1.3.1.1.20.3] * * @param array Categories * * @return array Categories */ public static function parse_categories($categories) { if (!is_array($categories)) { return; } $result = array(); foreach ($categories as $idx => $val) { $val = preg_replace('/(\x3B|\x2C|\x06\x1B|\xFE\x54|\xFF\x1B)/', '', $val); $val = preg_replace('/\s+/', ' ', $val); $val = trim($val); $len = mb_strlen($val); if ($len) { if ($len > 255) { $val = mb_substr($val, 0, 255); } $result[mb_strtolower($val)] = $val; } } return array_values($result); } /** * 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); } } } diff --git a/lib/filter/mapistore/attachment.php b/lib/filter/mapistore/attachment.php index 34ed7c4..52dfad7 100644 --- a/lib/filter/mapistore/attachment.php +++ b/lib/filter/mapistore/attachment.php @@ -1,184 +1,185 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_attachment { protected $map = array( // general props (read-only) 'PidTagAccessLevel' => '', // 0 - read-only, 1 - modify 'PidTagObjectType' => '', 'PidTagRecordKey' => '', // PtypBinary // other props 'PidTagLastModificationTime' => '', 'PidTagCreationTime' => '', 'PidTagDisplayName' => 'filename', 'PidTagAttachSize' => 'size', 'PidTagAttachNumber' => '', // @TODO: unique attachment index within a message 'PidTagAttachDataBinary' => '', // PtypBinary 'PidTagAttachDataObject' => '', // PtypBinary 'PidTagAttachMethod' => '', 'PidTagAttachLongFilename' => 'filename', // filename with extension 'PidTagAttachFilename' => '', // filename in 8.3 form 'PidTagAttachExtension' => '', 'PidTagAttachLongPathname' => '', 'PidTagAttachPathname' => '', 'PidTagAttachTag' => '', // PtypBinary 'PidTagRenderingPosition' => '', 'PidTagAttachRendering' => '', // PtypBinary 'PidTagAttachFlags' => '', 'PidTagAttachTransportName' => '', 'PidTagAttachEncoding' => '', // PtypBinary 'PidTagAttachAdditionalInformation' => '', // PtypBinary 'PidTagAttachmentLinkId' => '', 'PidTagAttachmentFlags' => '', 'PidTagAttachmentHidden' => '', // PtypBoolean 'PidTagTextAttachmentChars' => 'charset', // MIME props 'PidTagAttachMimeTag' => 'mimetype', 'PidTagAttachContentId' => 'content-id', 'PidTagAttachContentLocation' => 'content-location', 'PidTagAttachContentBase' => '', 'PidTagAttachPayloadClass' => '', 'PidTagAttachPayloadProviderGuidString' => '', 'PidNameAttachmentMacContentType' => '', 'PidNameAttachmentMacInfo' => '', // PtypBinary ); /** * Methods for PidTagAttachMethod */ protected $methods = array( 'afNone' => 0x00000001, 'afByValue' => 0x00000001, 'afByReference' => 0x00000002, 'afByReferenceOnly' => 0x00000004, 'afEmbeddedMessage' => 0x00000005, 'afStorage' => 0x00000006, ); /** * 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( 'PidTagObjectType' => 0x00000007, // mapistore REST API specific properties 'collection' => 'attachments', ); // @TODO: Set PidTagAccessLevel depending if a message is in Drafts or not // or the attachment belongs to a kolab object in writeable folder? foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } $result[$mapi_idx] = $value; } if (($pos = strrpos($data['filename'], '.')) !== false) { $result['PidTagAttachExtension'] = substr($data['filename'], $pos + 1); } // Store attachment body in base64 // @TODO: shouldn't we do this only in attachment.info request? $result['PidTagAttachDataBinary'] = $this->attachment_body($context['object'], $data, true); $result['PidTagAttachMethod'] = $this->methods['afByValue']; kolab_api_filter_mapistore::parse_common_props($result, $data, $context); $result['id'] = kolab_api_filter_mapistore::uid_encode($context['folder_uid'], $context['object_uid'], $data['id']); 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) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; $result[$kolab_idx] = $value; } return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidTagAttachExtension'] = 'filename'; return $map; } /** * Get attachment body */ protected function attachment_body($object, $attachment, $encoded = false) { if (empty($object)) { return; } $api = kolab_api::get_instance(); $body = $api->backend->attachment_get($object, $attachment['id']); return $encoded ? base64_encode($body) : $body; } } diff --git a/lib/filter/mapistore/contact.php b/lib/filter/mapistore/contact.php index fbedb59..3457842 100644 --- a/lib/filter/mapistore/contact.php +++ b/lib/filter/mapistore/contact.php @@ -1,579 +1,580 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_contact { protected $map = array( // contact name properties [MS-OXOCNTC] 'PidTagNickname' => 'nickname', 'PidTagGeneration' => 'n.suffix', 'PidTagDisplayNamePrefix' => 'n.prefix', 'PidTagSurname' => 'n.surname', 'PidTagMiddleName' => 'n.additional', 'PidTagGivenName' => 'n.given', 'PidTagInitials' => 'x-custom.MAPI:PidTagInitials', 'PidTagDisplayName' => 'fn', 'PidLidYomiFirstName' => '', 'PidLidYomiLastName' => '', 'PidLidFileUnder' => '', 'PidLidFileUnderId' => '', // PtypInteger32 'PidLidFileUnderList' => '', // PtypMultipleInteger32 // electronic and phisical address properties 'PidTagPrimaryFaxNumber' => 'x-custom.MAPI:PidTagPrimaryFaxNumber', 'PidTagBusinessFaxNumber' => 'x-custom.MAPI:PidTagBusinessFaxNumber', 'PidTagHomeFaxNumber' => '', 'PidTagHomeAddressStreet' => '', 'PidTagHomeAddressCity' => '', 'PidTagHomeAddressStateOrProvince' => '', 'PidTagHomeAddressPostalCode' => '', 'PidTagHomeAddressCountry' => '', 'PidLidHomeAddressCountryCode' => '', 'PidTagHomeAddressPostOfficeBox' => '', 'PidLidHomeAddress' => '', // @TODO: ? 'PidLidWorkAddressStreet' => '', 'PidLidWorkAddressCity' => '', 'PidLidWorkAddressState' => '', 'PidLidWorkAddressPostalCode' => '', 'PidLidWorkAddressCountry' => '', 'PidLidWorkAddressCountryCode' => '', 'PidLidWorkAddressPostOfficeBox' => '', 'PidLidWorkAddress' => '', // @TODO: ? 'PidTagOtherAddressStreet' => '', 'PidTagOtherAddressCity' => '', 'PidTagOtherAddressStateOrProvince' => '', 'PidTagOtherAddressPostalCode' => '', 'PidTagOtherAddressCountry' => '', 'PidLidOtherAddressCountryCode' => '', 'PidTagOtherAddressPostOfficeBox' => '', 'PidLidOtherAddress' => '', // @TODO: ? 'PidTagStreetAddress' => '', // @TODO: ? 'PidTagLocality' => '', // @TODO: ? 'PidTagStateOrProvince' => '', // @TODO: ? 'PidTagPostalCode' => '', // @TODO: ? 'PidTagCountry' => '', // @TODO: ? 'PidLidAddressCountryCode' => '', // @TODO: ? 'PidTagPostOfficeBox' => '', // @TODO: ? 'PidTagPostalAddress' => '', // @TODO: ? 'PidTagPagerTelephoneNumber' => '', 'PidTagCallbackTelephoneNumber' => '', 'PidTagBusinessTelephoneNumber' => '', 'PidTagHomeTelephoneNumber' => '', 'PidTagPrimaryTelephoneNumber' => '', 'PidTagBusiness2TelephoneNumber' => '', 'PidTagMobileTelephoneNumber' => '', 'PidTagRadioTelephoneNumber' => '', 'PidTagCarTelephoneNumber' => '', 'PidTagOtherTelephoneNumber' => '', 'PidTagAssistantTelephoneNumber' => '', 'PidTagHome2TelephoneNumber' => 'x-custom.MAPI:PidTagHome2TelephoneNumber', 'PidTagTelecommunicationsDeviceForDeafTelephoneNumber' => 'x-custom.MAPI:PidTagTelecommunicationsDeviceForDeafTelephoneNumber', 'PidTagCompanyMainTelephoneNumber' => 'x-custom.MAPI:PidTagCompanyMainTelephoneNumber', 'PidTagTelexNumber' => '', 'PidTagIsdnNumber' => '', 'PidTagBirthday' => 'bday', // PtypTime, UTC 'PidLidBirthdayLocal' => '', // PtypTime 'PidTagWeddingAnniversary' => 'anniversary', // PtypTime, UTC 'PidLidWeddingAnniversaryLocal' => '', // PtypTime // professional properties 'PidTagTitle' => 'title', 'PidTagCompanyName' => '', 'PidLidYomiCompanyName' => '', 'PidTagDepartmentName' => '', 'PidTagOfficeLocation' => 'x-custom.MAPI:PidTagOfficeLocation', 'PidTagManagerName' => '', 'PidTagAssistant' => '', 'PidTagProfession' => 'group.role', 'PidLidHasPicture' => '', // PtypBoolean, more about photo attachments in MS-OXOCNTC // other properties 'PidTagHobbies' => 'x-custom.MAPI:PidTagHobbies', 'PidTagSpouseName' => '', 'PidTagLanguage' => 'lang', 'PidTagLocation' => 'x-custom.MAPI:PidTagLocation', 'PidLidInstantMessagingAddress' => 'impp', 'PidTagOrganizationalIdNumber' => 'x-custom.MAPI:PidTagOrganizationalIdNumber', 'PidTagCustomerId' => 'x-custom.MAPI:PidTagCustomerId', 'PidTagGovernmentIdNumber' => 'x-custom.MAPI:PidTagGovernmentIdNumber', 'PidTagPersonalHomePage' => 'url', 'PidTagBusinessHomePage' => 'x-custom.MAPI:PidTagBussinessHomePage', 'PidTagFtpSite' => 'x-custom.MAPI:PidTagFtpSite', 'PidLidFreeBusyLocation' => 'fburl', 'PidTagChildrenNames' => '', // PtypMultipleString 'PidTagGender' => 'gender', 'PidTagUserX509Certificate' => 'key', // PtypMultipleBinary 'PidTagMessageClass' => '', // IPM.Contact, IPM.DistList 'PidTagBody' => 'note', 'PidTagLastModificationTime' => 'rev', /* // distribution lists [MS-OXOCNTC] 'PidLidDistributionListName' => '', // PtypString = PidTagDisplayName 'PidLidDistributionListMembers' => '', // PtypMultipleBinary 'PidLidDistributionListOneOffMembers' => '', // PtypMultipleBinary 'PidLidDistributionListChecksum' => '', // PtypInteger32 'PidLidDistributionListStream' => '', // PtypBinary */ ); protected $gender_map = array( 0 => '', 1 => 'F', 2 => 'M', ); protected $phone_map = array( 'PidTagPagerTelephoneNumber' => 'pager', 'PidTagBusinessTelephoneNumber' => 'work', 'PidTagHomeTelephoneNumber' => 'home', 'PidTagMobileTelephoneNumber' => 'cell', 'PidTagCarTelephoneNumber' => 'x-car', 'PidTagOtherTelephoneNumber' => 'textphone', 'PidTagBusinessFaxNumber' => 'faxwork', 'PidTagHomeFaxNumber' => 'faxhome', ); protected $email_map = array( 'PidLidEmail1EmailAddress' => 'home', 'PidLidEmail2EmailAddress' => 'work', 'PidLidEmail3EmailAddress' => 'other', ); /** * 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( // @TODO: IPM.DistList for groups 'PidTagMessageClass' => 'IPM.Contact', // mapistore REST API specific properties 'collection' => 'contacts', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagGender': $value = (int) array_search($value, $this->gender_map); break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (is_array($value)) { $value = $value[0]; } break; case 'PidTagBirthday': case 'PidTagWeddingAnniversary': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, false); break; case 'PidTagUserX509Certificate': foreach ((array) $value as $val) { if ($val && preg_match('|^data:application/pkcs7-mime;base64,|i', $val, $m)) { $result[$mapi_idx] = substr($val, strlen($m[0])); continue 3; } } $value = null; break; case 'PidTagTitle': if (is_array($value)) { $value = $value[0]; } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // contact photo attachment [MS-OXVCARD 2.1.3.2.4] if (!empty($data['photo'])) { // @TODO: check if photo is one of .bmp, .gif, .jpeg, .png // Photo in MAPI is handled as attachment // Set PidTagAttachmentContactPhoto=true on attachment object $result['PidLidHasPicture'] = true; } // Organization/Department $organization = $data['group']['org']; if (is_array($organization)) { $result['PidTagCompanyName'] = $organization[0]; $result['PidTagDepartmentName'] = $organization[1]; } else if ($organization !== null) { $result['PidTagCompanyName'] = $organization; } // Manager/Assistant $related = $data['group']['related']; if ($related && $related['parameters']) { $related = array($related); } foreach ((array) $related as $rel) { $type = $rel['parameters']['type']; if ($type == 'x-manager') { $result['PidTagManagerName'] = $rel['text']; } else if ($type == 'x-assistant') { $result['PidTagAssistant'] = $rel['text']; } } // Children, Spouse foreach ((array) $data['related'] as $rel) { $type = $rel['parameters']['type']; if ($type == 'child') { $result['PidTagChildrensNames'][] = $rel['text']; } else if ($type == 'spouse') { $result['PidTagSpouseName'] = $rel['text']; } } // Emails $email_map = array_flip($this->email_map); foreach ((array) $data['email'] as $email) { $type = is_array($email) ? $email['parameters']['type'] : 'other'; $key = $email_map[$type] ?: $email_map['other']; $result[$key] = is_array($email) ? $email['text'] : $email; } // Phone(s) $phone_map = array_flip($this->phone_map); $phones = $data['tel']; if ($phones && $phones['parameters']) { $phones = array($phones); } foreach ((array) $phones as $phone) { $type = implode('', (array)$phone['parameters']['type']); if ($phone['text'] && ($idx = $phone_map[$type])) { $result[$idx] = $phone['text']; } } // Addresses(s) $addresses = $data['adr']; if ($addresses && $addresses['parameters']) { $addresses = array($addresses); } foreach ((array) $addresses as $addr) { $type = $addr['parameters']['type']; $address = null; if ($type == 'home') { $address = array( 'PidTagHomeAddressStreet' => $addr['street'], 'PidTagHomeAddressCity' => $addr['locality'], 'PidTagHomeAddressStateOrProvince' => $addr['region'], 'PidTagHomeAddressPostalCode' => $addr['code'], 'PidTagHomeAddressCountry' => $addr['country'], 'PidTagHomeAddressPostOfficeBox' => $addr['pobox'], ); } else if ($type == 'work') { $address = array( 'PidLidWorkAddressStreet' => $addr['street'], 'PidLidWorkAddressCity' => $addr['locality'], 'PidLidWorkAddressState' => $addr['region'], 'PidLidWorkAddressPostalCode' => $addr['code'], 'PidLidWorkAddressCountry' => $addr['country'], 'PidLidWorkAddressPostOfficeBox' => $addr['pobox'], ); } if (!empty($address)) { $result = array_merge($result, array_filter($address)); } } $other_adr_map = array( 'street' => 'PidTagOtherAddressStreet', 'locality' => 'PidTagOtherAddressCity', 'region' => 'PidTagOtherAddressStateOrProvince', 'code' => 'PidTagOtherAddressPostalCode', 'country' => 'PidTagOtherAddressCountry', 'pobox' => 'PidTagOtherAddressPostOfficeBox', ); foreach ((array) $data['group']['adr'] as $idx => $value) { if ($value && ($key = $other_adr_map[$idx])) { $result[$key] = $value; } } kolab_api_filter_mapistore::parse_common_props($result, $data, $context); 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) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagBirthday': case 'PidTagWeddingAnniversary': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidTagGender': $value = $this->gender_map[(int)$value]; break; case 'PidTagUserX509Certificate': if (!empty($value)) { $value = array('data:application/pkcs7-mime;base64,' . $value); } break; case 'PidTagPersonalHomePage': case 'PidLidInstantMessagingAddress': if (!empty($value)) { $value = array($value); } break; } kolab_api_filter_mapistore::set_kolab_value($result, $kolab_idx, $value); } // MS-OXVCARD 2.1.3.2.1 if (!empty($data['PidTagNormalizedSubject']) && empty($data['PidTagDisplayName'])) { $result['fn'] = $data['PidTagNormalizedSubject']; } // Organization/Department if ($data['PidTagCompanyName']) { $result['group']['org'][] = $data['PidTagCompanyName']; } if (!empty($data['PidTagDepartmentName'])) { $result['group']['org'][] = $data['PidTagDepartmentName']; } // Manager if ($data['PidTagManagerName']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-manager'), 'text' => $data['PidTagManagerName'], ); } // Assistant if ($data['PidTagAssistant']) { $result['group']['related'][] = array( 'parameters' => array('type' => 'x-assistant'), 'text' => $data['PidTagAssistant'], ); } // Spouse if ($data['PidTagSpouseName']) { $result['related'][] = array( 'parameters' => array('type' => 'spouse'), 'text' => $data['PidTagSpouseName'], ); } // Children foreach ((array) $data['PidTagChildrensNames'] as $child) { $result['related'][] = array( 'parameters' => array('type' => 'child'), 'text' => $child, ); } // Emails foreach ($this->email_map as $mapi_idx => $type) { if ($email = $data[$mapi_idx]) { $result['email'][] = array( 'parameters' => array('type' => $type), 'text' => $email, ); } } // Phone(s) foreach ($this->phone_map as $mapi_idx => $type) { if (array_key_exists($mapi_idx, $data)) { // first remove the old phone... if (!empty($object['tel'])) { foreach ($object['tel'] as $idx => $phone) { $pt = implode('', (array) $phone['parameters']['type']); if ($pt == $type) { unset($object['tel'][$idx]); } } } if ($tel = $data[$mapi_idx]) { if (preg_match('/^fax(work|home)$/', $type, $m)) { $type = array('fax', $m[1]); } // and add it to the list $result['tel'][] = array( 'parameters' => array('type' => $type), 'text' => $tel, ); } } } if (!empty($object['tel'])) { $result['tel'] = array_merge((array) $result['tel'], (array) $object['tel']); } // Home address $address = array(); $adr_map = array( 'PidTagHomeAddressStreet' => 'street', 'PidTagHomeAddressCity' => 'locality', 'PidTagHomeAddressStateOrProvince' => 'region', 'PidTagHomeAddressPostalCode' => 'code', 'PidTagHomeAddressCountry' => 'country', 'PidTagHomeAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'home')); $result['adr'][] = array_merge($address, $type); } // Work address $address = array(); $adr_map = array( 'PidLidWorkAddressStreet' => 'street', 'PidLidWorkAddressCity' => 'locality', 'PidLidWorkAddressState' => 'region', 'PidLidWorkAddressPostalCode' => 'code', 'PidLidWorkAddressCountry' => 'country', 'PidLidWorkAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $type = array('parameters' => array('type' => 'work')); $result['adr'][] = array_merge($address, $type); } // Office address $address = array(); $adr_map = array( 'PidTagOtherAddressStreet' => 'street', 'PidTagOtherAddressCity' => 'locality', 'PidTagOtherAddressStateOrProvince' => 'region', 'PidTagOtherAddressPostalCode' => 'code', 'PidTagOtherAddressCountry' => 'country', 'PidTagOtherAddressPostOfficeBox' => 'pobox', ); foreach ($adr_map as $mapi_idx => $idx) { if ($adr = $data[$mapi_idx]) { $address[$idx] = $adr; } } if (!empty($address)) { $result['group']['adr'] = array_merge($address, $type); } kolab_api_filter_mapistore::convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/event.php b/lib/filter/mapistore/event.php index cfa172b..1aa6133 100644 --- a/lib/filter/mapistore/event.php +++ b/lib/filter/mapistore/event.php @@ -1,797 +1,798 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_event { protected $map = array( // common properties [MS-OXOCAL] 'PidLidAppointmentSequence' => 'sequence', // PtypInteger32 'PidLidBusyStatus' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-BUSYSTATUS 'PidLidAppointmentAuxiliaryFlags' => '', // PtypInteger32 'PidLidLocation' => 'location', // PtypString 'PidLidAppointmentStartWhole' => 'dtstart', // PtypTime, UTC 'PidLidAppointmentEndWhole' => 'dtend', // PtypTime, UTC 'PidLidAppointmentDuration' => '', // PtypInteger32, optional 'PidLidAppointmentSubType' => '', // PtypBoolean 'PidLidAppointmentStateFlags' => '', // PtypInteger32 'PidLidResponseStatus' => '', // PtypInteger32 'PidLidRecurring' => '', // PtypBoolean 'PidLidIsRecurring' => '', // PtypBoolean 'PidLidClipStart' => '', // PtypTime 'PidLidClipEnd' => '', // PtypTime 'PidLidAllAttendeesString' => '', // PtypString 'PidLidToAttendeesString' => '', // PtypString 'PidLidCCAttendeesString' => '', // PtypString 'PidLidNonSendableTo' => '', // PtypString 'PidLidNonSendableCc' => '', // PtypString 'PidLidNonSendableBcc' => '', // PtypString 'PidLidNonSendToTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendCcTrackStatus' => '', // PtypMultipleInteger32 'PidLidNonSendBccTrackStatus' => '', // PtypMultipleInteger32 'PidLidAppointmentUnsendableRecipients' => '', // PtypBinary, optional 'PidLidAppointmentNotAllowPropose' => '', // PtypBoolean, @TODO: X-MICROSOFT-CDO-DISALLOW-COUNTER 'PidLidGlobalObjectId' => '', // PtypBinary 'PidLidCleanGlobalObjectId' => '', // PtypBinary 'PidTagOwnerAppointmentId' => '', // PtypInteger32, @TODO: X-MICROSOFT-CDO-OWNERAPPTID 'PidTagStartDate' => '', // PtypTime 'PidTagEndDate' => '', // PtypTime 'PidLidCommonStart' => '', // PtypTime 'PidLidCommonEnd' => '', // PtypTime 'PidLidOwnerCriticalChange' => '', // PtypTime, @TODO: X-MICROSOFT-CDO-CRITICAL-CHANGE 'PidLidIsException' => '', // PtypBoolean 'PidTagResponseRequested' => '', // PtypBoolean 'PidTagReplyRequested' => '', // PtypBoolean 'PidLidTimeZoneStruct' => '', // PtypBinary 'PidLidTimeZoneDescription' => '', // PtypString 'PidLidAppointmentTimeZoneDefinitionRecur' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionStartDisplay' => '', // PtypBinary 'PidLidAppointmentTimeZoneDefinitionEndDisplay' => '', // PtypBinary 'PidLidAppointmentRecur' => '', // PtypBinary 'PidLidRecurrenceType' => '', // PtypInteger32 'PidLidRecurrencePattern' => '', // PtypString 'PidLidLinkedTaskItems' => '', // PtypMultipleBinary 'PidLidMeetingWorkspaceUrl' => '', // PtypString 'PidTagIconIndex' => '', // PtypInteger32 'PidLidAppointmentColor' => '', // PtypInteger32 'PidLidAppointmentReplyTime' => '', // @TODO: X-MICROSOFT-CDO-REPLYTIME 'PidLidIntendedBusyStatus' => '', // @TODO: X-MICROSOFT-CDO-INTENDEDSTATUS // calendar object properties [MS-OXOCAL] 'PidTagMessageClass' => '', 'PidLidSideEffects' => '', // PtypInteger32 'PidLidFExceptionAttendees' => '', // PtypBoolean 'PidLidClientIntent' => '', // PtypInteger32 // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagContentId' => '', 'PidTagBodyContentLocation' => '', 'PidTagImportance' => 'priority', 'PidTagSensitivity' => 'class', 'PidLidPrivate' => '', 'PidTagCreationTime' => 'created', 'PidTagLastModificationTime' => 'dtstamp', // reminder properties [MS-OXORMDR] 'PidLidReminderSet' => '', // PtypBoolean 'PidLidReminderSignalTime' => '', // PtypTime 'PidLidReminderDelta' => '', // PtypInteger32 'PidLidReminderTime' => '', // PtypTime 'PidLidReminderOverride' => '', // PtypBoolean 'PidLidReminderPlaySound' => '', // PtypBoolean 'PidLidReminderFileParameter' => '', // PtypString 'PidTagReplyTime' => '', // PtypTime 'PidLidReminderType' => '', // PtypInteger32 ); protected $recipient_track_status_map = array( 'TENTATIVE' => 0x00000002, 'ACCEPTED' => 0x00000003, 'DECLINED' => 0x00000004, ); protected $recipient_type_map = array( 'NON-PARTICIPANT' => 0x00000004, 'OPT-PARTICIPANT' => 0x00000002, 'REQ-PARTICIPANT' => 0x00000002, 'CHAIR' => 0x00000001, ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 0 => 0x00000000, 1 => 0x00000002, 2 => 0x00000002, 3 => 0x00000002, 4 => 0x00000002, 5 => 0x00000001, 6 => 0x00000000, 7 => 0x00000000, 8 => 0x00000000, 9 => 0x00000000, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'public' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Mapping of weekdays */ protected static $recurrence_day_map = array( 'SU' => 0x00000000, 'MO' => 0x00000001, 'TU' => 0x00000002, 'WE' => 0x00000003, 'TH' => 0x00000004, 'FR' => 0x00000005, 'SA' => 0x00000006, 'BYDAY-SU' => 0x00000001, 'BYDAY-MO' => 0x00000002, 'BYDAY-TU' => 0x00000004, 'BYDAY-WE' => 0x00000008, 'BYDAY-TH' => 0x00000010, 'BYDAY-FR' => 0x00000020, 'BYDAY-SA' => 0x00000040, ); /** * 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( 'PidTagMessageClass' => 'IPM.Appointment', // mapistore REST API specific properties 'collection' => 'calendars', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidTagSensitivity': $value = (int) $this->sensitivity[strtolower($value)]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; case 'PidTagImportance': $value = (int) $this->importance[(int) $value]; break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': $dt = kolab_api_input_json::to_datetime($value); $value = kolab_api_filter_mapistore::date_php2mapi($dt, true); // PidLidAppointmentTimeZoneDefinitionStartDisplay // PidLidAppointmentTimeZoneDefinitionEndDisplay // this is all-day event if ($dt->_dateonly) { $result['PidLidAppointmentSubType'] = 0x00000001; } break; } $result[$mapi_idx] = $value; } // Organizer if (!empty($data['organizer'])) { $this->add_attendee_to_result($data['organizer'], $result, true); } // Attendees [MS-OXCICAL 2.1.3.1.1.20.2] foreach ((array) $data['attendee'] as $attendee) { $this->add_attendee_to_result($attendee, $result); } // Alarms (MAPI supports only one) foreach ((array) $data['valarm'] as $alarm) { if ($alarm['properties'] && $alarm['properties']['action'] == 'DISPLAY' && ($duration = $alarm['properties']['trigger']['duration']) && ($delta = self::reminder_duration_to_delta($duration)) ) { $result['PidLidReminderDelta'] = $delta; $result['PidLidReminderSet'] = true; // PidLidReminderTime // PidLidReminderSignalTime break; } } // @TODO: PidLidAppointmentDuration // @TODO: exceptions, resources // Recurrence rule if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { if ($rule = self::recurrence_from_kolab($data['rrule']['recur'], $result)) { $result['PidLidAppointmentRecur'] = $rule; } } kolab_api_filter_mapistore::parse_common_props($result, $data, $context); 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) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagImportance': $map = array( 0x00000002 => 1, 0x00000001 => 5, 0x00000000 => 9, ); $value = (int) $map[(int) $value]; break; case 'PidTagSensitivity': $map = array_flip($this->sensitivity); $value = $map[$value]; break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; case 'PidLidAppointmentStartWhole': case 'PidLidAppointmentEndWhole': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $format = $data['PidLidAppointmentSubType'] ? 'Y-m-d' : 'Y-m-d\TH:i:s\Z'; $value = $value->format($format); } break; } $result[$kolab_idx] = $value; } // Alarms (MAPI supports only one, DISPLAY) if ($data['PidLidReminderSet'] && ($delta = $data['PidLidReminderDelta'])) { $duration = self::reminder_delta_to_duration($delta); $alarm = array( 'action' => 'DISPLAY', 'trigger' => array('duration' => $duration), 'description' => 'Reminder', ); $result['valarm'] = array(array('properties' => $alarm)); } else if (array_key_exists('PidLidReminderSet', $data) || array_key_exists('PidLidReminderDelta', $data)) { $result['valarm'] = array(); } // Recurrence if (array_key_exists('PidLidAppointmentRecur', $data)) { $result['rrule']['recur'] = $this->recurrence_to_kolab($data['PidLidAppointmentRecur']); } // @TODO: PidLidAppointmentDuration (?) // @TODO: exceptions, resources, attendees kolab_api_filter_mapistore::convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); // @TODO: add properties that are not in the map $map['PidLidAppointmentRecur'] = 'rrule'; return $map; } /** * Setting PidTagRecipientType according to [MS-OXCICAL 2.1.3.1.1.20.2] */ protected function to_recipient_type($cutype, $role) { if ($cutype && in_array($cutype, array('RESOURCE', 'ROOM'))) { return 0x00000003; } if ($role && ($type = $this->recipient_type_map[$role])) { return $type; } return 0x00000001; } /** * Convert Kolab 'attendee' specification into MAPI recipient * and add it to the result */ protected function add_attendee_to_result($attendee, &$result, $is_organizer = false) { $email = $attendee['cal-address']; $params = (array) $attendee['parameters']; // parse mailto string if (strpos($email, 'mailto:') === 0) { $email = urldecode(substr($email, 7)); } $emails = rcube_mime::decode_address_list($email, 1); if (!empty($email)) { $email = $emails[key($emails)]; $recipient = array( 'PidTagAddressType' => 'SMTP', 'PidTagDisplayName' => $params['cn'] ?: $email['name'], 'PidTagDisplayType' => 0, 'PidTagEmailAddress' => $email['mailto'], ); if ($is_organizer) { $recipient['PidTagRecipientFlags'] = 0x00000003; $recipient['PidTagRecipientType'] = 0x00000001; } else { $recipient['PidTagRecipientFlags'] = 0x00000001; $recipient['PidTagRecipientTrackStatus'] = (int) $this->recipient_track_status_map[$params['partstat']]; $recipient['PidTagRecipientType'] = $this->to_recipient_type($params['cutype'], $params['role']); } // PidTagEntryId // PidTagRecipientEntryId $recipient['PidTagRecipientDisplayName'] = $recipient['PidTagDisplayName']; $result['recipients'][] = $recipient; if (strtoupper($params['rsvp']) == 'TRUE') { $result['PidTagReplyRequested'] = true; $result['PidTagResponseRequested'] = true; } } } /** * Convert PidLidReminderDelta value into xCal duration */ protected static function reminder_delta_to_duration($delta) { if ($delta == 0x5AE980E1) { $delta = 15; } $delta = (int) $delta; return "-PT{$delta}M"; } /** * Convert Kolab alarm duration into PidLidReminderDelta */ protected static function reminder_duration_to_delta($duration) { if ($duration && preg_match('/^-[PT]*([0-9]+)([WDHMS])$/', $duration, $matches)) { $value = intval($matches[1]); switch ($matches[2]) { case 'S': $value = intval(round($value/60)); break; case 'H': $value *= 60; break; case 'D': $value *= 24 * 60; break; case 'W': $value *= 7 * 24 * 60; break; } return $value; } } /** * Convert Kolab recurrence specification into MAPI properties * * @param array $rule Recurrence rule in Kolab format * @param array $type Object data (MAPI format) * @param string $type Object type (event, task) * * @return object MAPI recurrence in binary format */ public static function recurrence_from_kolab($rule, $object = array(), $type = 'event') { $result = array( 'Period' => $rule['interval'] ? $rule['interval'] : 1, 'FirstDOW' => self::day2bitmask($rule['wkst'] ?: 'MO'), 'OccurrenceCount' => 0x0000000A, 'EndDate' => 0x5AE980DF, 'CalendarType' => kolab_api_filter_mapistore_structure_recurrencepattern::CALENDARTYPE_DEFAULT, // DeletedInstanceDates // ModifiedInstanceDates ); // Get event/task start date for FirstDateTime calculations if ($object['PidLidAppointmentStartWhole']) { $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidAppointmentStartWhole']); $result['StartDate'] = intval($object['PidLidAppointmentStartWhole'] / 10000000 / 60); } else if ($object['PidLidCommonStart']) { $startdate = kolab_api_filter_mapistore::date_mapi2php($object['PidLidCommonStart']); $result['StartDate'] = intval($object['PidLidCommonStart'] / 10000000 / 60); } else { rcube::raise_error(array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Found recurring $type without start date, skipping recurrence", ), true, false); return; } // $startdate->setTime(0, 0, 0); // @TODO: // StartDate: Set to the date portion of DTSTART, in the time zone specified // by PidLidTimeZoneStruct. This date is stored in minutes after // midnight Jan 1, 1601. Note that this value MUST always be // evenly divisible by 1440. // EndDate: Set to the start date of the last instance of a recurrence, in the // time zone specified by PidLidTimeZoneStruct. This date is // stored in minutes after midnight January 1, 1601. If the // recurrence is infinite, set EndDate to 0x5AE980DF. Note that // this value MUST always be evenly divisible by 1440, except for // the special value 0x5AE980DF. // @TODO: get first occurrence of the event using libcalendaring_recurrence class ? switch ($rule['freq']) { case 'DAILY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_DAILY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY; $result['Period'] *= 1440; break; case 'WEEKLY': // if BYDAY does not exist use day from DTSTART if (empty($rule['byday'])) { $rule['byday'] = strtoupper($startdate->format('S')); } $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_WEEKLY; $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK; $result['PatternTypeSpecific'] = self::day2bitmask($rule['byday'], 'BYDAY-'); break; case 'MONTHLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_MONTHLY; if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH; $result['PatternTypeSpecific'] = $month_day == -1 ? 0x0000001F : $month_day; } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; case 'YEARLY': $result['RecurFrequency'] = kolab_api_filter_mapistore_structure_recurrencepattern::RECURFREQUENCY_YEARLY; $result['Period'] *= 12; // MAPI doesn't support multi-valued months if ($rule['bymonth']) { // @TODO: set $startdate } if (!empty($rule['bymonthday'])) { // MAPI doesn't support multi-valued month days $month_day = min(explode(',', $rule['bymonthday'])); $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'] = array(0, $month_day == -1 ? 0x0000001F : $month_day); } else { $result['PatternType'] = kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH; $result['PatternTypeSpecific'][] = self::day2bitmask($rule['byday'], 'BYDAY-'); if (!empty($rule['bysetpos'])) { $result['PatternTypeSpecific'][] = $rule['bysetpos'] == -1 ? 0x00000005 : $rule['bysetpos']; } } break; } if (!empty($rule['until'])) { $result['EndDate'] = intval(kolab_api_filter_mapistore::date_php2mapi($rule['until']) / 10000000 / 60); // @TODO: calculate OccurrenceCount? $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER; } else if (!empty($rule['count'])) { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC; $result['OccurrenceCount'] = $rule['count']; // @TODO: set EndDate } else { $result['EndType'] = kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NEVER; } $result['FirstDateTime'] = self::date_minutes_diff($startdate); $result = new kolab_api_filter_mapistore_structure_recurrencepattern($result); if ($type == 'task') { return $result->output(true); } // @TODO: exceptions $byhour = $rule['byhour'] ? min(explode(',', $rule['byhour'])) : 0; $byminute = $rule['byminute'] ? min(explode(',', $rule['byminute'])) : 0; $offset = 60 * intval($byhour) + intval($byminute); $arp = array( 'RecurrencePattern' => $result, 'StartTimeOffset' => $offset, 'EndTimeOffset' => $offset + $object['PidLidAppointmentDuration'], // ExceptionInfo // ExtendedExceptions ); $result = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern($arp); return $result->output(true); } /** * Convert MAPI recurrence into Kolab (MS-OXICAL: 2.1.3.2.2) * * @param string $rule MAPI binary representation of recurrence rule * @param string $type Object type (task, event) * * @return array Recurrence rule in Kolab format */ public static function recurrence_to_kolab($rule, $type = 'event') { if (empty($rule)) { return array(); } // parse binary (Appointment)RecurrencePattern if ($type == 'event') { $arp = new kolab_api_filter_mapistore_structure_appointmentrecurrencepattern(); $arp->input($rule, true); $rp = $arp->RecurrencePattern; } else { $rp = new kolab_api_filter_mapistore_structure_recurrencepattern(); $rp->input($rule, true); } $result = array( 'interval' => $rp->Period, ); switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_DAY: $result['freq'] = 'DAILY'; $result['interval'] /= 1440; if ($arp) { $result['byhour'] = floor($arp->StartTimeOffset / 60); $result['byminute'] = $arp->StartTimeOffset - $result['byhour'] * 60; } break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_WEEK: $result['freq'] = 'WEEKLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific); if ($rp->Period >= 1) { $result['wkst'] = self::bitmask2day($rp->FirstDOW); } break; default: // monthly/yearly $evenly_divisible = $rp->Period % 12 == 0; switch ($rp->PatternType) { case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHEND: $result['freq'] = $evenly_divisible ? 'YEARLY' : 'MONTHLY'; break; case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_MONTHNTH: case kolab_api_filter_mapistore_structure_recurrencepattern::PATTERNTYPE_HJMONTHNTH: $result['freq'] = $evenly_divisible ? 'YEARLY-NTH' : 'MONTHLY-NTH'; break; default: // not-supported return; } if ($result['freq'] = 'MONTHLY') { $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); } else if ($result['freq'] = 'MONTHLY-NTH') { $result['freq'] = 'MONTHLY'; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } else if ($result['freq'] = 'YEARLY') { $result['interval'] /= 12; $rule['bymonthday'] = intval($rp->PatternTypeSpecific == 0x0000001F ? -1 : $rp->PatternTypeSpecific); $rule['bymonth'] = 0;// @TODO: month from FirstDateTime } else if ($result['freq'] = 'YEARLY-NTH') { $result['freq'] = 'YEARLY'; $result['interval'] /= 12; $result['byday'] = self::bitmask2day($rp->PatternTypeSpecific[0]); $result['bymonth'] = 0;// @TODO: month from FirstDateTime if ($rp->PatternTypeSpecific[1]) { $result['bysetpos'] = intval($rp->PatternTypeSpecific[1] == 0x00000005 ? -1 : $rp->PatternTypeSpecific[1]); } } } if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_AFTER) { // @TODO: set UNTIL to EndDate + StartTimeOffset, or the midnight of EndDate } else if ($rp->EndType == kolab_api_filter_mapistore_structure_recurrencepattern::ENDTYPE_NOCC) { $result['count'] = $rp->OccurrenceCount; } if ($result['interval'] == 1) { unset($result['interval']); } return $result; } /** * Converts string of days (TU,TH) to bitmask used by MAPI * * @param string $days * * @return int */ protected static function day2bitmask($days, $prefix = '') { $days = explode(',', $days); $result = 0; foreach ($days as $day) { $result = $result + self::$recurrence_day_map[$prefix.$day]; } return $result; } /** * Convert bitmask used by MAPI to string of days (TU,TH) * * @param int $days * * @return string */ protected static function bitmask2day($days) { $days_arr = array(); foreach (self::$recurrence_day_map as $day => $bit) { if ($days & $bit) { $days_arr[] = preg_replace('/^[A-Z-]+/', '', $day); } } $result = implode(',', $days_arr); return $result; } /** * Returns number of minutes between midnight 1601-01-01 * and specified UTC DateTime */ protected static function date_minutes_diff($date) { $start = new DateTime('1601-01-01 00:00:00 +00:00'); // make sure the specified date is in UTC $date->setTimezone(new DateTimeZone('UTC')); return round(($date->getTimestamp() - $start->getTimestamp()) * 60); } } diff --git a/lib/filter/mapistore/folder.php b/lib/filter/mapistore/folder.php index cadb8fc..61fda5b 100644 --- a/lib/filter/mapistore/folder.php +++ b/lib/filter/mapistore/folder.php @@ -1,167 +1,168 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_folder { protected $map = array( // read-only properties 'PidTagAccess' => '', 'PidTagChangeKey' => '', 'PidTagCreationTime' => '', // PtypTime, @TODO: store in folder annotation? 'PidTagLastodificationTime' => '', // PtypTime 'PidTagContentCount' => '', // PtypInteger32 'PidTagContentUnreadCount' => '', // PtypInteger32 'PidTagDeletedOn' => '', // PtypTime // 'PidTagAddressbookEntryId' => '', // PtypBinary 'PidTagFolderId' => '', // PtypInteger64 'PidTagHierarchyChangeNumber' => '', // PtypInteger32, number of subfolders 'PidTagMessageSize' => '', // PtypInteger32, size of all messages 'PidTagMessageSizeExtended' => '', // PtypInteger64 'PidTagSubfolders' => '', // PtypBoolean 'PidTagLocalCommitTime' => '', // PtypTime, last change time in UTC 'PidTagLocalCommitTimeMax' => '', // PtypTime 'PidTagDeletedCountTotal' => '', // PtypInteger32 // read-write properties 'PidTagAttributeHidden' => '', // Ptypboolean 'PidTagComment' => '', // PtypString, @TODO: store in folder annotation? 'PidTagContainerClass' => 'type', // PtypString, IPF.* 'PidTagContainerHierarchy' => '', // PtypObject 'PidTagDisplayName' => 'name', // PtypString 'PidTagFolderAssociatedContents' => '', // PtypObject 'PidTagFolderType' => '', // PtypInteger32, 0 - namespace roots, 1 - other, 2 - virtual/search folders 'PidTagRights' => '', // PtypInteger32 'PidTagAccessControlListData' => '', // PtypBinary, see [MS-OXCPERM] ); protected $type_map = array( '' => 'IPF.Note', 'mail' => 'IPF.Note', 'task' => 'IPF.Task', 'note' => 'IPF.StickyNote', 'event' => 'IPF.Appointment', 'journal' => 'IPF.Journal', 'contact' => 'IPF.Contact', ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { list($type, ) = explode('.', $data['type']); $type = $this->type_map[(string)$type]; // skip folders of unsupported type if (empty($type)) { return; } // skip folders that are not subfolders of the specified folder, // in list-mode MAPI always requests for one-level of the hierarchy (?) if ($api->input->path[1] == 'folders') { $api = kolab_api::get_instance(); $parent = !empty($api->input->path) ? $api->input->path[0] : ''; if ($data['parent'] != $parent) { return; } } $result = array( // mapistore properties 'id' => $data['uid'], // MAPI properties 'PidTagFolderType' => 1, 'PidTagDisplayName' => $data['name'], 'PidTagContainerClass' => $type, ); if ($data['parent']) { $result['parent_id'] = $data['parent']; } $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) { $result = array(); // mapistore properties if ($data['id']) { $result['uid'] = $data['id']; } if ($data['parent_id']) { $result['parent'] = $data['parent_id']; } // MAPI properties if ($data['PidTagDisplayName']) { $result['name'] = $data['PidTagDisplayName']; } if ($data['PidTagContainerClass']) { // @TODO: what if folder is already a *.default or *.sentitems, etc. // we should keep the subtype intact $map = array_flip($this->type_map); $result['type'] = $map[$data['PidTagContainerClass']]; } return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['parent_id'] = 'parent'; $map['PidTagContainerClass'] = 'type'; $map['PidTagFolderType'] = 'PidTagFolderType'; return $map; } } diff --git a/lib/filter/mapistore/info.php b/lib/filter/mapistore/info.php index 9abb435..42f740e 100644 --- a/lib/filter/mapistore/info.php +++ b/lib/filter/mapistore/info.php @@ -1,87 +1,88 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_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 = 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; } } diff --git a/lib/filter/mapistore/mail.php b/lib/filter/mapistore/mail.php index 09d36e7..141a958 100644 --- a/lib/filter/mapistore/mail.php +++ b/lib/filter/mapistore/mail.php @@ -1,289 +1,451 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_mail { protected $map = array( - 'PidTagMessageClass' => '', - 'PidTagHasAttachments' => '', - 'PidTagMessageCodepage' => '', - 'PidTagMessageLocaleId' => '', - 'PidTagMessageFlags' => '', - 'PidTagMessageSize' => 'size', // size in bytes - 'PidTagMessageStatus' => '', - 'PidTagSubjectPrefix' => '', - 'PidTagNormalizedSubject' => '', - 'PidTagImportance' => '', - 'PidTagPriority' => 'priority', - 'PidTagSensitivity' => '', - 'PidLidSmartNoAttach' => '', - 'PidLidPrivate' => '', - 'PidLidSideEffects' => '', - 'PidNameKeywords' => '', // PtypMultipleString - 'PidLidCommonStart' => '', - 'PidLidCommonEnd' => '', - 'PidTagAutoForwarded' => '', - 'PidTagAutoForwardComment' => '', - 'PidTagCategories' => '', // PtypMultipleString - 'PidLidClassification' => '', - 'PidLidClassificationDescription' => '', - 'PidLidClassified' => '', - 'PidTagInternetReferences' => '', - 'PidLidInfoPathFormName' => '', - 'PidTagMimeSkeleton' => '', - 'PidTagTnefCorrelationKey' => '', - 'PidTagAddressBookDisplayName' => '', - 'PidTagCreatorEntryId' => '', - 'PidTagLastModifierEntryId' => '', - 'PidLidAgingDontAgeMe' => '', - 'PidLidCurrentVersion' => '', - 'PidLidCurrentVersionName' => '', - 'PidTagAlternateRecipientAllowed' => '', - 'PidTagResponsibility' => '', - 'PidTagRowid' => '', - 'PidTagHasNamedProperties' => '', - 'PidTagRecipientOrder' => '', - 'PidNameContentBase' => '', - 'PidNameAcceptLanguage' => '', - 'PidTagPurportedSenderDomain' => '', - 'PidTagStoreEntryId' => '', - 'PidTagTrustSender' => '', - 'PidTagSubject' => 'subject', - 'PidTagMessageRecipients' => '', - 'PidNameContentClass' => '', - 'PidTagLocalCommitTime' => '', - 'PidNameContentType' => '', - 'PidTagCreatorName' => '', - 'PidTagMessageAttachments' => '', - 'PidTagRead' => '', - 'PidTagRecipientDisplayName' => '', - 'PidTagRecipientEntryId' => '', + // MS-OXCMSG properties + 'PidTagMessageClass' => '', // PtypString + 'PidTagHasAttachments' => '', // PtypBoolean + 'PidTagMessageCodepage' => '', // PtypInteger32 + 'PidTagMessageLocaleId' => '', // PtypInteger32 + 'PidTagMessageFlags' => '', // PtypInteger32, @TODO + 'PidTagMessageSize' => 'size', // PtypInteger32, size in bytes + 'PidTagMessageStatus' => '', // PtypInteger32 + 'PidTagNormalizedSubject' => '', // PtypString + 'PidTagImportance' => '', // PtypInteger32 + 'PidTagPriority' => 'priority', // PtypInteger32, @TODO + 'PidTagSensitivity' => '', // PtypInteger32 + 'PidLidSmartNoAttach' => '', // PtypBoolean + 'PidLidPrivate' => '', // PtypBoolean + 'PidLidSideEffects' => '', // PtypInteger32 + 'PidLidCommonStart' => '', // PtypTime + 'PidLidCommonEnd' => '', // PtypTime + 'PidTagAutoForwarded' => '', // PtypBoolean + 'PidTagAutoForwardComment' => '', // PtypString + 'PidTagCategories' => '', // PtypMultipleString + 'PidLidClassification' => '', // PtypString + 'PidLidClassificationDescription' => '', // PtypString + 'PidLidClassified' => '', // PtypBoolean + 'PidTagInternetReferences' => '', // PtypString, @TODO + 'PidLidInfoPathFormName' => '', // PtypString + 'PidTagMimeSkeleton' => '', // PtypBinary + 'PidTagTnefCorrelationKey' => '', // PtypBinary + 'PidTagAddressBookDisplayNamePrintable' => '', // PtypString + 'PidTagCreatorEntryId' => '', // PtypBinary + 'PidTagLastModifierEntryId' => '', // PtypBinary + 'PidLidAgingDontAgeMe' => '', // PtypBoolean + 'PidLidCurrentVersion' => '', // PtypInteger32 + 'PidLidCurrentVersionName' => '', // PtypString, @TODO: User-Agent? + 'PidTagAlternateRecipientAllowed' => '', // PtypBoolean + 'PidTagResponsibility' => '', // PtypBoolean + 'PidTagRowid' => '', // PtypInteger32 + 'PidTagHasNamedProperties' => '', // PtypBoolean + 'PidTagRecipientOrder' => '', // PtypInteger32 + 'PidNameContentBase' => '', // PtypString, Content-Base header + 'PidNameAcceptLanguage' => '', // PtypString, Accept-Language header + 'PidTagPurportedSenderDomain' => '', // PtypString + 'PidTagStoreEntryId' => '', // PtypBinary + 'PidTagTrustSender' => '', // PtypInteger32 + 'PidTagSubject' => 'subject', // PtypString + 'PidTagMessageRecipients' => '', // PtypObject + 'PidNameContentClass' => '', // PtypString, @TODO + 'PidTagLocalCommitTime' => '', // PtypTime + 'PidNameContentType' => 'content-type', // PtypString, @TODO: Content-type + 'PidTagCreatorName' => '', // PtypString + 'PidTagMessageAttachments' => '', // PtypObject + 'PidTagRead' => '', // PtypBoolean, @TODO + 'PidTagRecipientDisplayName' => '', // PtypString + 'PidTagRecipientEntryId' => '', // PtypBinary // body properties - 'PidTagBody' => '', - 'PidTagNativeBody' => '', - 'PidTagBodyHtml' => '', - 'PidTagRtfCompressed' => '', - 'PidTagRtfInSync' => '', - 'PidTagInternetCodepage' => '', - 'PidTagBodyContentId' => '', - 'PidTagBodyContentLocation' => '', - 'PidTagHtml' => '', + 'PidTagBody' => 'text', // PtypString + 'PidTagNativeBody' => '', // PtypInteger32 + 'PidTagBodyHtml' => '', // PtypString + 'PidTagRtfCompressed' => '', // PtypBinary + 'PidTagRtfInSync' => '', // PtypBoolean + 'PidTagInternetCodepage' => '', // PtypInteger32 + 'PidTagBodyContentId' => '', // PtypString + 'PidTagBodyContentLocation' => '', // PtypString + 'PidTagHtml' => 'html', // PtypBinary // contact linking properties - 'PidLidContactLinkEntry' => '', - 'PidLidContacts' => '', - 'PidLidContactLinkName' => '', - 'PidLidContactLinkSearchKey' => '', + 'PidLidContactLinkEntry' => '', // PtypBinary + 'PidLidContacts' => '', // PtypMultipleStrings + 'PidLidContactLinkName' => '', // PtypString + 'PidLidContactLinkSearchKey' => '', // PtypBinary // retention and archive properties - 'PidTagArchiveTag' => '', - 'PidTagPolicyTag' => '', - 'PidTagRetentionPeriod' => '', - 'PidTagStartDateEtc' => '', - 'PidTagRetentionDate' => '', - 'PidTagRetentionFlags' => '', - 'PidTagArchivePeriod' => '', - 'PidTagArchiveDate' => '', + 'PidTagArchiveTag' => '', // PtypBinary + 'PidTagPolicyTag' => '', // PtypBinary + 'PidTagRetentionPeriod' => '', // PtypInteger32 + 'PidTagStartDateEtc' => '', // PtypBinary + 'PidTagRetentionDate' => '', // PtypTime + 'PidTagRetentionFlags' => '', // PtypInteger32 + 'PidTagArchivePeriod' => '', // PtypInteger32 + 'PidTagArchiveDate' => '', // PtypTime // MS-OXOMSG properties - 'PidTagInternetMessageId' => 'message-id', - 'PidTagInReplyToId' => 'in-reply-to', + 'PidTagBlockStatus' => '', // PtypInteger32 + 'PidTagConversationId' => '', // PtypBinary, @TODO + 'PidTagConversationindex' => '', // PtypBinary + 'PidTagConversationindexTracking' => '', // PtypBoolean + 'PidTagConversationTopic' => '', // PtypString + 'DeferredDeliveryTime' => '', // PtypTime + 'PidTagDisplayBcc' => '', // PtypString + 'PidTagDisplayCc' => '', // PtypString + 'PidTagDisplayTo' => '', // PtypString + 'PidTagIconIndex' => '', // PtypInteger32, @TODO + 'PidTagInternetMailOverrideFormat' => '', // PtypInteger32 + 'PidTagInternetMessageId' => 'message-id', // PtypString + 'PidTagInReplyToId' => 'in-reply-to', // PtypString, + 'PidTagLastVerbExecuted' => '', // PtypInteger32 + 'PidTagLastVerbExecutionTime' => '', // PtypTime + 'PidTagMessageToMe' => '', // PtypBoolean, @TODO + 'PidTagMessageCcMe' => '', // PtypBoolean, @TODO + 'PidTagMessageRecipientMe' => '', // PtypBoolean, @TODO + 'PidTagOriginatorDeliveryReportRequested' => '', // PtypBoolean, @TODO + 'PidTagOriginatorNonDeliveryReportRequested' => '', // PtypBoolean + 'PidTagOriginalSensitivity' => '', // PtypInteger32 + 'PidTagReceivedRepresentingAddressType' => '', // PtypString + 'PidTagReceivedRepresentingEmailAddress' => '', // PtypString + 'PidTagReceivedRepresentingEntryId' => '', // PtypBinary + 'PidTagReceivedRepresentingName' => '', // PtypString + 'PidTagReceivedRepresentingSearchKey' => '', // PtypBinary + 'PidTagReceivedRepresentingSmtpAddress' => '', // PtypString + 'PidTagReadReceiptRequested' => '', // PtypBoolean, @TODO + 'PidTagReadReceiptSmtpAddress' => '', // PtypString, @TODO + 'PidTagNonReceiptNotificationRequested' => '', // PtypBoolean + 'PidTagOriginalAuthorEntryId' => '', // PtypBinary + 'PidTagOriginalAuthorName' => '', // PtypString + 'PidTagReportDisposition' => '', // PtypString, @TODO + 'PidTagReportDispositionMode' => '', // PtypString, @TODO + 'PidTagReceivedByAddressType' => '', // PtypString + 'PidTagReceivedByEmailAddress' => '', // PtypString + 'PidTagReceivedByEntryId' => '', // PtypBinary + 'PidTagReceivedBySearchKey' => '', // PtypBinary + 'PidTagReceivedByName' => '', // PtypString + 'PidTagReceivedBySmtpAddress' => '', // PtypString + 'PidTagRecipientReassignmentProhibited' => '', // PtypBoolean + 'PidTagReplyRecipientEntries' => '', // PtypBinary + 'PidTagReplyRecipientNames' => '', // PtypString + 'PidTagReplyRequested' => '', // PtypBoolean + 'PidTagResponseRequested' => '', // PtypBoolean + 'PidTagSendRichInfo' => '', // PtypBoolean + 'PidTagSenderAddressType' => '', // PtypString, @TODO + 'PidTagSenderEmailAddress' => '', // PtypString + 'PidTagSenderEntryId' => '', // PtypBinary + 'PidTagSenderSearchKey' => '', // PtypBinary + 'PidTagSenderName' => '', // PtypString + 'PidTagSenderSmtpAddress' => '', // PtypString + 'PidTagSentRepresentingAddressType' => '', // PtypString, @TODO + 'PidTagSentRepresentingEmailAddress' => '', // PtypString + 'PidTagSentRepresentingEntryId' => '', // PtypBinary + 'PidTagSentRepresentingSearchKey' => '', // PtypBinary + 'PidTagSentRepresentingName' => '', // PtypString + 'PidTagSentRepresentingSmtpAddress' => '', // PtypString + 'PidTagSubjectPrefix' => '', // PtypString + 'PidTagTransportMessageHeaders' => '', // PtypString + 'PidLidInternetAccountName' => '', // PtypString + 'PidLidInternetAccountStamp' => '', // PtypString + 'PidTagPrimarySendAccount' => '', // PtypString + 'PidTagNextSendAcct' => '', // PtypString + 'PidLidUseTnef' => '', // PtypBoolean + 'PidLidAutoProcessState' => '', // PtypInteger32 + 'PidLidVerbStream' => '', // PtypBinary + 'PidLidVerbResponse' => '', // PtypString + 'PidTagTargetEntryId' => '', // PtypBinary + 'PidTagAutoResponseSuppress' => '', // PtypInteger32 + 'PidTagMessageEditorFormat' => '', // PtypInteger32 + 'PidTagMessageSubmissionId' => '', // PtypBinary + 'PidTagSenderIdStatus' => '', // PtypInteger32 + 'PidTagListHelp' => '', // PtypString, List-Help header + 'PidTagListSubscribe' => '', // PtypString, List-Subscribe header + 'PidTagListUnsubscribe' => '', // PtypString, List-Unsubscribe header + 'PidTagDelegatedByRule' => '', // PtypBoolean + 'PidTagOriginalMessageId' => '', // PtypString, @TODO + 'PidTagOriginalMessageClass' => '', // PtypString + // @TODO MS-OXOMSG 2.2.2 Message Status Reports + // @TODO MS-OXOFLAG ); /** * Message flags for PidTagMessageFlags as defined in [MS-OXCMSG] */ protected $flags = array( 'mfRead' => 0x00000001, 'mfUnsent' => 0x00000008, 'mfResend' => 0x00000080, 'mfUnmodified' => 0x00000002, 'mfSubmitted' => 0x00000004, 'mfHasAttach' => 0x00000010, 'mfFromMe' => 0x00000020, 'mfFAI' => 0x00000040, 'mfNotifyRead' => 0x00000100, 'mfNotifyUnread' => 0x00000200, 'mfEventRead' => 0x00000400, 'mfInternet' => 0x00002000, 'mfUntrusted' => 0x00008000, ); /** * Message status for PidTagMessageStatus as defined in [MS-OXCMSG] */ protected $status = array( 'msRemoteDownload' => 0x00001000, 'msInConflict' => 0x00000800, 'msRemoteDelete' => 0x00002000, ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 'low' => 0x00000000, 'normal' => 0x00000001, 'high' => 0x00000002, ); /** * Message priority for PidTagPriority as defined in [MS-OXCMSG] */ protected $priority = array( 'urgent' => 0x00000001, 'normal' => 0x00000000, 'not-urgent' => 0xFFFFFFFF, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'normal' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Recipient type for PidTagRecipientType as defined in [MS-OXOMSG] */ protected $recipient_types = array( 'to' => 0x00000001, 'cc' => 0x00000002, 'bcc' => 0x00000003, ); + protected $body_types = array( + 'plain' => 0x00000001, + 'rtf' => 0x00000002, + 'html' => 0x00000003, + 'signed' => 0x00000004, + ); + /** * 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( 'PidTagMessageClass' => 'IPM.Note', // mapistore REST API specific properties 'collection' => 'mails', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); - switch ($kolab_idx) { + switch ($mapi_idx) { case 'PidTagInternetMessageId': - $value = trim($value, '<>'); + case 'PidTagInReplyToId': + if ($value) { + $value = trim($value, '<>'); + } + break; + + case 'PidTagHtml': + if ($value) { + $value = base64_encode($value); + } + break; + + case 'PidTagPriority': + if ($value == 1 || $value == 2) { + $value = $this->priority['urgent']; + } + else if ($value > 3) { + $value = $this->priority['not-urgent']; + } + else { + $value = null; + } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // Recipients (To, Cc, Bcc) foreach (array('to', 'cc', 'bcc') as $idx) { foreach ((array) $data[$idx] as $address) { // @TODO: PidTagEntryId, PidTagEmailAddress if ($address['address']) { $recipient = array( 'PidTagSmtpAddress' => $address['address'], 'PidTagAddressType' => 'EX', 'PidTagRecipientType' => $this->recipient_types[$idx], ); if ($address['name']) { $recipient['PidTagDisplayName'] = $address['name']; } $result['recipients'][] = $recipient; } } } + if ($data['html']) { + $result['PidTagNativeBody'] = $this->body_types['html']; + } + else { + $result['PidTagNativeBody'] = $this->body_types['plain']; + } + + // @TODO: PidTagHasAttachments + kolab_api_filter_mapistore::parse_common_props($result, $data, $context); 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) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { -/* - case 'PidLidNoteColor': - if (strlen($value)) { - $map = array_flip($this->color_map); - $value = isset($map[(int) $value]) ? $map[(int) $value] : ''; + case 'PidTagInternetMessageId': + case 'PidTagInReplyToId': + if ($value) { + $value = '<' . trim($value, '<>') . '>'; + } + break; + + case 'PidTagHtml': + if ($value) { + $value = base64_decode($value); + } + break; + + case 'PidTagPriority': + if ($value == $this->priority['urgent']) { + $value = 1; + } + else if ($value == $this->priority['not-urgent']) { + $value = 5; + } + else { + $value = null; } break; -*/ } $result[$kolab_idx] = $value; } - kolab_api_filter_mapistore::convert_common_props($result, $data); + // API supports only html and text, we convert RTF to HTML if needed + if ($data['PidTagRtfCompressed'] && empty($data['PidTagHtml']) && class_exists('rtf')) { + // The same class is used in kolab-syncroton + $rtf = new rtf(); + $rtf->loadrtf($data['PidTagRtfCompressed']); + + // @TODO: Conversion to HTML is broken, convert to text + $rtf->output('ascii'); + $rtf->parse(); + $result['text'] = trim($rtf->out); + } + + // Recipients (To, Cc, Bcc) + if (array_key_exists('recipients', $data)) { + $types = array_flip($this->recipient_types); + + foreach ($data['recipients'] as $recip) { + // @TODO: PidTagEntryId, PidTagEmailAddress + $address = $recip['PidTagSmtpAddress']; + $name = $recip['PidTagDisplayName']; + + if ($address && ($type = $types[$recip['PidTagRecipientType']])) { + $result[$type][] = array( + 'address' => $address, + 'name' => $name, + ); + } + } + } + + kolab_api_filter_mapistore::convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/note.php b/lib/filter/mapistore/note.php index 12ddef0..46ceb1e 100644 --- a/lib/filter/mapistore/note.php +++ b/lib/filter/mapistore/note.php @@ -1,139 +1,140 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_note { protected $map = array( // note specific props [MS-OXNOTE] 'PidLidNoteColor' => 'x-custom.MAPI:PidLidNoteColor', // PtypInteger32 'PidLidNoteHeight' => 'x-custom.MAPI:PidLidNoteHeight', // PtypInteger32 'PidLidNoteWidth' => 'x-custom.MAPI:PidLidNoteWidth', // PtypInteger32 'PidLidNoteX' => 'x-custom.MAPI:PidLidNoteX', // PtypInteger32 'PidLidNoteY' => 'x-custom.MAPI:PidLidNoteY', // PtypInteger32 // common props [MS-OXCMSG] 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagMessageClass' => '', 'PidTagSubject' => 'summary', 'PidTagNormalizedSubject' => '', // @TODO: abbreviated note body 'PidTagIconIndex' => '', // @TODO: depends on PidLidNoteColor ); protected $color_map = array( '0000FF' => 0x00000000, // blue '008000' => 0x00000001, // green 'FFC0CB' => 0x00000002, // pink 'FFFF00' => 0x00000003, // yellow 'FFFFFF' => 0x00000004, // white ); /** * 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( 'PidTagMessageClass' => 'IPM.StickyNote', // notes do not have attachments in MAPI // 'PidTagHasAttachments' => 0, // mapistore REST API specific properties 'collection' => 'notes', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidNoteColor': case 'PidLidNoteHeight': case 'PidLidNoteWidth': case 'PidLidNoteX': case 'PidLidNoteY': $value = (int) $value; break; } $result[$mapi_idx] = $value; } kolab_api_filter_mapistore::parse_common_props($result, $data, $context); 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) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; $result[$kolab_idx] = $value; } kolab_api_filter_mapistore::convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/filter/mapistore/structure.php b/lib/filter/mapistore/structure.php index bad99ca..ff75c5c 100644 --- a/lib/filter/mapistore/structure.php +++ b/lib/filter/mapistore/structure.php @@ -1,332 +1,333 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * MAPI structures handler */ class kolab_api_filter_mapistore_structure { protected $structure = array(); protected $data = array(); protected $lengths = array( 'BYTE' => 1, 'WORD' => 2, 'LONG' => 4, 'ULONG' => 4, 'SYSTEMTIME' => 16, ); /** * Class constructor * * @param array Structure data properties */ public function __construct($data = array()) { if (!empty($data)) { $this->data = $data; } } /** * Convert binary input into internal structure * * @param string $input Binary representation of the structure * @param bool $base64 Set to TRUE if the input is base64-encoded * @param object $parent Parent structure * * @return int Number of bytes read from the binary input */ public function input($input, $base64 = false, $parent = null) { if ($base64) { $input = base64_decode($input); } $input_length = strlen($input); $position = 0; $counter = 0; foreach ($this->structure as $idx => $struct) { $length = 0; $class = null; $is_array = false; $count = 1; switch ($struct['type']) { case 'EMPTY': continue 2; case 'STRING': $length = $struct['length'] ?: (int) $this->data[$struct['counter']]; break; case 'WSTRING': $length = $struct['length'] ?: ((int) $this->data[$struct['counter']]) * 2; break; case 'BYTE': case 'WORD': case 'LONG': case 'ULONG': case 'SYSTEMTIME': default: if (preg_match('/^(LONG|ULONG|WORD|BYTE)\[([0-9]*)\]$/', $struct['type'], $m)) { $is_array = true; $count = $m[2] ? $m[2] : (int) $this->data[$struct['counter']]; $struct['type'] = $m[1]; $length = $this->lengths[$struct['type']]; } else if (preg_match('/^(\[?)(kolab_api_[a-z_]+)\]?$/', $struct['type'], $m)) { $length = 0; $class = $m[2]; $is_array = !empty($m[1]); $count = $is_array ? (int) $this->data[$struct['counter']] : 1; } else { $length = $this->lengths[$struct['type']]; } } if ($length && $position >= $input_length) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Invalid MAPI structure for ' . get_class($this) )); } for ($i = 0; $i < $count; $i++) { if ($length) { $value = substr($input, $position, $length); $position += $length; } else { $value = null; } switch ($struct['type']) { case 'WSTRING': $value = rcube_charset::convert($value, 'UTF-16LE', RCUBE_CHARSET); // no-break case 'STRING': break; case 'BYTE': $value = ord($value); break; case 'WORD': $unpack = unpack('v', $value); $value = $unpack[1]; break; case 'LONG': $unpack = unpack('l', $value); $value = $unpack[1]; break; case 'ULONG': $unpack = unpack('V', $value); $value = $unpack[1]; break; case 'SYSTEMTIME': $structure = new kolab_api_filter_mapistore_structure_systemtime; $structure->input($value, false, $this, $is_array ? $i : null); $value = $structure; break; default: $structure = new $class; $position += $structure->input(substr($input, $position), false, $this, $is_array ? $i : null); $value = $structure; } if ($value !== null) { if ($is_array) { $this->data[$idx][] = $value; } else { $this->data[$idx] = $value; } } } } return $position; } /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { $output = ''; foreach ($this->structure as $idx => $struct) { if (!array_key_exists($idx, $this->data)) { if ($struct['counter'] && !$this->data[$struct['counter']]) { continue; } else if (!isset($struct['default']) && $struct['type'] !== 'STRING' && $struct['type'] !== 'EMPTY') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Missing property " . get_class($this) . "::$idx", )); } else { $value = $struct['default']; } } else { $value = $this->data[$idx]; } switch ($struct['type']) { case 'EMPTY': break; case 'WSTRING': $value = rcube_charset::convert($value, RCUBE_CHARSET, 'UTF-16LE'); // no-break case 'STRING': $output .= $value; break; case 'BYTE': $output .= chr((int) $value); break; case 'WORD': $output .= pack('v', $value); break; case 'LONG': $output .= pack('l', $value); break; case 'ULONG': $output .= pack('V', (int) $value); break; case 'SYSTEMTIME': if ($value instanceof kolab_api_filter_mapistore_structure_systemtime) { $output .= $value->output(); } else { $output .= pack('llll', 0, 0, 0, 0); } break; default: if (preg_match('/^(LONG|WORD|ULONG|BYTE)\[([0-9]*)\]$/', $struct['type'], $m)) { $count = $m[2] ? $m[2] : count((array) $value); for ($x = 0; $x < $count; $x++) { switch ($m[1]) { case 'BYTE': $output .= chr((int) $value[$x]); break; case 'WORD': $output .= pack('v', $value[$x]); break; case 'LONG': $output .= pack('l', $value[$x]); break; case 'ULONG': $output .= pack('V', $value[$x]); break; } } } else if (preg_match('/^\[?(kolab_api_[a-z_]+)\]?$/', $struct['type'], $m)) { $type = $m[1]; if (!is_array($value)) { $value = !empty($value) ? array($value) : array(); } foreach ($value as $v) { if (!($v instanceof $type)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Expected object of type $type" )); } $output .= $v->output(); } } } } if ($base64) { $output = base64_encode($output); } return $output; } /** * Sets class data item */ public function __set($name, $value) { if (!array_key_exists($name, $this->structure)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Invalid member of MAPI structure: ' . get_class($this) . '::' . $name )); } $this->data[$name] = $value; } /** * Gets class data item */ public function __get($name) { if (!array_key_exists($name, $this->structure)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Invalid member of MAPI structure: ' . get_class($this) . '::' . $name )); } return $this->data[$name]; } } diff --git a/lib/filter/mapistore/structure/appointmentrecurrencepattern.php b/lib/filter/mapistore/structure/appointmentrecurrencepattern.php index 1c7cfe4..1a4a5eb 100644 --- a/lib/filter/mapistore/structure/appointmentrecurrencepattern.php +++ b/lib/filter/mapistore/structure/appointmentrecurrencepattern.php @@ -1,68 +1,69 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * AppointmentRecurrencePattern structure definition according to MS-OXOCAL 2.2.1.44.5 */ class kolab_api_filter_mapistore_structure_appointmentrecurrencepattern extends kolab_api_filter_mapistore_structure { protected $structure = array( 'RecurrencePattern' => array('type' => 'kolab_api_filter_mapistore_structure_recurrencepattern'), 'ReaderVersion' => array('type' => 'ULONG', 'default' => 0x00003006), 'WriterVersion' => array('type' => 'ULONG', 'default' => 0x00003009), 'StartTimeOffset' => array('type' => 'ULONG'), 'EndTimeOffset' => array('type' => 'ULONG'), 'ExceptionCount' => array('type' => 'WORD'), 'ExceptionInfo' => array('type' => '[kolab_api_filter_mapistore_structure_exceptioninfo]', 'counter' => 'ExceptionCount'), 'ReservedBlock1Size' => array('type' => 'ULONG', 'default' => 0), 'ReservedBlock1' => array('type' => 'STRING', 'counter' => 'ReservedBlock1Size'), 'ExtendedException' => array('type' => '[kolab_api_filter_mapistore_structure_extendedexception]', 'counter' => 'ExceptionCount'), 'ReservedBlock2Size' => array('type' => 'ULONG', 'default' => 0), 'ReservedBlock2' => array('type' => 'STRING', 'counter' => 'ReservedBlock2Size'), ); /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { if (count($this->data['ExceptionInfo']) != count($this->ExtendedException)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'ExceptionInfo and ExtendedException need to be of the same size' )); } $this->data['ExceptionCount'] = count($this->data['ExceptionInfo']); $this->data['ReservedBlock1Size'] = strlen($this->data['ReservedBlock1']); $this->data['ReservedBlock2Size'] = strlen($this->data['ReservedBlock2']); return parent::output($base64); } } diff --git a/lib/filter/mapistore/structure/changehighlight.php b/lib/filter/mapistore/structure/changehighlight.php index 75fed18..bbd45c6 100644 --- a/lib/filter/mapistore/structure/changehighlight.php +++ b/lib/filter/mapistore/structure/changehighlight.php @@ -1,72 +1,73 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * ChangeHighlight structure definition according to MS-OXOCAL 2.2.1.44.3 */ class kolab_api_filter_mapistore_structure_changehighlight extends kolab_api_filter_mapistore_structure { protected $structure = array( 'ChangeHighlightSize' => array('type' => 'ULONG'), 'ChangeHighlightValue' => array('type' => 'ULONG'), 'Reserved' => array('type' => 'STRING'), ); /** * Convert binary input into internal structure * * @param string $input Binary representation of the structure * @param bool $base64 Set to TRUE if the input is base64-encoded * * @return int Number of bytes read from the binary input */ public function input($input, $base64 = false) { if ($base64) { $input = base64_decode($input); } // Read size $unpack = unpack('V', substr($input, 0, 4)); $value = $unpack[1]; $this->structure['Reserved']['length'] = $value - 4; return parent::input($input, false); } /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { $this->data['ChangeHighlightSize'] = strlen($this->data['Reserved']) + 4; return parent::output($base64); } } diff --git a/lib/filter/mapistore/structure/exceptioninfo.php b/lib/filter/mapistore/structure/exceptioninfo.php index 536d131..a9314e7 100644 --- a/lib/filter/mapistore/structure/exceptioninfo.php +++ b/lib/filter/mapistore/structure/exceptioninfo.php @@ -1,200 +1,201 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * ExceptionInfo structure definition according to MS-OXOCAL 2.2.1.44.2 */ class kolab_api_filter_mapistore_structure_exceptioninfo extends kolab_api_filter_mapistore_structure { protected $structure = array( 'StartDateTime' => array('type' => 'ULONG'), 'EndDateTime' => array('type' => 'ULONG'), 'OriginalStartDate' => array('type' => 'ULONG'), 'OverrideFlags' => array('type' => 'WORD'), 'SubjectLength' => array('type' => 'WORD'), 'SubjectLength2' => array('type' => 'WORD'), 'Subject' => array('type' => 'STRING', 'counter' => 'SubjectLength2'), 'MeetingType' => array('type' => 'ULONG'), 'ReminderDelta' => array('type' => 'ULONG'), 'ReminderSet' => array('type' => 'ULONG'), 'LocationLength' => array('type' => 'WORD'), 'LocationLength2' => array('type' => 'WORD'), 'Location' => array('type' => 'STRING', 'counter' => 'LocationLength2'), 'BusyStatus' => array('type' => 'ULONG'), 'Attachment' => array('type' => 'ULONG'), 'SubType' => array('type' => 'ULONG'), 'AppointmentColor' => array('type' => 'ULONG'), ); const OVERRIDEFLAGS_ARO_SUBJECT = 0x0001; const OVERRIDEFLAGS_ARO_MEETINGTYPE = 0x0002; const OVERRIDEFLAGS_ARO_REMINDERDELTA = 0x0004; const OVERRIDEFLAGS_ARO_REMINDER = 0x0008; const OVERRIDEFLAGS_ARO_LOCATION = 0x0010; const OVERRIDEFLAGS_ARO_BUSYSTATUS = 0x0020; const OVERRIDEFLAGS_ARO_ATTACHMENT = 0x0040; const OVERRIDEFLAGS_ARO_SUBTYPE = 0x0080; const OVERRIDEFLAGS_ARO_APPTCOLOR = 0x0100; const OVERRIDEFLAGS_ARO_EXCEPTIONAL_BODY = 0x0200; /** * Convert binary input into internal structure * * @param string $input Binary representation of the structure * @param bool $base64 Set to TRUE if the input is base64-encoded * * @return int Number of bytes read from the binary input */ public function input($input, $base64 = false) { if ($base64) { $input = base64_decode($input); } // Read OverrideFlags $unpack = unpack('v', substr($input, 12, 2)); $value = $unpack[1]; $this->data['OverrideFlags'] = $value; // modify structure according to OverrideFlags $this->set_structure(); return parent::input($input, false); } /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { $flags = 0; if ($this->data['Subject'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_SUBJECT; $length = strlen($this->data['Subject']); $this->data['SubjectLength2'] = $length; $this->data['SubjectLength'] = $length + 1; } if ($this->data['Location'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_LOCATION; $length = strlen($this->data['Location']); $this->data['LocationLength2'] = $length; $this->data['LocationLength'] = $length + 1; } if ($this->data['MeetingType'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_MEETINGTYPE; } if ($this->data['ReminderDelta'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_REMINDERDELTA; } if ($this->data['ReminderSet'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_REMINDER; } if ($this->data['BusyStatus'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_BUSYSTATUS; } if ($this->data['Attachment'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_ATTACHMENT; } if ($this->data['SubType'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_SUBTYPE; } if ($this->data['AppointmentColor'] !== null) { $flags += self::OVERRIDEFLAGS_ARO_APPTCOLOR; } $this->data['OverrideFlags'] = $flags; $this->set_structure(); return parent::output($base64); } /** * Modify the structure according to OverrideFlags */ protected function set_structure() { $flags = $this->data['OverrideFlags']; // Enable/Disable structure fields according to OverrideFlags if (!($flags & self::OVERRIDEFLAGS_ARO_SUBJECT)) { $this->structure['Subject']['type'] = 'EMPTY'; $this->structure['SubjectLength']['type'] = 'EMPTY'; $this->structure['SubjectLength2']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_MEETINGTYPE)) { $this->structure['MeetingType']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_REMINDERDELTA)) { $this->structure['ReminderDelta']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_REMINDER)) { $this->structure['ReminderSet']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_LOCATION)) { $this->structure['Location']['type'] = 'EMPTY'; $this->structure['LocationLength']['type'] = 'EMPTY'; $this->structure['LocationLength2']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_BUSYSTATUS)) { $this->structure['BusyStatus']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_ATTACHMENT)) { $this->structure['Attachment']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_SUBTYPE)) { $this->structure['SubType']['type'] = 'EMPTY'; } if (!($flags & self::OVERRIDEFLAGS_ARO_APPTCOLOR)) { $this->structure['AppointmentColor']['type'] = 'EMPTY'; } if ($flags & self::OVERRIDEFLAGS_ARO_EXCEPTIONAL_BODY) { // @TODO } } } diff --git a/lib/filter/mapistore/structure/extendedexception.php b/lib/filter/mapistore/structure/extendedexception.php index 8c831a3..292f175 100644 --- a/lib/filter/mapistore/structure/extendedexception.php +++ b/lib/filter/mapistore/structure/extendedexception.php @@ -1,129 +1,130 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * ExtendedException structure definition according to MS-OXOCAL 2.2.1.44.4 */ class kolab_api_filter_mapistore_structure_extendedexception extends kolab_api_filter_mapistore_structure { protected $parent; protected $structure = array( 'ChangeHighlight' => array('type' => 'kolab_api_filter_mapistore_structure_changehighlight'), 'ReservedBlockEE1Size' => array('type' => 'ULONG', 'default' => 0), 'ReservedBlockEE1' => array('type' => 'STRING', 'counter' => 'ReservedBlockEE1Size'), 'StartDateTime' => array('type' => 'ULONG'), 'EndDateTime' => array('type' => 'ULONG'), 'OriginalStartDate' => array('type' => 'ULONG'), 'WideCharSubjectLength' => array('type' => 'WORD',), 'WideCharSubject' => array('type' => 'WSTRING', 'counter' => 'WideCharSubjectLength'), 'WideCharLocationLength' => array('type' => 'WORD'), 'WideCharLocation' => array('type' => 'WSTRING', 'counter' => 'WideCharLocationLength'), 'ReservedBlockEE2Size' => array('type' => 'ULONG', 'default' => 0), 'ReservedBlockEE2' => array('type' => 'STRING', 'counter' => 'ReservedBlockEE2Size'), ); /** * Convert binary input into internal structure * * @param string $input Binary representation of the structure * @param bool $base64 Set to TRUE if the input is base64-encoded * @param object $parent Parent structure * @param int $index Index in the parent property array * * @return int Number of bytes read from the binary input */ public function input($input, $base64 = false, $parent = null, $index = null) { if ($base64) { $input = base64_decode($input); } // read OverrideFlags from matching ExceptionInfo if (empty($parent) || $index === null || !array_key_exists($index, (array) $parent->ExceptionInfo)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => 'Missing ExceptionInfo structure for ' . get_class($this) )); } $flags = $parent->ExceptionInfo[$index]->OverrideFlags; if (!($flags & kolab_api_filter_mapistore_structure_exceptioninfo::OVERRIDEFLAGS_ARO_SUBJECT)) { $no_subject = true; $this->structure['WideCharSubject']['type'] = 'EMPTY'; $this->structure['WideCharSubjectLength']['type'] = 'EMPTY'; } if (!($flags & kolab_api_filter_mapistore_structure_exceptioninfo::OVERRIDEFLAGS_ARO_LOCATION)) { $no_location = true; $this->structure['WideCharLocation']['type'] = 'EMPTY'; $this->structure['WideCharLocationLength']['type'] = 'EMPTY'; } if ($no_subject && $no_location) { $this->structure['StartDateTime']['type'] = 'EMPTY'; $this->structure['EndDateTime']['type'] = 'EMPTY'; $this->structure['OriginalStartDate']['type'] = 'EMPTY'; } return parent::input($input, false); } /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { if ($this->data['WideCharSubject'] !== null) { $got_subject = true; $this->data['WideCharSubjectLength'] = mb_strlen($this->data['WideCharSubject']); } else { $this->structure['WideCharSubjectLength']['type'] = 'EMPTY'; $this->structure['WideCharSubject']['type'] = 'EMPTY'; } if ($this->data['WideCharLocation'] !== null) { $got_location = true; $this->data['WideCharLocationLength'] = mb_strlen($this->data['WideCharLocation']); } else { $this->structure['WideCharLocationLength']['type'] = 'EMPTY'; $this->structure['WideCharLocation']['type'] = 'EMPTY'; } if (!$got_subject && !$got_location) { $this->structure['StartDateTime']['type'] = 'EMPTY'; $this->structure['EndDateTime']['type'] = 'EMPTY'; $this->structure['OriginalStartDate']['type'] = 'EMPTY'; } return parent::output($base64); } } diff --git a/lib/filter/mapistore/structure/recipientrow.php b/lib/filter/mapistore/structure/recipientrow.php index 7b7fe8e..fa0f6a8 100644 --- a/lib/filter/mapistore/structure/recipientrow.php +++ b/lib/filter/mapistore/structure/recipientrow.php @@ -1,55 +1,56 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * RecipientRow structure definition according to MS-OXCDATA 2.8.3 */ class kolab_api_filter_mapistore_structure_recipientrow extends kolab_api_filter_mapistore_structure { protected $structure = array( 'RecipientFlags' => array('type' => 'kolab_api_filter_mapistore_structure_recipientflags'), 'AddressPrefixUsed' => array('type' => 'BYTE'), 'DisplayType' => array('type' => 'WORD'), 'X500DN' => array('type' => 'STRING'), 'EntryIdSize' => array('type' => 'WORD', 'default' => 0), 'EntryId' => array('type' => 'STRING'), 'SearchKeySize' => array('type' => 'WORD'), 'SearchKey' => array(), 'AddressType' => array(), 'EmailAddress' => array(), 'DisplayName' => array(), 'SimpleDisplayName' => array(), 'TransmittableDisplayName' => array(), 'RecipientColumnCount' => array('type' => 'WORD'), 'RecipientProperties' => array('type' => '[kolab_api_filter_mapistore_structure_propertyrow]'), ); const DISPLAYTYPE_USER = 0x00; const DISPLAYTYPE_LIST = 0x01; const DISPLAYTYPE_FORUM = 0x02; const DISPLAYTYPE_AGENT = 0x03; const DISPLAYTYPE_ABOOK = 0x04; const DISPLAYTYPE_PRIVATE = 0x05; const DISPLAYTYPE_ABOOK_REMOTE = 0x06; } diff --git a/lib/filter/mapistore/structure/recurrencepattern.php b/lib/filter/mapistore/structure/recurrencepattern.php index d533d86..af5be50 100644 --- a/lib/filter/mapistore/structure/recurrencepattern.php +++ b/lib/filter/mapistore/structure/recurrencepattern.php @@ -1,170 +1,171 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * RecurrencePattern structure definition according to MS-OXOCAL 2.2.1.44.1 */ class kolab_api_filter_mapistore_structure_recurrencepattern extends kolab_api_filter_mapistore_structure { protected $structure = array( 'ReaderVersion' => array('type' => 'WORD', 'default' => 0x3004), 'WriterVersion' => array('type' => 'WORD', 'default' => 0x3004), 'RecurFrequency' => array('type' => 'WORD'), 'PatternType' => array('type' => 'WORD'), 'CalendarType' => array('type' => 'WORD', 'default' => 0x0000), 'FirstDateTime' => array('type' => 'ULONG'), 'Period' => array('type' => 'ULONG'), 'SlidingFlag' => array('type' => 'ULONG', 'default' => 0x00000000), 'PatternTypeSpecific' => array('type' => 'EMPTY'), // default for PatternType=0 'EndType' => array('type' => 'ULONG'), 'OccurrenceCount' => array('type' => 'ULONG', 'default' => 0x0000000A), 'FirstDOW' => array('type' => 'ULONG', 'default' => 0x00000000), 'DeletedInstanceCount' => array('type' => 'ULONG', 'default' => 0x00000000), 'DeletedInstanceDates' => array('type' => 'ULONG[]', 'counter' => 'DeletedInstanceCount'), 'ModifiedInstanceCount' => array('type' => 'ULONG', 'default' => 0x00000000), 'ModifiedInstanceDates' => array('type' => 'ULONG[]', 'counter' => 'ModifiedInstanceCount'), 'StartDate' => array('type' => 'ULONG'), 'EndDate' => array('type' => 'ULONG'), ); const RECURFREQUENCY_DAILY = 0x200A; const RECURFREQUENCY_WEEKLY = 0x200B; const RECURFREQUENCY_MONTHLY = 0x200C; const RECURFREQUENCY_YEARLY = 0x200D; const PATTERNTYPE_DAY = 0x0000; const PATTERNTYPE_WEEK = 0x0001; const PATTERNTYPE_MONTH = 0x0002; const PATTERNTYPE_MONTHEND = 0x0004; const PATTERNTYPE_MONTHNTH = 0x0003; const PATTERNTYPE_HJMONTH = 0x000A; const PATTERNTYPE_HJMONTHNTH = 0x000B; const PATTERNTYPE_HJMONTHEND = 0x000C; const CALENDARTYPE_DEFAULT = 0x0000; const CALENDARTYPE_GREGORIAN = 0x0001; const CALENDARTYPE_GREGORIAN_US = 0x0002; const CALENDARTYPE_JAPAN = 0x0003; const CALENDARTYPE_TAIWAN = 0x0004; const CALENDARTYPE_KOREA = 0x0005; const CALENDARTYPE_HIJRI = 0x0006; const CALENDARTYPE_THAI = 0x0007; const CALENDARTYPE_HEBREW = 0x0008; const CALENDARTYPE_GREGORIAN_ME_FRENCH = 0x0009; const CALENDARTYPE_GREGORIAN_ARABIC = 0x000A; const CALENDARTYPE_GREGORIAN_XLIT_ENGLISH = 0x000B; const CALENDARTYPE_GREGORIAN_XLIT_FRENCH = 0x000C; const CALENDARTYPE_LUNAR_JAPANESE = 0x000E; const CALENDARTYPE_CHINESE_LUNAR = 0x000F; const CALENDARTYPE_SAKA = 0x0010; const CALENDARTYPE_LUNAR_ETO_CHN = 0x0011; const CALENDARTYPE_LUNAR_ETO_KOR = 0x0012; const CALENDARTYPE_LUNAR_ROKUYOU = 0x0013; const CALENDARTYPE_LUNAR_KOREAN = 0x0014; const CALENDARTYPE_UMALQURA = 0x0017; const ENDTYPE_AFTER = 0x00002021; const ENDTYPE_NOCC = 0x00002022; const ENDTYPE_NEVER = 0x00002023; // can be 0xffffffff const FIRSTDOW_SUNDAY = 0x00000000; const FIRSTDOW_MONDAY = 0x00000001; const FIRSTDOW_TUESDAY = 0x00000002; const FIRSTDOW_WEDNESDAY = 0x00000003; const FIRSTDOW_THURSDAY = 0x00000004; const FIRSTDOW_FRIDAY = 0x00000005; const FIRSTDOW_SATURDAY = 0x00000006; /** * Convert binary input into internal structure * * @param string $input Binary representation of the structure * @param bool $base64 Set to TRUE if the input is base64-encoded * * @return int Number of bytes read from the binary input */ public function input($input, $base64 = false) { if ($base64) { $input = base64_decode($input); } // Read PatternType $unpack = unpack('v', substr($input, 6, 2)); $value = $unpack[1]; $this->data['PatternType'] = $value; // modify structure according to PatternType $this->set_structure(); return parent::input($input, false); } /** * Convert internal structure into binary string * * @param bool $base64 Enables base64 encoding of the output * * @return string Binary representation of the structure */ public function output($base64 = false) { $this->set_structure(); $this->data['DeletedInstanceDates'] = (array) $this->data['DeletedInstanceDates']; $this->data['ModifiedInstanceDates'] = (array) $this->data['ModifiedInstanceDates']; $this->data['DeletedInstanceCount'] = count($this->data['DeletedInstanceDates']); $this->data['ModifiedInstanceCount'] = count($this->data['ModifiedInstanceDates']); return parent::output($base64); } /** * Modify the structure according to PatternType */ protected function set_structure() { // Set PatternTypeSpecific field type according to PatternType switch ($this->data['PatternType']) { case self::PATTERNTYPE_WEEK: case self::PATTERNTYPE_MONTH: case self::PATTERNTYPE_MONTHEND: case self::PATTERNTYPE_HJMONTH: case self::PATTERNTYPE_HJMONTHEND: $this->structure['PatternTypeSpecific']['type'] = 'ULONG'; break; case self::PATTERNTYPE_MONTHNTH: case self::PATTERNTYPE_HJMONTHNTH: $this->structure['PatternTypeSpecific']['type'] = 'ULONG[2]'; break; case self::PATTERNTYPE_DAY: default: $this->structure['PatternTypeSpecific']['type'] = 'EMPTY'; break; } } } diff --git a/lib/filter/mapistore/structure/systemtime.php b/lib/filter/mapistore/structure/systemtime.php index b03f4e7..acd2a16 100644 --- a/lib/filter/mapistore/structure/systemtime.php +++ b/lib/filter/mapistore/structure/systemtime.php @@ -1,40 +1,41 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Systemtime structure definition according to MS-DTYP 2.3.13 */ class kolab_api_filter_mapistore_structure_systemtime extends kolab_api_filter_mapistore_structure { protected $structure = array( 'WYear' => array('type' => 'WORD'), 'WMonth' => array('type' => 'WORD'), 'WDayOfWeek' => array('type' => 'WORD'), 'WDay' => array('type' => 'WORD'), 'WHour' => array('type' => 'WORD'), 'WMinute' => array('type' => 'WORD', 'default' => 0), 'WSecond' => array('type' => 'WORD', 'default' => 0), 'WMilliseconds' => array('type' => 'WORD', 'default' => 0), ); } diff --git a/lib/filter/mapistore/structure/timezonestruct.php b/lib/filter/mapistore/structure/timezonestruct.php index 14911f6..b85506e 100644 --- a/lib/filter/mapistore/structure/timezonestruct.php +++ b/lib/filter/mapistore/structure/timezonestruct.php @@ -1,39 +1,40 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * TimeZoneStruct structure definition according to MS-OXOCAL 2.2.1.39 */ class kolab_api_filter_mapistore_structure_timezonestruct extends kolab_api_filter_mapistore_structure { protected $structure = array( 'IBias' => array('type' => 'LONG'), 'IStandardBias' => array('type' => 'LONG'), 'IDaylightBias' => array('type' => 'LONG'), 'WStandardYear' => array('type' => 'WORD'), 'StStandardDate' => array('type' => 'SYSTEMTIME'), 'WDaylightYear' => array('type' => 'WORD'), 'StDaylightDate' => array('type' => 'SYSTEMTIME'), ); } diff --git a/lib/filter/mapistore/structure/tzrule.php b/lib/filter/mapistore/structure/tzrule.php index 56fb041..660bdb9 100644 --- a/lib/filter/mapistore/structure/tzrule.php +++ b/lib/filter/mapistore/structure/tzrule.php @@ -1,46 +1,47 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * TZRule structure definition according to MS-OXOCAL 2.2.1.41.1 */ class kolab_api_filter_mapistore_structure_tzrule extends kolab_api_filter_mapistore_structure { protected $structure = array( 'MajorVersion' => array('type' => 'BYTE', 'default' => 0x01), 'MinorVersion' => array('type' => 'BYTE', 'default' => 0x01), 'Reserved' => array('type' => 'WORD', 'default' => 0x003E), 'Flags' => array('type' => 'WORD', 'default' => 0), 'WYear' => array('type' => 'WORD'), 'X' => array('type' => 'BYTE[14]', 'default' => 0), 'IBias' => array('type' => 'LONG'), 'IStandardBias' => array('type' => 'LONG'), 'IDaylightBias' => array('type' => 'LONG'), 'StStandardDate' => array('type' => 'SYSTEMTIME'), 'StDaylightDate' => array('type' => 'SYSTEMTIME'), ); const FLAG_RECUR_CURRENT = 1; const FLAG_EFFECTIVE = 2; } diff --git a/lib/filter/mapistore/task.php b/lib/filter/mapistore/task.php index 1f4e42b..f61b81a 100644 --- a/lib/filter/mapistore/task.php +++ b/lib/filter/mapistore/task.php @@ -1,272 +1,273 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_task { protected $map = array( // task specific props [MS-OXOTASK] 'PidTagProcessed' => '', // PtypBoolean 'PidLidTaskMode' => '', // ignored 'PidLidTaskStatus' => '', // PtypInteger32 'PidLidPercentComplete' => 'percent-complete', // PtypFloating64 'PidLidTaskStartDate' => 'dtstart', // PtypTime 'PidLidTaskDueDate' => 'due', // PtypTime 'PidLidTaskResetReminder' => '', // @TODO // PtypBoolean 'PidLidTaskAccepted' => '', // @TODO // PtypBoolean 'PidLidTaskDeadOccurrence' => '', // @TODO // PtypBoolean 'PidLidTaskDateCompleted' => 'x-custom.MAPI:PidLidTaskDateCompleted', // PtypTime 'PidLidTaskLastUpdate' => '', // PtypTime 'PidLidTaskActualEffort' => 'x-custom.MAPI:PidLidTaskActualEffort', // PtypInteger32 'PidLidTaskEstimatedEffort' => 'x-custom.MAPI:PidLidTaskEstimatedEffort', // PtypInteger32 'PidLidTaskVersion' => '', // PtypInteger32 'PidLidTaskState' => '', // PtypInteger32 'PidLidTaskRecurrence' => '', // PtypBinary 'PidLidTaskAssigners' => '', // PtypBinary 'PidLidTaskStatusOnComplete' => '', // PtypBoolean 'PidLidTaskHistory' => '', // @TODO: ? // PtypInteger32 'PidLidTaskUpdates' => '', // PtypBoolean 'PidLidTaskComplete' => '', // PtypBoolean 'PidLidTaskFCreator' => '', // PtypBoolean 'PidLidTaskOwner' => '', // @TODO // PtypString 'PidLidTaskMultipleRecipients' => '', // PtypBoolean 'PidLidTaskAssigner' => '', // PtypString 'PidLidTaskLastUser' => '', // PtypString 'PidLidTaskOrdinal' => '', // PtypInteger32 'PidLidTaskLastDelegate' => '', // PtypString 'PidLidTaskFRecurring' => '', // PtypBoolean 'PidLidTaskOwnership' => '', // @TODO // PtypInteger32 'PidLidTaskAcceptanceState' => '', // PtypInteger32 'PidLidTaskFFixOffline' => '', // PtypBoolean 'PidLidTaskGlobalId' => '', // @TODO // PtypBinary 'PidLidTaskCustomFlags' => '', // ignored 'PidLidTaskRole' => '', // ignored 'PidLidTaskNoCompute' => '', // ignored 'PidLidTeamTask' => '', // ignored // common props [MS-OXCMSG] 'PidTagSubject' => 'summary', 'PidTagBody' => 'description', 'PidTagHtml' => '', // @TODO: (?) 'PidTagNativeBody' => '', 'PidTagBodyHtml' => '', 'PidTagRtfCompressed' => '', 'PidTagInternetCodepage' => '', 'PidTagMessageClass' => '', 'PidLidCommonStart' => 'dtstart', 'PidLidCommonEnd' => 'due', 'PidTagIconIndex' => '', // @TODO 'PidTagCreationTime' => 'created', // PtypTime, UTC 'PidTagLastModificationTime' => 'dtstamp', // PtypTime, UTC ); /** * Values for PidLidTaskStatus property */ protected $status_map = array( 'none' => 0x00000000, // PidLidPercentComplete = 0 'in-progress' => 0x00000001, // PidLidPercentComplete > 0 and PidLidPercentComplete < 1 'complete' => 0x00000002, // PidLidPercentComplete = 1 'waiting' => 0x00000003, 'deferred' => 0x00000004, ); /** * Values for PidLidTaskHistory property */ protected $history_map = array( 'none' => 0x00000000, 'accepted' => 0x00000001, 'rejected' => 0x00000002, 'changed' => 0x00000003, 'due-changed' => 0x00000004, 'assigned' => 0x00000005, ); /** * 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( 'PidTagMessageClass' => 'IPM.Task', // mapistore REST API specific properties 'collection' => 'tasks', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); if ($value === null) { continue; } switch ($mapi_idx) { case 'PidLidPercentComplete': $value /= 100; break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': $value = kolab_api_filter_mapistore::date_php2mapi($value, false, array('hour' => 0)); break; case 'PidLidCommonStart': case 'PidLidCommonEnd': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; // case 'PidLidTaskLastUpdate': case 'PidTagCreationTime': case 'PidTagLastModificationTime': $value = kolab_api_filter_mapistore::date_php2mapi($value, true); break; case 'PidLidTaskActualEffort': case 'PidLidTaskEstimatedEffort': $value = (int) $value; break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // set status $percent = $result['PidLidPercentComplete']; if ($precent == 1) { $result['PidLidTaskStatus'] = $this->status_map['complete']; // PidLidTaskDateCompleted (?) } else if ($precent > 0) { $result['PidLidTaskStatus'] = $this->status_map['in-progress']; } else { $result['PidLidTaskStatus'] = $this->status_map['none']; } // Recurrence rule if (!empty($data['rrule']) && !empty($data['rrule']['recur'])) { if ($rule = kolab_api_filter_mapistore_event::recurrence_from_kolab($data['rrule']['recur'], $result)) { $result['PidLidTaskRecurrence'] = $rule; $result['PidLidTaskFRecurring'] = true; } } kolab_api_filter_mapistore::parse_common_props($result, $data, $context); 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) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidLidPercentComplete': $value = intval($value * 100); break; case 'PidLidTaskStartDate': case 'PidLidTaskDueDate': if (intval($value) !== 0x5AE980E0) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d'); } break; case 'PidLidCommonStart': case 'PidLidCommonEnd': // $value = kolab_api_filter_mapistore::date_mapi2php($value, true); break; case 'PidTagCreationTime': case 'PidTagLastModificationTime': if ($value) { $value = kolab_api_filter_mapistore::date_mapi2php($value); $value = $value->format('Y-m-d\TH:i:s\Z'); } break; } $result[$kolab_idx] = $value; } if ($data['PidLidTaskComplete']) { $result['status'] = 'COMPLETED'; } // Recurrences if (array_key_exists('PidLidTaskRecurrence', $data)) { $result['rrule']['recur'] = kolab_api_filter_mapistore_event::recurrence_to_kolab($data['PidLidTaskRecurrence'], 'task'); } kolab_api_filter_mapistore::convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); $map['PidLidTaskRecurrence'] = 'rrule'; return $map; } } diff --git a/lib/input/json.php b/lib/input/json.php index ec025a1..f3579c5 100644 --- a/lib/input/json.php +++ b/lib/input/json.php @@ -1,148 +1,149 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json extends kolab_api_input { /** * Get request data (JSON) * * @param string Expected object type * @param bool Disable filters application * @param array Original object data (set on update requests) * * @return array Request data */ public function input($type = null, $disable_filters = false, $original = null) { if ($this->input_body === null) { $data = file_get_contents('php://input'); $data = trim($data); $data = json_decode($data, true); $this->input_body = $data; } if (!$disable_filters) { if ($this->filter) { if (!empty($original)) { // convert object data into API format $data = $this->api->get_object_data($original, $type); } $this->filter->input_body($this->input_body, $type, $data); } // convert input to internal kolab_storage format if ($type) { $class = "kolab_api_input_json_$type"; $model = new $class; $model->input($this->input_body, $original); } } return $this->input_body; } /** * Convert xCard/xCal date and date-time into internal DateTime * * @param array|string Date or Date-Time * * @return DateTime */ public static function to_datetime($input) { if (empty($input)) { return; } if (is_array($input)) { if ($input['date-time']) { if ($input['parameters']['tzid']) { $tzid = str_replace('/kolab.org/', '', $input['parameters']['tzid']); } else { $tzid = 'UTC'; } $datetime = $input['date-time']; try { $timezone = new DateTimeZone($tzid); } catch (Exception $e) {} } else if ($input['timestamp']) { $datetime = $input['timestamp']; } else if ($input['date']) { $datetime = $input['date']; $is_date = true; } else { return; } } else { $datetime = $input; $is_date = preg_match('/^[0-9]{4}-?[0-9]{2}-?[0-9]{2}$/', $input); } try { $dt = new DateTime($datetime, $timezone ?: new DateTimeZone('UTC')); } catch (Exception $e) { return; } if ($is_date) { $dt->_dateonly = true; $dt->setTime(0, 0, 0); } return $dt; } /** * Add x-custom fields to the result */ public static function add_x_custom($data, &$result) { if (array_key_exists('x-custom', (array) $data)) { $value = (array) $data['x-custom']; foreach ((array) $value as $idx => $v) { if ($v['identifier'] && $v['value'] !== null) { $value[$idx] = array($v['identifier'], $v['value']); } else { unset($value[$idx]); } } $result['x-custom'] = $value; } } } diff --git a/lib/input/json/attachment.php b/lib/input/json/attachment.php index 80e7530..90cbdb0 100644 --- a/lib/input/json/attachment.php +++ b/lib/input/json/attachment.php @@ -1,71 +1,72 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_attachment { // map xml/json attributes into internal format protected $field_map = array( ); /** * Convert attachment input array into an array that can * be handled by the API * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': // $value = strtolower($value); break; } $result[$kolab] = $value; } if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/input/json/contact.php b/lib/input/json/contact.php index 5faedfe..79e18d7 100644 --- a/lib/input/json/contact.php +++ b/lib/input/json/contact.php @@ -1,337 +1,338 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_contact { // map xml/json attributes into internal (kolab_format) protected $field_map = array( // 'created' => 'creation-date', 'changed' => 'rev', 'categories' => 'categories', 'kind' => 'kind', // not supported by kolab_format_contact 'freebusyurl' => 'fburl', 'notes' => 'note', 'name' => 'fn', 'jobtitle' => 'title', 'nickname' => 'nickname', 'birthday' => 'bday', 'anniversary' => 'anniversary', 'photo' => 'photo', 'gender' => 'gender', 'im' => 'impp', 'lang' => 'lang', 'geo' => 'geo', // not supported by kolab_format_contact 'x-crypto' => 'x-crypto', // not supported by kolab_format_contact // the rest of properties is handled separately ); protected $address_props = array( // 'parameters', // 'pobox', // 'ext', 'street', 'locality', 'region', 'code', 'country' ); protected $gendermap = array( 'M' => 'male', 'F' => 'female', ); /** * Convert contact input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'gender': $value = $this->gendermap[$value]; break; case 'created': case 'changed': case 'birthday': case 'anniversary': $value = kolab_api_input_json::to_datetime($value); break; case 'photo': if (preg_match('/^(data:image\/[a-z]+;base64,).+$/i', $value, $m)) { $value = base64_decode(substr($value, strlen($m[1]))); } else { continue 2; } break; case 'jobtitle': $value = (array) $value; break; } $result[$kolab] = $value; } // contact name properties if (array_key_exists('n', $data)) { $name_attrs = array( 'surname' => 'surname', 'firstname' => 'given', 'middlename' => 'additional', 'prefix' => 'prefix', 'suffix' => 'suffix', ); foreach ($name_attrs as $kolab => $api) { $result[$kolab] = $data['n'][$api]; } } // contact additional properties if (array_key_exists('group', $data)) { $group_changed = true; $result['organization'] = null; $result['department'] = null; $result['profession'] = null; $result['manager'] = null; $result['assistant'] = null; foreach ((array) $data['group'] as $idx => $entry) { // organization, department if ($idx == 'org') { if (is_array($entry) && count($entry) > 1) { $result['organization'] = $entry[0]; $result['department'] = $entry[1]; } else { $result['organization'] = is_array($entry) ? $entry[0] : $entry; } } // profession else if ($idx == 'role') { $result['profession'] = $entry; } // manager, assistant else if ($idx == 'related') { foreach ((array) $entry as $item) { if ($item['text'] !== null) { foreach (array('manager', 'assistant') as $i) { if ($item['parameters']['type'] == "x-$i") { $result[$i] = $item['text']; } } } } } // office address else if ($idx == 'adr') { $address = array('type' => 'office'); foreach ($this->address_props as $prop) { if ($entry[$prop] !== null) { $address[$prop] = $entry[$prop]; } } $result['address'][] = $address; } } } // website url if (array_key_exists('url', $data)) { $result['website'] = array(); foreach ((array) $data['url'] as $url) { if (is_array($url) && $url['url']) { $result['website'][] = array( 'url' => $url['url'], 'type' => $url['parameters']['type'], ); } else if ($url) { $result['website'][] = array('url' => $url); } } } // home and work address if (array_key_exists('adr', $data)) { $adr_changed = true; foreach ((array) $data['adr'] as $addr) { $address = array('type' => $addr['parameters']['type']); foreach ($this->address_props as $prop) { if ($addr[$prop] !== null) { $address[$prop] = $addr[$prop]; } } $result['address'][] = $address; } } // spouse, children if (array_key_exists('related', $data)) { $result['spouse'] = null; $result['children'] = array(); foreach ($data['related'] as $entry) { if (isset($entry['text'])) { $type = $entry['parameters']['type']; if ($type == 'spouse') { $result['spouse'] = $entry['text']; } else if ($type == 'child') { $result['children'][] = $entry['text']; } } } } // phone numbers if (array_key_exists('tel', $data)) { $result['phone'] = array(); foreach ((array) $data['tel'] as $phone) { if (!empty($phone) && isset($phone['text'])) { $type = implode('', (array) $phone['parameters']['type']); $aliases = array( 'faxhome' => 'homefax', 'faxwork' => 'workfax', 'x-car' => 'car', 'textphone' => 'other', 'cell' => 'mobile', 'voice' => 'main', ); $result['phone'][] = array( 'type' => $aliases[$type] ?: $type, 'number' => $phone['text'], ); } } } // email addresses if (array_key_exists('email', $data)) { $result['email'] = array(); foreach ($data['email'] as $email) { if (!empty($email)) { if (!is_array($email)) { $result['email'][] = array( 'type' => 'other', 'address' => $email, ); } else if (isset($email['text'])) { $type = implode('', (array) $email['parameters']['type']); $result['email'][] = array( 'type' => $type ? $type : 'other', 'address' => $email['text'], ); } } } } // PGP or S/MIME key if (array_key_exists('key', $data)) { $key_types = array( 'pgp-keys' => 'pgppublickey', 'pkcs7-mime' => 'pkcs7publickey', ); foreach ($key_types as $type) { $result[$type] = null; } foreach ((array) $data['key'] as $key) { if (preg_match('#^data:application/(pgp-keys|pkcs7-mime);base64,#', $key, $m)) { $result[$key_types[$m[1]]] = base64_decode(substr($key, strlen($m[0]))); } } } // x-custom fields kolab_api_input_json::add_x_custom($data, $result); // @TODO: which contact properties should we require? if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { // fix addresses merging... $addresses = (array) $original['address']; // unset office address if ($group_changed) { foreach ($addresses as $idx => $adr) { if ($adr['type'] == 'office') { unset($addresses[$idx]); } } } // unset other addresses if ($adr_changed) { foreach ($addresses as $idx => $adr) { if ($adr['type'] != 'office') { unset($addresses[$idx]); } } } // merge old and new addresses if (isset($result['address']) && !empty($addresses)) { $result['address'] = array_merge($result['address'], $addresses); } $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/input/json/event.php b/lib/input/json/event.php index 2646b88..2790136 100644 --- a/lib/input/json/event.php +++ b/lib/input/json/event.php @@ -1,118 +1,119 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_event { // map xml/json attributes into internal (kolab_format) protected $field_map = array( 'description' => 'description', 'title' => 'summary', 'sensitivity' => 'class', 'sequence' => 'sequence', 'categories' => 'categories', 'created' => 'created', 'changed' => 'dtstamp', 'attendees' => 'attendee', 'organizer' => 'organizer', 'recurrence' => 'rrule', 'start' => 'dtstart', 'end' => 'dtend', 'valarms' => 'valarms', 'location' => 'location', 'priority' => 'priority', 'status' => 'status', 'url' => 'url', ); /** * Convert event input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } // require at least 'dtstart' property for new objects if (empty($original) && empty($data['dtstart'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': if ($value) { $value = strtolower($value); } break; case 'url': if (is_array($value)) { $value = $value[0]; } break; case 'created': case 'changed': case 'start': case 'end': $value = kolab_api_input_json::to_datetime($value); break; } $result[$kolab] = $value; } // @TODO: attendees // @TODO: organizer // @TODO: recurrence // @TODO: exceptions // @TOOD: alarms // x-custom fields kolab_api_input_json::add_x_custom($data, $result); // @TODO: should we require event summary/title? if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/input/json/folder.php b/lib/input/json/folder.php index 6077566..a67ec03 100644 --- a/lib/input/json/folder.php +++ b/lib/input/json/folder.php @@ -1,48 +1,49 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_folder { /** * Convert folder input array into internal format * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { // folder objects don't need to be converted // we do some sanity check only if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } /* if (!isset($data['name']) || !strlen($data['name'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ } } diff --git a/lib/input/json/mail.php b/lib/input/json/mail.php index b8b446f..885ec64 100644 --- a/lib/input/json/mail.php +++ b/lib/input/json/mail.php @@ -1,71 +1,58 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_mail { - // map xml/json attributes into internal format - protected $field_map = array( - ); - - /** - * Convert note input array into an array that can - * be handled by kolab_storage_folder::save() + * Convert mail input array into an object to store/send * - * @param array Request body - * @param array Original object data (on update) + * @param array $data Request body + * @param kolab_api_mail $original Original message (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } - foreach ($this->field_map as $kolab => $api) { - if (!array_key_exists($api, $data)) { - continue; - } + $result = $original ?: new kolab_api_mail; + $body_fields = array('text', 'html'); + $fields = array_merge(kolab_api_mail::$header_fields, $body_fields); - $value = $data[$api]; - - switch ($kolab) { - case 'sensitivity': -// $value = strtolower($value); - break; + foreach ($fields as $field) { + if (!array_key_exists($field, $data)) { + continue; } - $result[$kolab] = $value; + $result->{$field} = $data[$field]; } - if (empty($result)) { + if (!$result->changed() || !$result->valid()) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } - if (!empty($original)) { - $result = array_merge($original, $result); - } - $data = $result; } } diff --git a/lib/input/json/note.php b/lib/input/json/note.php index 376a009..936b191 100644 --- a/lib/input/json/note.php +++ b/lib/input/json/note.php @@ -1,88 +1,89 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_note { // map xml/json attributes into internal (kolab_format) protected $field_map = array( 'description' => 'description', 'title' => 'summary', 'sensitivity' => 'classification', 'categories' => 'categories', 'created' => 'creation-date', 'changed' => 'last-modification-date', ); /** * Convert note input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': if ($value) { $value = strtolower($value); } break; case 'created': case 'changed': $value = kolab_api_input_json::to_datetime($value); break; } $result[$kolab] = $value; } // x-custom fields kolab_api_input_json::add_x_custom($data, $result); // @TODO: should we require note summary/title? if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/input/json/task.php b/lib/input/json/task.php index f956e06..6600538 100644 --- a/lib/input/json/task.php +++ b/lib/input/json/task.php @@ -1,111 +1,112 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_task { // map xml/json attributes into internal (kolab_format) protected $field_map = array( 'description' => 'description', 'title' => 'summary', 'sensitivity' => 'class', 'sequence' => 'sequence', 'categories' => 'categories', 'created' => 'created', 'changed' => 'dtstamp', 'complete' => 'percent-complete', 'status' => 'status', 'start' => 'dtstart', 'due' => 'due', 'parent_id' => 'related-to', 'location' => 'location', 'priority' => 'priority', 'url' => 'url', ); /** * Convert task input array into an array that can * be handled by kolab_storage_folder::save() * * @param array Request body * @param array Original object data (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } foreach ($this->field_map as $kolab => $api) { if (!array_key_exists($api, $data)) { continue; } $value = $data[$api]; switch ($kolab) { case 'sensitivity': if ($value) { $value = strtolower($value); } break; case 'parent_id': // kolab_format_task supports only one parent if (is_array($value)) { $value = $value[0]; } break; case 'created': case 'changed': case 'start': case 'due': $value = kolab_api_input_json::to_datetime($value); break; } $result[$kolab] = $value; } // @TOOD: categories // @TODO: attendees // @TODO: organizer // @TODO: recurrence // @TOOD: alarms // x-custom fields kolab_api_input_json::add_x_custom($data, $result); if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } if (!empty($original)) { $result = array_merge($original, $result); } $data = $result; } } diff --git a/lib/kolab_api.php b/lib/kolab_api.php index ea93628..0455276 100644 --- a/lib/kolab_api.php +++ b/lib/kolab_api.php @@ -1,417 +1,421 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api extends rcube { const APP_NAME = 'Kolab REST API'; const VERSION = '0.1'; public $backend; public $filter; public $input; public $output; protected $model; /** * This implements the 'singleton' design pattern * * @return kolab_api The one and only instance */ public static function get_instance() { if (!self::$instance || !is_a(self::$instance, 'kolab_api')) { $path = kolab_api_input::request_path(); $request = array_shift($path); $class = 'kolab_api_' . $request; if (!$request || !class_exists($class)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND, array( 'code' => 500, 'line' => __LINE__, 'file' => __FILE__, 'message' => "Invalid request method: $request" )); } self::$instance = new $class(); self::$instance->startup(); } return self::$instance; } /** * Initial startup function * to register session, create database and imap connections */ protected function startup() { $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array) $this->config->get('kolab_api_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('libkolab'))); // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->plugins->load_plugin($plugin, true); } /* // start session $this->session_init(); // create user object $this->set_user(new rcube_user($_SESSION['user_id'])); */ } /** * Exception handler * * @param kolab_api_exception Exception */ public static function exception_handler($exception) { $code = $exception->getCode(); $message = $exception->getMessage(); if ($code == 401) { header('WWW-Authenticate: Basic realm="' . self::APP_NAME .'"'); } if (!$exception instanceof kolab_api_exception) { rcube::raise_error($exception, true, false); } header("HTTP/1.1 $code $message"); exit; } /** * Program execution handler */ protected function initialize_handler() { // Handle request input $this->input = kolab_api_input::factory($this); // Get input/output filter $this->filter = $this->input->filter; // Initialize backend $this->backend = kolab_api_backend::get_instance(); // Check authentication $this->authenticate(); // Filter the input, we want this after authentication if ($this->filter) { $this->filter->input($this->input); } // set response output class $this->output = kolab_api_output::factory($this); } /** * Script shutdown handler */ public function shutdown() { parent::shutdown(); // write performance stats to logs/console if ($this->config->get('devel_mode')) { if (function_exists('memory_get_peak_usage')) $mem = memory_get_peak_usage(); else if (function_exists('memory_get_usage')) $mem = memory_get_usage(); $log = trim(kolab_api_input::request_uri() . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '')); if (defined('KOLAB_API_START')) { rcube::print_timer(KOLAB_API_START, $log); } else { rcube::console($log); } } } /** * Authentication request handler (HTTP Auth) */ protected function authenticate() { if (!empty($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; } // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule else if (!isset($_SERVER['PHP_AUTH_USER'])) { // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..." if (isset($_SERVER['REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REMOTE_USER'], 6)); } else if (isset($_SERVER['REDIRECT_REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REDIRECT_REMOTE_USER'], 6)); } else if (isset($_SERVER['Authorization'])) { $basicAuthData = base64_decode(substr($_SERVER['Authorization'], 6)); } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $basicAuthData = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)); } if (isset($basicAuthData) && !empty($basicAuthData)) { list($username, $password) = explode(':', $basicAuthData); } } if (!empty($username)) { $result = $this->backend->authenticate($username, $password); } if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::UNAUTHORIZED); } + + $this->user = $this->backend->user; } /** * Handle API request */ public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if (!$path[1] && $path[0] && $method == 'POST') { $this->api_object_create(); } else if ($path[1]) { switch (strtolower($path[2])) { case 'attachments': if ($method == 'HEAD') { $this->api_object_count_attachments(); } else if ($method == 'GET') { $this->api_object_list_attachments(); } break; case '': if ($method == 'GET') { $this->api_object_info(); } else if ($method == 'PUT') { $this->api_object_update(); } else if ($method == 'HEAD') { $this->api_object_exists(); } else if ($method == 'DELETE') { $this->api_object_delete(); } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Fetch object info */ protected function api_object_info() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array('folder_uid' => $folder, 'object' => $object); $this->output->send($object, $this->model, $context); } /** * Create an object */ protected function api_object_create() { $folder = $this->input->path[0]; $input = $this->input->input($this->model); $context = array('folder_uid' => $folder); $uid = $this->backend->object_create($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * Update specified object */ protected function api_object_update() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); + $context = array( + 'folder_uid' => $folder, + 'object_uid' => $uid, + 'object' => $object, + ); - // convert object data into API format - $data = $this->get_object_data($object, $this->model); - - // parse input and merge with current data (result is in kolab_format) + // parse input and merge with current data (result is in kolab_format/kolab_api_mail) $input = $this->input->input($this->model, false, $object); // update object on the backend - $this->backend->object_update($folder, $input, $this->model); + $uid = $this->backend->object_update($folder, $input, $this->model); -// $this->output->send(array('uid' => $uid), $this->model, $folder); - $this->output->send_status(kolab_api_output::STATUS_EMPTY); + $this->output->send(array('uid' => $uid), $this->model, $context); } /** * Check if specified object exists */ protected function api_object_exists() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove specified object */ protected function api_object_delete() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->backend->objects_delete($folder, array($uid)); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Count object attachments */ protected function api_object_count_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, - 'object' => $object + 'object' => $object, ); $count = !empty($object['_attachments']) ? count($object['_attachments']) : 0; $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * List object attachments */ protected function api_object_list_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, - 'object' => $object + 'object' => $object, ); // @TODO: currently Kolab format (libkolabxml) allows attachments // in events, tasks and notes. We should support them also in contacts $list = $this->get_object_attachments($object); $this->output->send($list, 'attachment-list', $context, $props); } /** * Extract attachments from the object, depending if it's * Kolab object or email message */ protected function get_object_attachments($object) { // this is a kolab_format object data if (is_array($object)) { $list = (array) $object['_attachments']; foreach ($list as $idx => $att) { $attachment = new rcube_message_part; $attachment->mime_id = $att['id']; $attachment->filename = $att['name']; $attachment->mimetype = $att['mimetype']; $attachment->size = $att['size']; $attachment->disposition = 'attachment'; $list[$idx] = $attachment; } } // this is rcube_message(_header) else { $list = (array) $object->attachments; } return $list; } /** * Convert kolab_format object into API format * * @param array Object data in kolab_format * @param string Object type * * @return array Object data in API format */ public function get_object_data($object, $type) { $output = $this->output; if (!$this->output instanceof kolab_api_output_json) { $class = "kolab_api_output_json"; $output = new $class($this); } return $output->convert($object, $type); } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index 23eb830..f06dbb4 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1227 +1,1239 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $api; public $storage; public $username; public $password; + public $user; public $delimiter; protected $icache = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->api = kolab_api::get_instance(); $this->storage = $this->api->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); $this->delimiter = $this->storage->get_hierarchy_delimiter(); } /** * 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) { $data['type'] = $type_data[$folder][$key]; 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); + + 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; } } } } 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($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $target = $this->folder_get_by_uid($uid, $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 rcube_message|array Object data + * @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; } } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier - * @param array $data Object data + * @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 + // @TODO: categories + + return $data->save($folder); } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // 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 array $data Object data + * @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); - // @TODO + return $data->save($folder); } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // 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 rcube_message) { return $object->get_part_body($part_id, false, 0, $mode); } // otherwise use kolab_storage else { return $this->storage->get_message_part($this->uid, $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 boolean|string True or message UID (if changed) * @throws kolab_api_exception */ public function attachment_delete($object, $id) { // object is a mail message if ($object instanceof rcube_message) { // @TODO } // 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($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return true; } } /** * 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); } // 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'] = array_pop($path); $info['fullpath'] = implode($this->delimiter, $path); $info['uid'] = $uid; $info['type'] = kolab_storage::folder_type($folder, true) ?: 'mail'; if ($info['fullpath'] !== '') { $parent = $this->folder_name2uid(rcube_charset::convert($info['fullpath'], 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_PRIVATE, 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))) { if ($this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_PRIVATE => $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_PRIVATE, 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()); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); return $user->ID; } /** * Returns list of tag-relation names assigned to kolab object */ protected 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->headers->get('message-id', false); $folder = $message->folder; $uid = $message->uid; } if (empty($ident)) { return array(); } $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($ident); $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) { // @TODO: set_tags() for email $config = kolab_storage_config::get_instance(); $config->save_tags($uid, $tags); } } diff --git a/lib/kolab_api_exception.php b/lib/kolab_api_exception.php index 630babd..5a2f4bd 100644 --- a/lib/kolab_api_exception.php +++ b/lib/kolab_api_exception.php @@ -1,72 +1,73 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Main exception class for Kolab REST API responses */ class kolab_api_exception extends Exception { const UNAUTHORIZED = 401; const FORBIDDEN = 403; const NOT_FOUND = 404; const TIMEOUT = 408; const INVALID_REQUEST = 422; const SERVER_ERROR = 500; const NOT_IMPLEMENTED = 501; const UNAVAILABLE = 503; private $messages = array( self::UNAUTHORIZED => 'Unauthorized', self::FORBIDDEN => 'Forbidden', self::NOT_FOUND => 'Not found', self::TIMEOUT => 'Request timeout', self::INVALID_REQUEST => 'Invalid request', self::SERVER_ERROR => 'Internal server error', self::NOT_IMPLEMENTED => 'Not implemented', self::UNAVAILABLE => 'Service unavailable', ); /** * Constructor * * @param int HTTP error code (default 500) * @param array Optional error info to log */ function __construct($code = 0, $error = array()) { $message = $this->messages[$code]; if (!$message) { $code = self::SERVER_ERROR; $message = $this->messages[self::SERVER_ERROR]; } if (!empty($error)) { rcube::raise_error($error, true, false); } parent::__construct($message, $code); } } diff --git a/lib/kolab_api_filter.php b/lib/kolab_api_filter.php index 07edae5..4584d8b 100644 --- a/lib/kolab_api_filter.php +++ b/lib/kolab_api_filter.php @@ -1,74 +1,74 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ abstract class kolab_api_filter { /** * Modify initial request path * * @param array (Exploded) request path */ abstract function path(&$path); /** * Executed before every api action * * @param kolab_api_input Request data */ abstract function input(&$input); /** * Executed when parsing request body * * @param array Request body * @param string Object type * @param array Original object data (set on update requests) */ abstract function input_body(&$data, $type, $original = null); /** * 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 */ abstract function output(&$output, $type, $context, $attrs_filter = array()); /** * Executed for response headers * * @param array Response headers */ abstract function headers(&$headers); /** * Executed for empty response status * * @param int Status code */ abstract function send_status(&$status); } diff --git a/lib/kolab_api_input.php b/lib/kolab_api_input.php index bac8d6e..6ace49a 100644 --- a/lib/kolab_api_input.php +++ b/lib/kolab_api_input.php @@ -1,137 +1,138 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ abstract class kolab_api_input { public $method; public $action; public $path = array(); public $args = array(); public $supports = array(); public $api; public $filter; public $input_body; /** * Factory method to create input object * according to the API request input * * @param kolab_api The API * * @return kolab_api_input Output object */ public static function factory($api) { // default mode $mode = 'json'; $class = "kolab_api_input_$mode"; return new $class($api); } /** * Object constructor */ public function __construct($api) { $this->api = $api; $this->method = $_SERVER['REQUEST_METHOD']; if ($this->method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) { $this->method = $_SERVER['HTTP_X_HTTP_METHOD']; } $this->path = self::request_path($this->filter); // remove first argument - action name $this->action = array_shift($this->path); if ($this->api->config->get('kolab_api_debug')) { rcube::console($this->method . ': ' . self::request_uri()); // @TODO: log request input data for PUT/POST } $accept_header = strtolower(rcube_utils::request_header('Accept')); list($this->supports,) = explode(';', $accept_header); $this->supports = explode(',', $this->supports); // store GET arguments $this->args = $_GET; unset($this->args['api']); unset($this->args['request']); } /** * Parse request arguments * * @return array Request arguments */ public static function request_path(&$filter = null) { $api = (string) $_GET['api']; $path = explode('/', trim((string) $_GET['request'], ' /')); // map api specific request to Kolab API if ($api && class_exists("kolab_api_filter_$api")) { $class = "kolab_api_filter_$api"; $filter = new $class; $filter->path($path); } foreach ($path as $idx => $value) { $path[$idx] = strip_tags($value); } $path[0] = strtolower($path[0]); return $path; } /** * Return request URI (for logging) */ public static function request_uri() { $url = trim((string) $_GET['request'], ' /'); list($uri, $params) = explode('?', $_SERVER['REQUEST_URI']); if ($params) { $url .= '?' . $params; } return $url; } /** * Get request data (JSON) * * @param string Expected object type * @param bool Disable filters application * @param array Original object data (set on update requests) * * @return array Request data */ abstract function input($type = null, $disable_filters = false, $original = null); } diff --git a/lib/kolab_api_mail.php b/lib/kolab_api_mail.php new file mode 100644 index 0000000..67cf210 --- /dev/null +++ b/lib/kolab_api_mail.php @@ -0,0 +1,874 @@ + | + +--------------------------------------------------------------------------+ + | 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', + ); + + /** + * 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; + + + /** + * 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; + break; + + 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; + 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; + } + + // 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; + } + + /** + * 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()?' + )); + } + + $api = kolab_api::get_instance(); + $message = $this->get_message(); + $specials = array('flags', 'categories'); + $diff = array_diff($this->data, $specials); + $headers = array(); + $endln = $this->endln; + + if (empty($message) && !strlen($folder)) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( + 'file' => __FILE__, + 'line' => __LINE__, + 'message' => 'Folder not specified' + )); + } + + // header change requested, get old headers + if (!empty($diff) && $message) { + $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 'subject': + $headers['Subject'] = $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': + if ($value) { + $headers[$normalized] = $value; + } + break; + } + } + + // Prepare message body + $body_mod = $this->prepare_body($headers); + + // Write message headers to the stream + if (!empty($headers) || empty($message) || $body_mod) { + 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'] = $api->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)) { + 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 + $this->write_body($stream, $headers, $text, $html); + + // 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); + } + } + + return $uid; + } + + /** + * Send the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function send() + { + // @TODO + } + + /** + * Add attachment to the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function attachment_add() + { + // @TODO + if (!($message = $this->get_message())) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + + /** + * Remove attachment from the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function attachment_remove() + { + // @TODO + if (!($message = $this->get_message())) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + + /** + * Update attachment in the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function attachment_update() + { + // @TODO + if (!($message = $this->get_message())) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + + /** + * 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); + + if (!$body_mod) { + return false; + } + + $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; + + 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(); + $modified = array_key_exists('text', $this->data) || array_key_exists('html', $this->data); + + // @TODO: related parts for inline images + // @TODO: attachment parts + + // 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') { + // 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, 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); + + $inside = false; + $done = 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)) { + if ($inside) { + $headers = null; + $inside = false; + } + else if (!$done) { + $headers = ''; + $inside = true; + } + } + else if ($inside) { + if ($headers !== null) { + // parse headers + if (!strlen(rtrim($line, "\r\n"))) { +// $a_headers = self::parse_headers($headers); + $boundary = '_' . md5(rand() . microtime()); + $this->write_body_content($stream, $boundary, true); + + $headers = null; + $done = true; + } + else { + $headers .= $line; + } + } + + 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); + } + + 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 + $saved = $api->backend->storage->save_message($folder, array($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; + } + } +} diff --git a/lib/kolab_api_output.php b/lib/kolab_api_output.php index 16729b7..8e8d2f4 100644 --- a/lib/kolab_api_output.php +++ b/lib/kolab_api_output.php @@ -1,125 +1,130 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ abstract class kolab_api_output { const STATUS_OK = 200; const STATUS_EMPTY = 204; protected $api; protected $messages = array( self::STATUS_OK => 'OK', self::STATUS_EMPTY => 'No content', ); /** * Factory method to create output object * according to the API request input * * @param kolab_api The API * * @return kolab_api_output Output object */ public static function factory($api) { // default mode of kolab format $mode = 'xml'; $modes_map = array( // 'text/html' => 'html', // 'application/html+xml' => 'xml', 'application/xml' => 'xml', 'application/json' => 'json', ); foreach ((array) $api->input->supports as $type) { if ($_mode = $modes_map[$type]) { $mode = $_mode; break; } } $mode = 'json'; $class = "kolab_api_output_$mode"; return new $class($api); } /** * Object constructor * * @param kolab_api The API */ public function __construct($api) { $this->api = $api; } /** * Set response headers (must be done before send()). * * @param array Response headers */ public function headers($headers) { if ($this->api->filter) { $this->api->filter->headers($headers); } if (!empty($headers)) { if ($this->api->config->get('kolab_api_debug')) { rcube::console($headers); } foreach ((array) $headers as $header => $value) { header($header . ': ' . $value); } } } /** * Send status of successful (empty) response * - * @param int Status code + * @param int $status Status code + * @param bool $exit Call exit() */ - public function send_status($status) + public function send_status($status, $exit = true) { if ($this->api->filter) { $this->api->filter->send_status($status); } $message = $this->messages[$status]; header("HTTP/1.1 $status $message"); - exit; + + if ($exit) { + exit; + } } /** * Send successful response * * @param mixed Response data * @param string Data type * @param array Context (folder_uid, object_uid, object) */ abstract function send($data, $type, $context = null); } diff --git a/lib/output/json.php b/lib/output/json.php index b187706..331e52a 100644 --- a/lib/output/json.php +++ b/lib/output/json.php @@ -1,247 +1,250 @@ | +--------------------------------------------------------------------------+ | 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 (!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]); } } } } diff --git a/lib/output/json/attachment.php b/lib/output/json/attachment.php index a6d4a79..167242b 100644 --- a/lib/output/json/attachment.php +++ b/lib/output/json/attachment.php @@ -1,99 +1,100 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_attachment { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert message data into an array * * @param rcube_message_part Attachment data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { $result = array(); // supported attachment data fields $fields = array( 'id', 'mimetype', 'size', 'filename', 'disposition', 'content-id', 'content-location', ); if (!empty($attrs_filter)) { $header_fields = array_intersect($header_fields, $attrs_filter); } foreach ($fields as $field) { $value = null; switch ($field) { case 'id': $value = (string) $data->mime_id; break; case 'mimetype': case 'filename': case 'disposition': case 'content-id': case 'content-location': $value = $data->{str_replace('-', '_', $field)}; break; case 'size': $value = (int) $data->size; break; } // 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); $result[$field] = $value; } } return $result; } } diff --git a/lib/output/json/configuration.php b/lib/output/json/configuration.php index 53da007..4e8014e 100644 --- a/lib/output/json/configuration.php +++ b/lib/output/json/configuration.php @@ -1,59 +1,60 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_configuration { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data if (count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'configuration', 'configuration', $attrs_filter); return $result; } } diff --git a/lib/output/json/contact.php b/lib/output/json/contact.php index dd1c232..90ccecf 100644 --- a/lib/output/json/contact.php +++ b/lib/output/json/contact.php @@ -1,79 +1,80 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_contact { protected $output; protected $array_elements = array( // 'group', // @TODO 'adr', 'related', 'url', 'lang', 'tel', 'impp', 'email', 'geo', 'key', 'title', 'categories', 'member', //dist-list 'x-custom', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data - if (count($data) == 1) { + if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'contact', 'vcard', $attrs_filter, $this->array_elements); if ($result['uid'] && strpos($result['uid'], 'urn:uuid:') === 0) { $result['uid'] = substr($result['uid'], 9); } return $result; } } diff --git a/lib/output/json/event.php b/lib/output/json/event.php index 2e9798a..575b764 100644 --- a/lib/output/json/event.php +++ b/lib/output/json/event.php @@ -1,80 +1,81 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_event { protected $output; protected $array_elements = array( 'attach', 'attendee', 'categories', 'x-custom', 'valarm', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data - if (count($data) == 1) { + if (is_array($data) && count($data) == 1) { return $data; } $result = $this->output->object_to_array($data, 'event', 'vevent'); if (!empty($attrs_filter)) { $result['properties'] = array_intersect_key($result['properties'], array_combine($attrs_filter, $attrs_filter)); } // add 'components' to the result if (!empty($result['components'])) { $result['properties'] += (array) $result['components']; } $result = $result['properties']; kolab_api_output_json::parse_array_result($result, $this->array_elements); return $result; } } diff --git a/lib/output/json/file.php b/lib/output/json/file.php index 043493b..9f2c728 100644 --- a/lib/output/json/file.php +++ b/lib/output/json/file.php @@ -1,62 +1,63 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_file { protected $output; protected $array_elements = array( 'x-custom', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data - if (count($data) == 1) { + if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'file', 'file', $attrs_filter, $this->array_elements); return $result; } } diff --git a/lib/output/json/folder.php b/lib/output/json/folder.php index 1f27a8d..9f90e7d 100644 --- a/lib/output/json/folder.php +++ b/lib/output/json/folder.php @@ -1,62 +1,63 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_folder { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { $element = array(); foreach ($data as $idx => $value) { if ($value !== null && $value !== '') { if (empty($attrs_filter) || in_array($idx, $attrs_filter)) { $element[$idx] = $value; } } } return $element; } } diff --git a/lib/output/json/info.php b/lib/output/json/info.php index 761853e..79095e6 100644 --- a/lib/output/json/info.php +++ b/lib/output/json/info.php @@ -1,62 +1,63 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_info { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { $element = array(); foreach ($data as $idx => $value) { if ($value !== null && $value !== '') { if (empty($attrs_filter) || in_array($idx, $attrs_filter)) { $element[$idx] = $value; } } } return $element; } } diff --git a/lib/output/json/mail.php b/lib/output/json/mail.php index a26d1b1..e1efa07 100644 --- a/lib/output/json/mail.php +++ b/lib/output/json/mail.php @@ -1,155 +1,53 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_mail { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert message data into an array * - * @param rcube_message|rcube_message_header Message data - * @param array Optional attributes filter + * @param kolab_api_mail Message data + * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { - $is_header = $data instanceof rcube_message_header; - $headers = $is_header ? $data : $data->headers; - $result = array(); - - // supported header fields - $header_fields = array( - 'uid', - 'subject', - 'from', - 'to', - 'cc', - 'bcc', - 'reply-to', - 'date', - 'internaldate', - 'content-type', - 'priority', - 'size', - 'flags', - 'categories', - ); - - if (!empty($attrs_filter)) { - $header_fields = array_intersect($header_fields, $attrs_filter); - } - - foreach ($header_fields as $field) { - $value = null; - - switch ($field) { - case 'uid': - $value = (string) $headers->uid; - break; - - case 'priority': - case 'size': - if (isset($headers->{$field})) { - $value = (int) $headers->{$field}; - } - break; - - case 'content-type': - $value = $headers->ctype; - if ($pos = strpos($value, ';')) { - $value = substr($value, 0, $pos); - } - break; - - case 'date': - case 'internaldate': - $value = $headers->{$field}; - 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 'to': - case 'cc': - case 'bcc': - case 'reply-to': - $addresses = $headers->{$field == 'reply-to' ? 'replyto' : $field}; - $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 ($field == 'from' && !empty($value)) { - $value = $value[0]; - } - - break; - } - - // 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); - - $result[$field] = $value; - } - } - - // complete rcube_message object, we can set more props? - if (!$is_header) { - // @TODO - } - - return $result; + return is_array($data) ? $data : $data->data($attrs_filter); } } diff --git a/lib/output/json/note.php b/lib/output/json/note.php index 0d8906e..d40819d 100644 --- a/lib/output/json/note.php +++ b/lib/output/json/note.php @@ -1,64 +1,65 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_note { protected $output; protected $array_elements = array( 'attachment', 'categories', 'x-custom', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { // partial data - if (count($data) == 1) { + if (is_array($data) && count($data) == 1) { $attrs_filter = array(key($data)); } $result = $this->output->object_to_array($data, 'note', 'note', $attrs_filter, $this->array_elements); return $result; } } diff --git a/lib/output/json/task.php b/lib/output/json/task.php index 618d344..f99814b 100644 --- a/lib/output/json/task.php +++ b/lib/output/json/task.php @@ -1,76 +1,82 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_json_task { protected $output; protected $array_elements = array( 'attach', 'attendee', 'related-to', 'x-custom', 'categories', 'valarm', ); /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data into an array * * @param array Data * @param array Optional attributes filter * * @return array Data */ public function element($data, $attrs_filter = array()) { + // partial data + if (is_array($data) && count($data) == 1) { + $attrs_filter = array(key($data)); + } + $result = $this->output->object_to_array($data, 'task', 'vtodo'); if (!empty($attrs_filter)) { $result['properties'] = array_intersect_key($result['properties'], array_combine($attrs_filter, $attrs_filter)); } // add 'components' to the result if (!empty($result['components'])) { $result['properties'] += (array) $result['components']; } $result = $result['properties']; kolab_api_output_json::parse_array_result($result, $this->array_elements); return $result; } } diff --git a/lib/output/xml.php b/lib/output/xml.php index 186226b..2c5bff3 100644 --- a/lib/output/xml.php +++ b/lib/output/xml.php @@ -1,145 +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; } } } diff --git a/lib/output/xml/configuration.php b/lib/output/xml/configuration.php index 5ba0b81..709c475 100644 --- a/lib/output/xml/configuration.php +++ b/lib/output/xml/configuration.php @@ -1,82 +1,83 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_configuration { const XMLNS = "http://kolab.org"; protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * * @param array Data object * @param DOMElement XML element * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { $dom = $this->output->object_to_dom($data, 'configuration'); if ($dom) { // get 'configuration' element $content = $dom->getElementsByTagName('configuration'); // and copy it into output 'configurations' element foreach ($content as $element) { $node = $xml->ownerDocument->importNode($element, true); kolab_api_output_xml::attrs_filter($node, $attrs_filter); $xml->appendChild($node); } } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { $doc = $xml->createElement('configurations'); $doc->setAttribute('xmlns', self::XMLNS); $doc->setAttribute('version', '3.0'); $xml->appendChild($doc); return $doc; } } diff --git a/lib/output/xml/contact.php b/lib/output/xml/contact.php index 77df6f1..d8cf777 100644 --- a/lib/output/xml/contact.php +++ b/lib/output/xml/contact.php @@ -1,81 +1,82 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_contact { const XMLNS = "urn:ietf:params:xml:ns:vcard-4.0"; protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * * @param array Data object * @param DOMElement XML element * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { $dom = $this->output->object_to_dom($data, 'contact'); if ($dom) { // get 'vevent' element $content = $dom->getElementsByTagName('vcard'); // and copy it into output 'vcards' element foreach ($content as $vcard) { $node = $xml->ownerDocument->importNode($vcard, true); kolab_api_output_xml::attrs_filter($node, $attrs_filter); $xml->appendChild($node); } } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { $doc = $xml->createElement('vcards'); $doc->setAttribute('xmlns', self::XMLNS); $xml->appendChild($doc); return $doc; } } diff --git a/lib/output/xml/event.php b/lib/output/xml/event.php index 3501d53..a487b94 100644 --- a/lib/output/xml/event.php +++ b/lib/output/xml/event.php @@ -1,102 +1,103 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_event { const XMLNS = "urn:ietf:params:xml:ns:icalendar-2.0"; protected $output; protected $type = 'event'; protected $wrapper = 'vevent'; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * * @param array Data object * @param DOMElement XML element * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { $dom = $this->output->object_to_dom($data, $this->type); if ($dom) { // get object element(s) $content = $dom->getElementsByTagName($this->wrapper); // and copy it into output 'components' element foreach ($content as $element) { $node = $xml->ownerDocument->importNode($element, true); kolab_api_output_xml::attrs_filter($node, $attrs_filter); $xml->appendChild($node); } } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { $icalendar = $xml->createElement('icalendar'); $vcalendar = $xml->createElement('vcalendar'); $properties = $xml->createElement('properties'); $components = $xml->createElement('components'); $props = array( 'prodid' => kolab_api_output_xml::$prod_id, 'version' => kolab_api_output_xml::$version, 'x-kolab-version' => kolab_api_output_xml::$x_version, ); foreach ($props as $name => $value) { $prop = $xml->createElement($name); $prop->appendChild($xml->createElement('text', $value)); $properties->appendChild($prop); } $icalendar->setAttribute('xmlns', self::XMLNS); $vcalendar->appendChild($properties); $vcalendar->appendChild($components); $icalendar->appendChild($vcalendar); $xml->appendChild($icalendar); return $components; } } diff --git a/lib/output/xml/file.php b/lib/output/xml/file.php index 755e07b..7a4e4f8 100644 --- a/lib/output/xml/file.php +++ b/lib/output/xml/file.php @@ -1,82 +1,83 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_file { const XMLNS = "http://kolab.org"; protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * * @param array Data object * @param DOMElement XML element * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { $dom = $this->output->object_to_dom($data, 'file'); if ($dom) { // get 'file' element $content = $dom->getElementsByTagName('file'); // and copy it into output 'files' element foreach ($content as $element) { $node = $xml->ownerDocument->importNode($element, true); kolab_api_output_xml::attrs_filter($node, $attrs_filter); $xml->appendChild($node); } } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { $doc = $xml->createElement('files'); $doc->setAttribute('xmlns', self::XMLNS); $doc->setAttribute('version', '3.0'); $xml->appendChild($doc); return $doc; } } diff --git a/lib/output/xml/folder.php b/lib/output/xml/folder.php index e6d9b1e..4b7749b 100644 --- a/lib/output/xml/folder.php +++ b/lib/output/xml/folder.php @@ -1,79 +1,80 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_folder { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * * @param array Data object * @param DOMElement XML element * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { $dom = $xml->ownerDocument->createElement('folder'); foreach ($data as $name => $v) { if ($v !== null && $v !== '') { if (empty($attrs_filter) || in_array($name, $attrs_filter)) { $prop_element = $xml->ownerDocument->createElement($name, $v); $dom->appendChild($prop_element); } } } if ($dom->hasChildNodes()) { $xml->appendChild($dom); } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { $folderlist = $xml->createElement('folderlist'); $xml->appendChild($folderlist); return $folderlist; } } diff --git a/lib/output/xml/folder.php b/lib/output/xml/mail.php similarity index 78% copy from lib/output/xml/folder.php copy to lib/output/xml/mail.php index e6d9b1e..9ee66bf 100644 --- a/lib/output/xml/folder.php +++ b/lib/output/xml/mail.php @@ -1,79 +1,86 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_output_xml_folder +class kolab_api_output_xml_mail { protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * - * @param array Data object - * @param DOMElement XML element - * @param array Optional attributes filter + * @param kolab_api_mail Object data + * @param DOMElement XML element + * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { - $dom = $xml->ownerDocument->createElement('folder'); + $dom = $xml->ownerDocument->createElement('message'); + $data = is_array($data) ? $data : $data->data($attrs_filter); foreach ($data as $name => $v) { if ($v !== null && $v !== '') { - if (empty($attrs_filter) || in_array($name, $attrs_filter)) { - $prop_element = $xml->ownerDocument->createElement($name, $v); - $dom->appendChild($prop_element); - } + $this->add_element($xml, $dom, $name, $v); } } if ($dom->hasChildNodes()) { $xml->appendChild($dom); } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { - $folderlist = $xml->createElement('folderlist'); - $xml->appendChild($folderlist); + $list = $xml->createElement('messages'); + $xml->appendChild($list); + + return $list; + } - return $folderlist; + /** + * Add element to the XML document + */ + protected function add_element($xml, $parent, $name, $value) + { + // @TODO: "array to xml dom php" } } diff --git a/lib/output/xml/note.php b/lib/output/xml/note.php index 1708231..ac6b7f5 100644 --- a/lib/output/xml/note.php +++ b/lib/output/xml/note.php @@ -1,82 +1,83 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_note { const XMLNS = "http://kolab.org"; protected $output; /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { $this->output = $output; } /** * Convert data array into XML * * @param array Data object * @param DOMElement XML element * @param array Optional attributes filter */ public function append_element($data, $xml, $attrs_filter = array()) { $dom = $this->output->object_to_dom($data, 'note'); if ($dom) { // get 'note' element $content = $dom->getElementsByTagName('note'); // and copy it into output 'notes' element foreach ($content as $element) { $node = $xml->ownerDocument->importNode($element, true); kolab_api_output_xml::attrs_filter($node, $attrs_filter); $xml->appendChild($node); } } } /** * Add model-specific structure to the XML document * * @param DOMDocument XML Document * * @return DOMNode Element to which object structure will be added */ public function structure($xml) { $doc = $xml->createElement('notes'); $doc->setAttribute('xmlns', self::XMLNS); $doc->setAttribute('version', '3.0'); $xml->appendChild($doc); return $doc; } } diff --git a/lib/output/xml/task.php b/lib/output/xml/task.php index 3687bea..5784a8c 100644 --- a/lib/output/xml/task.php +++ b/lib/output/xml/task.php @@ -1,40 +1,41 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_output_xml_task extends kolab_api_output_xml_event { /** * Object constructor * * @param kolab_api_output Output object */ public function __construct($output) { parent::__construct($output); $this->type = 'task'; $this->wrapper = 'vtodo'; } } diff --git a/tests/API/Contacts.php b/tests/API/Contacts.php index 0053048..97b3c55 100644 --- a/tests/API/Contacts.php +++ b/tests/API/Contacts.php @@ -1,187 +1,188 @@ 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', )); 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(204, $code); - $this->assertSame('', $body); + $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']); } /** * 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 b6bfddd..fbae553 100644 --- a/tests/API/Events.php +++ b/tests/API/Events.php @@ -1,217 +1,217 @@ 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']); } /** * 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', )); 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(204, $code); - $this->assertSame('', $body); + $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']); } - /** * 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/Folders.php b/tests/API/Folders.php index 57f1403..04e34eb 100644 --- a/tests/API/Folders.php +++ b/tests/API/Folders.php @@ -1,371 +1,372 @@ get('folders/test'); $code = self::$api->response_code(); $this->assertEquals(404, $code); // non-existing action self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/test'); $code = self::$api->response_code(); $this->assertEquals(404, $code); // existing action and folder, but wrong method self::$api->get('folders/' . kolab_api_tests::folder_uid('Mail-Test') . '/empty'); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test listing all folders */ function test_folder_list_folders() { // get all folders self::$api->get('folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(15, $body); $this->assertSame('Calendar', $body[0]['fullpath']); $this->assertSame('event.default', $body[0]['type']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['uid']); $this->assertNull($body[0]['parent']); // test listing subfolders of specified folder self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/folders'); $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('Calendar/Personal Calendar', $body[0]['fullpath']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['parent']); // get all folders with properties filter self::$api->get('folders', array('properties' => 'uid')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body[0]); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['uid']); } /** * Test folder delete */ function test_folder_delete() { // delete existing folder self::$api->delete('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing folder self::$api->delete('folders/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder existence */ function test_folder_exists() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing folder - deleted in test_folder_delete() self::$api->head('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder update */ function test_folder_update() { $post = json_encode(array( 'name' => 'Mail-Test22', 'type' => 'mail' )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test2'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); + $body = json_decode($body, true); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); + $this->assertSame(kolab_api_tests::folder_uid('Mail-Test2'), $body['uid']); // move into an existing folder $post = json_encode(array( 'name' => 'Trash', )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // change parent to an existing folder $post = json_encode(array( 'parent' => kolab_api_tests::folder_uid('Trash'), )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); - $this->assertEquals(204, $code); + $this->assertEquals(200, $code); } /** * Test folder create */ function test_folder_create() { $post = json_encode(array( 'name' => 'Test-create', 'type' => 'mail' )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create'), $body['uid']); // folder already exists $post = json_encode(array( 'name' => 'Test-create', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // create a subfolder $post = json_encode(array( 'name' => 'Test', 'parent' => kolab_api_tests::folder_uid('Test-create'), 'type' => 'mail' )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create/Test'), $body['uid']); // parent folder does not exists $post = json_encode(array( 'name' => 'Test-create-2', 'parent' => '123456789', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test folder info */ function test_folder_info() { self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('INBOX', $body['name']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body['uid']); } /** * Test folder create */ function test_folder_delete_objects() { $post = json_encode(array('10')); self::$api->post('folders/' . kolab_api_tests::folder_uid('Notes') . '/deleteobjects', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test folder create */ function test_folder_empty() { self::$api->post('folders/' . kolab_api_tests::folder_uid('Trash') . '/empty'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test listing folder content */ function test_folder_list_objects() { self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); // get all objects with properties filter self::$api->get('folders/' . kolab_api_tests::folder_uid('Notes') . '/objects', array('properties' => 'uid')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body); $this->assertCount(1, $body[0]); $this->assertSame('1-1-1-1', $body[0]['uid']); } /** * Test counting folder content */ function test_folder_count_objects() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX') . '/objects'); $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(4, (int) $count); // folder emptied in test_folder_empty() self::$api->head('folders/' . kolab_api_tests::folder_uid('Trash') . '/objects'); $count = self::$api->response_header('X-Count'); $this->assertSame(0, (int) $count); // one item removed in test_folder_delete_objects() self::$api->head('folders/' . kolab_api_tests::folder_uid('Notes') . '/objects'); $count = self::$api->response_header('X-Count'); $this->assertSame(2, (int) $count); } /** * Test moving objects from one folder to another */ function test_folder_move_objects() { $post = json_encode(array('100-100-100-100')); // invalid request: target == source self::$api->post('folders/' . kolab_api_tests::folder_uid('Calendar') . '/move/' . kolab_api_tests::folder_uid('Calendar'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); // move one object self::$api->post('folders/' . kolab_api_tests::folder_uid('Calendar') . '/move/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar') . '/objects'); $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('100-100-100-100', $body[0]['uid']); self::$api->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->assertCount(1, $body); $this->assertSame('101-101-101-101', $body[0]['uid']); // @TODO: the same for mail } } diff --git a/tests/API/Mails.php b/tests/API/Mails.php index 7143d3c..a08cdc3 100644 --- a/tests/API/Mails.php +++ b/tests/API/Mails.php @@ -1,245 +1,310 @@ 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(4, count($body)); $this->assertSame('1', $body[0]['uid']); $this->assertSame('"test" wurde aktualisiert', $body[0]['subject']); $this->assertSame('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') . '/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') . '/1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('1', $body['uid']); $this->assertSame('"test" wurde aktualisiert', $body['subject']); $this->assertSame(624, $body['size']); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/6'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('6', $body['uid']); } + /** + * Test counting mail attachments + */ + function test_count_attachments() + { + self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/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') . '/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') . '/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') . '/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() { -return; // @TODO $post = json_encode(array( - 'summary' => 'Test summary', - 'description' => 'Test description' + 'subject' => 'Test summary', + 'text' => 'This is the body.', )); 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']); + // folder does not exists $post = json_encode(array( - 'summary' => 'Test summary 2', + '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() { -return; // @TODO $post = json_encode(array( - 'summary' => 'Modified summary', - 'description' => 'Modified description' + 'subject' => 'Modified summary', + 'html' => 'now it is HTML', )); - self::$api->put('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1', array(), $post); + + 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(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); + $this->assertTrue(!empty($body['uid'])); - self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1'); + 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->assertSame('Modified summary', $body['summary']); - $this->assertSame('Modified description', $body['description']); + $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'])); + + // 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') . '/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() { // @TODO $this->markTestIncomplete('TODO'); } /** * Test mail delete */ function test_mail_delete() { // delete existing mail self::$api->delete('mails/' . kolab_api_tests::folder_uid('INBOX') . '/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); } - - /** - * Test counting mail attachments - */ - function test_count_attachments() - { - self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/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') . '/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') . '/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') . '/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']); - } } diff --git a/tests/API/Notes.php b/tests/API/Notes.php index 943b9d9..e3be857 100644 --- a/tests/API/Notes.php +++ b/tests/API/Notes.php @@ -1,213 +1,213 @@ 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']); } /** * 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(204, $code); - $this->assertSame('', $body); + $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(204, $code); - $this->assertSame('', $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); } - - /** - * Test counting task attachments - */ - function test_count_attachments() - { - // @TODO - } - - /** - * Test listing task attachments - */ - function test_list_attachments() - { - // @TODO - } } diff --git a/tests/API/Tasks.php b/tests/API/Tasks.php index ade9198..11e0bb7 100644 --- a/tests/API/Tasks.php +++ b/tests/API/Tasks.php @@ -1,234 +1,235 @@ 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']); } /** * 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', )); 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(204, $code); - $this->assertSame('', $body); + $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']); // 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(204, $code); - $this->assertSame('', $body); + $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/Mapistore/Contacts.php b/tests/Mapistore/Contacts.php index b9e5eb1..8a29615 100644 --- a/tests/Mapistore/Contacts.php +++ b/tests/Mapistore/Contacts.php @@ -1,232 +1,231 @@ get('folders/' . kolab_api_tests::folder_uid('Contacts') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts'), $body[0]['parent_id']); $this->assertSame('contacts', $body[0]['collection']); $this->assertSame('IPM.Contact', $body[0]['PidTagMessageClass']); $this->assertSame('displname', $body[0]['PidTagDisplayName']); } /** * Test contact existence */ function test_contact_exists() { self::$api->head('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, '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->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, '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::mapi_uid('Contacts', true, '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(kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d'), $body['id']); $this->assertSame(kolab_api_tests::folder_uid('Contacts'), $body['parent_id']); $this->assertSame('contacts', $body['collection']); $this->assertSame('IPM.Contact', $body['PidTagMessageClass']); $this->assertSame('displname', $body['PidTagDisplayName']); } /** * Test contact create */ function test_contact_create() { $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Contacts'), 'PidTagSurname' => 'lastname', 'PidTagTitle' => 'Test title', )); self::$api->post('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['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('non-existing'), 'PidTagSurname' => 'lastname', 'PidTagTitle' => 'Test title', )); self::$api->post('contacts', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Contacts'), 'test' => 'Test summary 2', )); self::$api->post('contacts', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test contact update */ function test_contact_update() { // @TODO: most of this should probably be in unit-tests not functional-tests $post = array( 'PidTagTitle' => 'Title', 'PidTagNickname' => 'Nickname', 'PidTagDisplayName' => 'DisplayName', 'PidTagSurname' => 'Surname', 'PidTagGivenName' => 'GivenName', 'PidTagMiddleName' => 'MiddleName', 'PidTagDisplayNamePrefix' => 'Prefix', 'PidTagGeneration' => 'Generation', 'PidTagBody' => 'Body', 'PidLidFreeBusyLocation' => 'FreeBusyLocation', 'PidTagCompanyName' => 'CompanyName', 'PidTagDepartmentName' => 'Department', 'PidTagProfession' => 'Profession', 'PidTagManagerName' => 'Manager', 'PidTagAssistant' => 'Assistant', 'PidTagPersonalHomePage' => 'HomePage', 'PidTagOtherAddressStreet' => 'OtherStreet', 'PidTagOtherAddressCity' => 'OtherCity', 'PidTagOtherAddressStateOrProvince' => 'OtherState', 'PidTagOtherAddressPostalCode' => 'OtherCode', 'PidTagOtherAddressCountry' => 'OtherCountry', // 'PidTagOtherAddressPostOfficeBox' => 'OtherBox', 'PidTagHomeAddressStreet' => 'HomeStreet', 'PidTagHomeAddressCity' => 'HomeCity', 'PidTagHomeAddressStateOrProvince' => 'HomeState', 'PidTagHomeAddressPostalCode' => 'HomeCode', 'PidTagHomeAddressCountry' => 'HomeCountry', // 'PidTagHomeAddressPostOfficeBox' => 'HomeBox', 'PidLidWorkAddressStreet' => 'WorkStreet', 'PidLidWorkAddressCity' => 'WorkCity', 'PidLidWorkAddressState' => 'WorkState', 'PidLidWorkAddressPostalCode' => 'WorkCode', 'PidLidWorkAddressCountry' => 'WorkCountry', // 'PidLidWorkAddressPostOfficeBox' => 'WorkBox', 'PidTagGender' => 1, 'PidTagSpouseName' => 'Spouse', 'PidTagChildrensNames' => array('child'), 'PidTagHomeTelephoneNumber' => 'HomeNumber', 'PidTagBusinessTelephoneNumber' => 'BusinessNumber', 'PidTagHomeFaxNumber' => 'HomeFax', 'PidTagBusinessFaxNumber' => 'BusinessFax', 'PidTagMobileTelephoneNumber' => 'Mobile', 'PidTagPagerTelephoneNumber' => 'Pager', 'PidTagCarTelephoneNumber' => 'Car', 'PidTagOtherTelephoneNumber' => 'OtherNumber', 'PidLidInstantMessagingAddress' => 'IM', 'PidLidEmail1EmailAddress' => 'email1@domain.tld', 'PidLidEmail2EmailAddress' => 'email1@domain.tld', 'PidLidEmail3EmailAddress' => 'email1@domain.tld', 'PidTagUserX509Certificate' => base64_encode('Certificate'), ); self::$api->put('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d'), array(), json_encode($post)); $code = self::$api->response_code(); $body = self::$api->response_body(); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); self::$api->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, 'a-b-c-d')); $body = self::$api->response_body(); $body = json_decode($body, true); foreach ($post as $idx => $value) { $this->assertSame($value, $body[$idx], "Test for update of $idx"); } } /** * Test contact delete */ function test_contact_delete() { // delete existing contact self::$api->delete('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, '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->get('contacts/' . kolab_api_tests::mapi_uid('Contacts', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/Mapistore/Events.php b/tests/Mapistore/Events.php index 71b147a..59e8eac 100644 --- a/tests/Mapistore/Events.php +++ b/tests/Mapistore/Events.php @@ -1,253 +1,251 @@ get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), $body[0]['id']); $this->assertSame('Summary', $body[0]['PidTagSubject']); $this->assertSame('Description', $body[0]['PidTagBody']); $this->assertSame('calendars', $body[0]['collection']); $this->assertSame('IPM.Appointment', $body[0]['PidTagMessageClass']); $this->assertSame(kolab_api_tests::mapi_uid('Calendar', true, '101-101-101-101'), $body[1]['id']); $this->assertSame(0, $body[1]['PidTagSensitivity']); $this->assertSame('calendars', $body[1]['collection']); $this->assertSame('IPM.Appointment', $body[1]['PidTagMessageClass']); } /** * Test event existence */ function test_event_exists() { self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '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('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '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('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '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(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), $body['id']); $this->assertSame('Summary', $body['PidTagSubject']); $this->assertSame('calendars', $body['collection']); $this->assertSame('IPM.Appointment', $body['PidTagMessageClass']); } /** * Test event create */ function test_event_create() { $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Calendar'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', 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['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); self::$api->post('calendars', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Calendar'), 'test' => 'Test summary 2', )); self::$api->post('calendars', 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( 'PidTagSubject' => 'Modified subject (1)', 'PidLidAppointmentStartWhole' => kolab_api_filter_mapistore::date_php2mapi('2015-01-01'), )); self::$api->put('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified subject (1)', $body['PidTagSubject']); } /** * Test counting event attachments */ function test_count_attachments() { self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); self::$api->head('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '101-101-101-101') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); } /** * Test listing event attachments */ function test_list_attachments() { self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '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(kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100', '3'), $body[0]['id']); $this->assertSame('image/jpeg', $body[0]['PidTagAttachMimeTag']); $this->assertSame('photo-mini.jpg', $body[0]['PidTagDisplayName']); $this->assertSame(793, $body[0]['PidTagAttachSize']); } /** * Test event delete */ function test_event_delete() { // delete existing event self::$api->delete('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '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('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test event update with moving to another folder */ function test_event_update_and_move() { // test event moving to another folder (by parent_id change) $post = json_encode(array( 'PidTagSubject' => 'Modified subject (2)', 'parent_id' => kolab_api_tests::folder_uid('Calendar/Personal Calendar'), )); self::$api->put('calendars/' . kolab_api_tests::mapi_uid('Calendar', true, '100-100-100-100'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); self::$api->get('calendars/' . kolab_api_tests::mapi_uid('Calendar/Personal Calendar', true, '100-100-100-100')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified subject (2)', $body['PidTagSubject']); } } diff --git a/tests/Mapistore/Folders.php b/tests/Mapistore/Folders.php index b04be3b..f2a31ae 100644 --- a/tests/Mapistore/Folders.php +++ b/tests/Mapistore/Folders.php @@ -1,297 +1,296 @@ get('folders/1/folders'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(12, $body); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('Calendar'), $body[1]['parent_id']); $this->assertNull($body[0]['parent_id']); $this->assertSame('IPF.Appointment', $body[0]['PidTagContainerClass']); $this->assertSame('IPF.Task', $body[10]['PidTagContainerClass']); // test listing subfolders of specified folder self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar') . '/folders'); $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(kolab_api_tests::folder_uid('Calendar'), $body[0]['parent_id']); // get all folders with properties filter self::$api->get('folders/1/folders', array('properties' => 'id')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array('id' => kolab_api_tests::folder_uid('Calendar')), $body[0]); } /** * Test folder delete */ function test_folder_delete() { // delete existing folder self::$api->delete('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing folder self::$api->get('folders/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder existence */ function test_folder_exists() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing folder - deleted in test_folder_delete() self::$api->get('folders/' . kolab_api_tests::folder_uid('Mail-Test')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test folder update */ function test_folder_update() { $post = json_encode(array( 'PidTagDisplayName' => 'Mail-Test22', )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test2'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); // move into an existing folder $post = json_encode(array( 'PidTagDisplayName' => 'Trash', )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // change parent to an existing folder $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Trash'), )); self::$api->put('folders/' . kolab_api_tests::folder_uid('Mail-Test22'), array(), $post); $code = self::$api->response_code(); - $this->assertEquals(204, $code); + $this->assertEquals(200, $code); } /** * Test folder create */ function test_folder_create() { $post = json_encode(array( 'PidTagDisplayName' => 'Test-create', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create'), $body['id']); // folder already exists $post = json_encode(array( 'PidTagDisplayName' => 'Test-create', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(500, $code); // create a subfolder $post = json_encode(array( 'PidTagDisplayName' => 'Test', 'parent_id' => kolab_api_tests::folder_uid('Test-create'), )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::folder_uid('Test-create/Test'), $body['id']); // parent folder does not exists $post = json_encode(array( 'PidTagDisplayName' => 'Test-create-2', 'parent_id' => '123456789', )); self::$api->post('folders', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test folder info */ function test_folder_info() { self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('INBOX', $body['PidTagDisplayName']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body['id']); } /** * Test folder create */ function test_folder_delete_objects() { $post = json_encode(array(array('id' => '10'))); self::$api->post('folders/' . kolab_api_tests::folder_uid('Notes') . '/deletemessages', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test folder create */ function test_folder_empty() { self::$api->post('folders/' . kolab_api_tests::folder_uid('Trash') . '/empty'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test listing folder content */ function test_folder_list_objects() { self::$api->get('folders/' . kolab_api_tests::folder_uid('Calendar/Personal Calendar') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); // get all objects with properties filter self::$api->get('folders/' . kolab_api_tests::folder_uid('Notes') . '/messages', array('properties' => 'id,collection')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body[0]); $this->assertTrue(!empty($body[0]['id'])); $this->assertSame('notes', $body[0]['collection']); } /** * Test counting folder content */ function test_folder_count_objects() { self::$api->head('folders/' . kolab_api_tests::folder_uid('INBOX') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(4, (int) $count); // folder emptied in test_folder_empty() self::$api->head('folders/' . kolab_api_tests::folder_uid('Trash') . '/mssages'); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertSame(0, (int) $count); // one item removed in test_folder_delete_objects() self::$api->head('folders/' . kolab_api_tests::folder_uid('Notes') . '/messages'); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertSame(2, (int) $count); } } diff --git a/tests/Mapistore/Mails.php b/tests/Mapistore/Mails.php index ed9032e..0488ff8 100644 --- a/tests/Mapistore/Mails.php +++ b/tests/Mapistore/Mails.php @@ -1,257 +1,254 @@ get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(4, count($body)); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, '1'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body[0]['parent_id']); $this->assertSame('mails', $body[0]['collection']); $this->assertSame('IPM.Note', $body[0]['PidTagMessageClass']); $this->assertSame('"test" wurde aktualisiert', $body[0]['PidTagSubject']); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, '2'), $body[1]['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body[1]['parent_id']); $this->assertSame('IPM.Note', $body[1]['PidTagMessageClass']); $this->assertSame('Re: dsda', $body[1]['PidTagSubject']); // get all messages with properties filter self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/messages', array('properties' => 'id')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array('id' => kolab_api_tests::mapi_uid('INBOX', true, '1')), $body[0]); } /** * Test mail existence check */ function test_mail_exists() { self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing note self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '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::mapi_uid('INBOX', true, '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::mapi_uid('INBOX', true, '1'), $body['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body['parent_id']); $this->assertSame('"test" wurde aktualisiert', $body['PidTagSubject']); $this->assertSame(624, $body['PidTagMessageSize']); $this->assertSame('IPM.Note', $body['PidTagMessageClass']); } + /** + * Test mail attachments count + */ + function test_count_attachments() + { + self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '2') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $count = self::$api->response_header('X-mapistore-rowcount'); + + $this->assertEquals(200, $code); + $this->assertSame('', $body); + $this->assertSame(0, (int) $count); + + self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '6') . '/attachments'); + + $code = self::$api->response_code(); + $body = self::$api->response_body(); + $count = self::$api->response_header('X-mapistore-rowcount'); + + $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::mapi_uid('INBOX', true, '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::mapi_uid('INBOX', true, '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(kolab_api_tests::mapi_uid('INBOX', true, '6', '2'), $body[0]['id']); + $this->assertSame('attachments', $body[0]['collection']); + $this->assertSame('text/plain', $body[0]['PidTagAttachMimeTag']); + $this->assertSame('test.txt', $body[0]['PidTagDisplayName']); + $this->assertSame('txt', $body[0]['PidTagAttachExtension']); + $this->assertSame(4, $body[0]['PidTagAttachSize']); + } + /** * Test mail create */ function test_mail_create() { -return; // @TODO $post = json_encode(array( - 'summary' => 'Test summary', - 'description' => 'Test description' + 'PidTagSubject' => 'Test summary', + 'PidTagBody' => 'Test description' )); 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'])); + $this->assertTrue(!empty($body['id'])); - // folder does not exists + // folder does not exist $post = json_encode(array( - 'summary' => 'Test summary 2', + 'PidTagSubject' => 'Test summary 2', )); self::$api->post('mails/' . md5('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 mail update */ function test_mail_update() { - $this->markTestIncomplete('TODO'); - return; // @TODO - $post = json_encode(array( - 'summary' => 'Modified summary', - 'description' => 'Modified description' + 'PidTagSubject' => 'Modified summary', + 'PidTagBody' => 'Modified description' )); - self::$api->put('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1'), array(), $post); + self::$api->put('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '2'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); + $body = json_decode($body, true); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); + $this->assertTrue(!empty($body['id'])); - self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1')); + self::$api->get('mails/' . $body['id']); $body = self::$api->response_body(); $body = json_decode($body, true); - $this->assertSame('Modified summary', $body['summary']); - $this->assertSame('Modified description', $body['description']); + $this->assertSame('Modified summary', $body['PidTagSubject']); + $this->assertSame('Modified description', $body['PidTagBody']); } /** * Test mail submit */ function test_mail_submit() { $this->markTestIncomplete('TODO'); } /** * Test mail delete */ function test_mail_delete() { // delete existing note self::$api->delete('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing note self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } - - /** - * Test mail attachments count - */ - function test_count_attachments() - { - self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '2') . '/attachments'); - - $code = self::$api->response_code(); - $body = self::$api->response_body(); - $count = self::$api->response_header('X-mapistore-rowcount'); - - $this->assertEquals(200, $code); - $this->assertSame('', $body); - $this->assertSame(0, (int) $count); - - self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '6') . '/attachments'); - - $code = self::$api->response_code(); - $body = self::$api->response_body(); - $count = self::$api->response_header('X-mapistore-rowcount'); - - $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::mapi_uid('INBOX', true, '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::mapi_uid('INBOX', true, '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(kolab_api_tests::mapi_uid('INBOX', true, '6', '2'), $body[0]['id']); - $this->assertSame('attachments', $body[0]['collection']); - $this->assertSame('text/plain', $body[0]['PidTagAttachMimeTag']); - $this->assertSame('test.txt', $body[0]['PidTagDisplayName']); - $this->assertSame('txt', $body[0]['PidTagAttachExtension']); - $this->assertSame(4, $body[0]['PidTagAttachSize']); - } } diff --git a/tests/Mapistore/Notes.php b/tests/Mapistore/Notes.php index 539efe8..5448f5b 100644 --- a/tests/Mapistore/Notes.php +++ b/tests/Mapistore/Notes.php @@ -1,206 +1,205 @@ get('folders/' . kolab_api_tests::folder_uid('Notes') . '/messages'); $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(kolab_api_tests::mapi_uid('Notes', true, '1-1-1-1'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('Notes'), $body[0]['parent_id']); $this->assertSame('test', $body[0]['PidTagSubject']); $this->assertSame('notes', $body[0]['collection']); $this->assertSame('IPM.StickyNote', $body[0]['PidTagMessageClass']); $this->assertSame(kolab_api_tests::mapi_uid('Notes', true, '2-2-2-2'), $body[1]['id']); $this->assertSame('wwww', $body[1]['PidTagSubject']); $this->assertSame('notes', $body[1]['collection']); $this->assertSame('IPM.StickyNote', $body[1]['PidTagMessageClass']); } /** * Test note existence */ function test_note_exists() { self::$api->head('notes/' . kolab_api_tests::mapi_uid('Notes', true, '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->get('notes/' . kolab_api_tests::mapi_uid('Notes', true, '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::mapi_uid('Notes', true, '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(kolab_api_tests::mapi_uid('Notes', true, '1-1-1-1'), $body['id']); $this->assertSame(kolab_api_tests::folder_uid('Notes'), $body['parent_id']); $this->assertSame('test', $body['PidTagSubject']); } /** * Test note create */ function test_note_create() { $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Notes'), 'PidTagSubject' => 'Test summary', 'PidTagBody' => 'Test description' )); self::$api->post('notes', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $uid = $body['id']; $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($uid)); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary 2', )); self::$api->post('notes', 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', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test note update */ function test_note_update() { $post = json_encode(array( 'PidTagSubject' => 'Modified summary', 'PidTagBody' => 'Modified description', )); self::$api->put('notes/' . kolab_api_tests::mapi_uid('Notes', true, '1-1-1-1'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); self::$api->get('notes/' . kolab_api_tests::mapi_uid('Notes', true, '1-1-1-1')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary', $body['PidTagSubject']); $this->assertSame('Modified description', $body['PidTagBody']); } /** * Test note delete */ function test_note_delete() { // delete existing note self::$api->delete('notes/' . kolab_api_tests::mapi_uid('Notes', true, '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->get('notes/' . kolab_api_tests::mapi_uid('Notes', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test mail attachments count */ function test_count_attachments() { // notes do not have attachments in MAPI self::$api->head('notes/' . kolab_api_tests::mapi_uid('Notes', true, '1-1-1-1') . '/attachments'); $code = self::$api->response_code(); $this->assertEquals(404, $code); } /** * Test listing mail attachments */ function test_list_attachments() { // notes do not have attachments in MAPI self::$api->get('notes/' . kolab_api_tests::mapi_uid('Notes', true, '1-1-1-1') . '/attachments'); $code = self::$api->response_code(); $this->assertEquals(404, $code); } } diff --git a/tests/Mapistore/Tasks.php b/tests/Mapistore/Tasks.php index 783f0ed..7f682b8 100644 --- a/tests/Mapistore/Tasks.php +++ b/tests/Mapistore/Tasks.php @@ -1,238 +1,237 @@ 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(kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks'), $body[0]['parent_id']); $this->assertSame('IPM.Task', $body[0]['PidTagMessageClass']); $this->assertSame('tasks', $body[0]['collection']); $this->assertSame('task title', $body[0]['PidTagSubject']); $this->assertSame(kolab_api_tests::mapi_uid('Tasks', true, '20-20-20-20'), $body[1]['id']); $this->assertSame(kolab_api_tests::folder_uid('Tasks'), $body[1]['parent_id']); $this->assertSame('IPM.Task', $body[1]['PidTagMessageClass']); $this->assertSame('tasks', $body[1]['collection']); $this->assertSame('task', $body[1]['PidTagSubject']); } /** * Test task existence */ function test_task_exists() { self::$api->head('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '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::mapi_uid('Tasks', true, '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::mapi_uid('Tasks', true, '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(kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10'), $body['id']); $this->assertSame('task title', $body['PidTagSubject']); $this->assertSame("task description\nsecond line", $body['PidTagBody']); } /** * Test task create */ function test_task_create() { $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Tasks'), 'PidTagSubject' => 'Test summary', 'PidTagBody' => 'Test description', )); self::$api->post('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['id'])); // folder does not exists $post = json_encode(array( 'parent_id' => md5('non-existing'), 'PidTagSubject' => 'Test summary 2', )); self::$api->post('tasks', array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'parent_id' => kolab_api_tests::folder_uid('Tasks'), 'test' => 'Test summary 2', )); self::$api->post('tasks', array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test task update */ function test_task_update() { $post = json_encode(array( 'PidTagSubject' => 'Modified summary', 'PidTagBody' => 'Modified description' )); self::$api->put('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); - $this->assertEquals(204, $code); - $this->assertSame('', $body); + $this->assertEquals(200, $code); self::$api->get('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10')); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary', $body['PidTagSubject']); $this->assertSame('Modified description', $body['PidTagBody']); } /** * Test counting task attachments */ function test_count_attachments() { // task with an attachment self::$api->head('tasks/' . kolab_api_tests::mapi_uid('Tasks', true,'10-10-10-10') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(1, (int) $count); // task with no attachments self::$api->head('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '20-20-20-20') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); } /** * Test listing task attachments */ function test_list_attachments() { self::$api->get('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '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(kolab_api_tests::mapi_uid('Tasks', true, '10-10-10-10', 3), $body[0]['id']); $this->assertSame('text/plain', $body[0]['PidTagAttachMimeTag']); $this->assertSame('test.txt', $body[0]['PidTagDisplayName']); $this->assertSame(4, $body[0]['PidTagAttachSize']); // task with no attachments self::$api->get('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '20-20-20-20') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(0, $body); } /** * Test task delete */ function test_task_delete() { // delete existing task self::$api->delete('tasks/' . kolab_api_tests::mapi_uid('Tasks', true, '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::mapi_uid('Tasks', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/Unit/Filter/Mapistore/Mail.php b/tests/Unit/Filter/Mapistore/Mail.php index 8ee395e..03d2f60 100644 --- a/tests/Unit/Filter/Mapistore/Mail.php +++ b/tests/Unit/Filter/Mapistore/Mail.php @@ -1,73 +1,152 @@ 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']); $data = kolab_api_tests::get_data('2', 'INBOX', 'mail', 'json', $context); $result = $api->output($data, $context); - $this->assertSame(kolab_api_tests::mapi_uid('INBOX', false, '2'), $result['id']); - $this->assertSame(kolab_api_tests::folder_uid('INBOX', false), $result['parent_id']); $this->assertSame('IPM.Note', $result['PidTagMessageClass']); $this->assertSame('Re: dsda', $result['PidTagSubject']); - $this->assertSame(807, $result['PidTagMessageSize']); - $this->assertCount(1, $result['recipients']); + $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']); + + // @TODO: sender/from } /** * Test input method */ function test_input() { - // @TODO - $this->markTestIncomplete('TODO'); + $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() { - // @TODO - $this->markTestIncomplete('TODO'); + $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/Input/Json/Mail.php b/tests/Unit/Input/Json/Mail.php index 61f741d..7a1c915 100644 --- a/tests/Unit/Input/Json/Mail.php +++ b/tests/Unit/Input/Json/Mail.php @@ -1,56 +1,162 @@ input($data); } /** * Test expected exception in input method * * @expectedException kolab_api_exception * @expectedExceptionCode 422 */ function test_input_exception2() { $input = new kolab_api_input_json_mail; $data = 'test'; $input->input($data); } /** * Test input method (convert JSON to internal format) */ function test_input() { - // @TODO - $this->markTestIncomplete('TODO'); + $input = new kolab_api_input_json_mail; + $data = array( + 'subject' => 'subject', + 'from' => array( + 'address' => 'mark.german@example.org', + 'name' => 'German, Mark' + ), + 'to' => array(array( + 'address' => 'mark.german@example.org', + 'name' => 'German, Mark' + )), + 'cc' => array(array( + 'address' => 'cc@example.org', + )), + 'bcc' => array(array( + 'address' => 'bcc@example.org', + )), + 'reply-to' => array(array( + 'address' => 'reply@example.org', + )), + 'priority' => 5, + 'flags' => array('deleted'), + 'categories' => array('test'), + 'date' => '2015-01-01 01:00:00 UTC', + 'text' => 'test', + 'html' => 'test', + ); + + $input->input($data); + + $this->assertInstanceOf('kolab_api_mail', $data); + $this->assertSame('subject', $data->subject); + $this->assertSame(5, $data->priority); + $this->assertSame(null, $data->size); + $this->assertSame('2015-01-01 01:00:00 UTC', $data->date); + $this->assertSame(array( + 'address' => 'mark.german@example.org', + 'name' => 'German, Mark' + ), $data->from); + $this->assertSame(array(array( + 'address' => 'mark.german@example.org', + 'name' => 'German, Mark' + )), $data->to); + $this->assertSame(array(array( + 'address' => 'cc@example.org', + )), $data->cc); + $this->assertSame(array(array( + 'address' => 'bcc@example.org', + )), $data->bcc); + $this->assertSame(array(array( + 'address' => 'reply@example.org', + )), $data->{'reply-to'}); + $this->assertSame('test', $data->text); + $this->assertSame('test', $data->html); + + self::$original = $data; } /** * Test input method with merging (?) */ function test_input2() { - // @TODO - $this->markTestIncomplete('TODO'); + $input = new kolab_api_input_json_mail; + $data = array( + 'subject' => 'subject1', + 'from' => array( + 'address' => 'mark1.german@example.org', + 'name' => 'German, Mark' + ), + 'to' => array(array( + 'address' => 'mark1.german@example.org', + 'name' => 'German, Mark' + )), + 'cc' => array(array( + 'address' => 'cc1@example.org', + )), + 'bcc' => array(array( + 'address' => 'bcc1@example.org', + )), + 'reply-to' => array(array( + 'address' => 'reply1@example.org', + )), + 'priority' => 2, + 'flags' => array('deleted1'), + 'categories' => array('test1'), + 'date' => '2015-01-02 01:00:00 UTC', + 'text' => 'test1', + 'html' => 'test1', + ); + + $input->input($data, self::$original); + + $this->assertInstanceOf('kolab_api_mail', $data); + $this->assertSame('subject1', $data->subject); + $this->assertSame(2, $data->priority); + $this->assertSame('2015-01-02 01:00:00 UTC', $data->date); + $this->assertSame(array( + 'address' => 'mark1.german@example.org', + 'name' => 'German, Mark' + ), $data->from); + $this->assertSame(array(array( + 'address' => 'mark1.german@example.org', + 'name' => 'German, Mark' + )), $data->to); + $this->assertSame(array(array( + 'address' => 'cc1@example.org', + )), $data->cc); + $this->assertSame(array(array( + 'address' => 'bcc1@example.org', + )), $data->bcc); + $this->assertSame(array(array( + 'address' => 'reply1@example.org', + )), $data->{'reply-to'}); + $this->assertSame('test1', $data->text); + $this->assertSame('test1', $data->html); } } diff --git a/tests/Unit/Output/Json/Mail.php b/tests/Unit/Output/Json/Mail.php index a22a316..217718c 100644 --- a/tests/Unit/Output/Json/Mail.php +++ b/tests/Unit/Output/Json/Mail.php @@ -1,49 +1,63 @@ element($object); $this->assertSame('1', $result['uid']); $this->assertSame('"test" wurde aktualisiert', $result['subject']); $this->assertSame(624, $result['size']); $this->assertSame(array( 'address' => 'mark.german@example.org', 'name' => 'Mark German' ), $result['from']); $this->assertSame(array(array( 'address' => 'mark.german@example.org' )), $result['to']); $this->assertSame('text/plain', $result['content-type']); $object = kolab_api_tests::get_data('2', 'INBOX', 'mail', null, $context); $result = $output->element($object); $this->assertSame('2', $result['uid']); $this->assertSame('Re: dsda', $result['subject']); - $this->assertSame(807, $result['size']); + $this->assertSame(849, $result['size']); $this->assertSame('text/plain', $result['content-type']); $this->assertSame(array( 'address' => 'mark.german@example.org', 'name' => 'German, Mark' ), $result['from']); $this->assertSame(array(array( 'address' => 'mark.german@example.org', 'name' => 'German, Mark' )), $result['to']); + $this->assertRegexp('/^On 2014-08-24/', $result['text']); + $this->assertSame(null, $result['html']); + $this->assertSame('<5071c053c207e5916123b0aa36959f03@example.org>', $result['message-id']); + $this->assertSame('', $result['in-reply-to']); + $this->assertSame('', $result['references']); + + // @TODO: categories, flags + + $object = kolab_api_tests::get_data('5', 'INBOX', 'mail', null, $context); + $result = $output->element($object); + + $this->assertSame('5', $result['uid']); + $this->assertRegexp('/test content/', $result['html']); + $this->assertRegexp('/TEST CONTENT/', $result['text']); } } diff --git a/tests/data/mail/2 b/tests/data/mail/2 index a88a2f2..bd30af9 100644 --- a/tests/data/mail/2 +++ b/tests/data/mail/2 @@ -1,21 +1,22 @@ Return-Path: Received: from kolab32.example.org ([unix socket]) by kolab32.example.org (Cyrus git2.5+0-Kolab-2.5-96.el6.kolab_3.4) with LMTPA; Mon, 08 Dec 2014 07:38:11 -0500 X-Sieve: CMU Sieve 2.4 MIME-Version: 1.0 Content-Type: text/plain; charset=US-ASCII; format=flowed Content-Transfer-Encoding: 7bit Date: Mon, 08 Dec 2014 13:38:09 +0100 From: "German, Mark" To: "German, Mark" +Cc: "Pitt, Brad" Subject: Re: dsda In-Reply-To: References: Message-ID: <5071c053c207e5916123b0aa36959f03@example.org> X-Sender: mark.german@example.org User-Agent: Kolab 3.1/Roundcube 1.1-git On 2014-08-24 12:51, German, Mark wrote: > yery diff --git a/tests/index.php b/tests/index.php index 463be97..915375f 100644 --- a/tests/index.php +++ b/tests/index.php @@ -1,46 +1,48 @@ | | | | 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 | +--------------------------------------------------------------------------+ */ // environment initialization require_once __DIR__ . '/../lib/init.php'; try { $API = kolab_api::get_instance(); // If tests_username is set we use real Kolab server // otherwise use dummy backend class that simulates a real server if (!$API->config->get('tests_username')) { - // Replace kolab_api_backend + // Load backend wrappers for tests // @TODO: maybe we could replace kolab_storage and rcube_imap instead? require_once __DIR__ . '/lib/kolab_api_backend.php'; require_once __DIR__ . '/lib/kolab_api_message.php'; + + // Load some helper methods for tests require_once __DIR__ . '/lib/kolab_api_tests.php'; } $API->run(); } catch (Exception $e) { kolab_api::exception_handler($e); } diff --git a/tests/lib/bootstrap.php b/tests/lib/bootstrap.php index e86f0f7..2ace955 100644 --- a/tests/lib/bootstrap.php +++ b/tests/lib/bootstrap.php @@ -1,40 +1,40 @@ | | | | 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 | +--------------------------------------------------------------------------+ */ // environment initialization require_once __DIR__ . '/../../lib/init.php'; // load HTTP_Request2 wrapper require_once __DIR__ . '/kolab_api_request.php'; -// load message wrapper for tests +// load wrappers for tests require_once __DIR__ . '/kolab_api_message.php'; // load tests utils require_once __DIR__ . '/kolab_api_tests.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); diff --git a/tests/lib/kolab_api_backend.php b/tests/lib/kolab_api_backend.php index 323c0f8..7e2b91d 100644 --- a/tests/lib/kolab_api_backend.php +++ b/tests/lib/kolab_api_backend.php @@ -1,714 +1,891 @@ | +--------------------------------------------------------------------------+ | 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; protected $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 rcube_message_header or kolab_format) + * @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 = $object->headers; + $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 rcube_message|array Object data + * @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 = file_get_contents(__DIR__ . '/../data/' . $type . '/' . $uid); + $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; } else { + $object = new kolab_api_message($object); $object->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); } */ - $data['uid'] = microtime(true); + $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($data['categories'])) { + 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'][] = $data['uid']; + $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; - $this->db['items'][$folder_uid][$data['uid']] = $data; $this->save_db(); - return $data['uid']; + 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); } */ - 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; - } + if (is_array($data)) { + $uid = $data['uid']; - $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'])); + 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'])); + } } + + // remove _formatobj which is problematic in serialize/unserialize + unset($data['_formatobj']); + + $this->db['items'][$folder_uid][$uid] = $data; + $this->save_db(); } + else { + $uid = $data->save($folder['fullpath']); - // remove _formatobj which is problematic in serialize/unserialize - unset($data['_formatobj']); - $this->db['items'][$folder_uid][$data['uid']] = $data; - $this->save_db(); + $this->folders[$folder_uid]['items'][] = $uid; + $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; + $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']; } // check if it's not deleted if (in_array($msg_uid . ":" . $part_id, (array) $this->db['deleted_attachments'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $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 boolean|string True or message UID (if changed) * @throws kolab_api_exception */ public function attachment_delete($object, $id) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; $key = $msg_uid . ":" . $part_id; // check if it's not deleted if (in_array($key, (array) $this->db['deleted_attachments'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // 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); } $this->db['deleted_attachments'][] = $key; $this->save_db(); } /** * 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 */ protected 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($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, 'mail'); + $file = explode("\r\n\r\n", $file, 2); + + // we assume $part=TEXT + + if ($fp) { + fwrite($fp, $file[1]); + return true; + } + else { + echo $file[1]; + } + } + + /** + * Wrapper for rcube_imap::get_raw_headers + */ + public function get_raw_headers($uid) + { + $file = $this->get_file_content($uid, 'mail'); + $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) + { + if ($file = $this->db['messages'][$uid]) { + $file = base64_decode($file); + } + else { + $file = file_get_contents(__DIR__ . '/../data/' . $type . '/' . $uid); + } + + return $file; + } +} + +/** + * Dummy class imitating rcube_user + */ +class kolab_api_user +{ + public function get_username() + { + return 'user@example.org'; + } + + public function get_user_id() + { + return 10; + } } diff --git a/tests/lib/kolab_api_message.php b/tests/lib/kolab_api_message.php index 761b9dd..0cb6340 100644 --- a/tests/lib/kolab_api_message.php +++ b/tests/lib/kolab_api_message.php @@ -1,139 +1,237 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class kolab_api_message +class kolab_api_message extends kolab_api_mail { public $attachments = array(); + public $parts = array(); + public $mime_parts = array(); + public $folder; + public $uid; + public $headers; + /** * Class initialization */ public function __construct($content, $params = array()) { - $this->msg = rcube_mime::parse_message($content); - $this->headers = rcube_message_header::from_array($this->msg->headers); + // 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->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) { + $this->attachments[] = $part; + } - foreach ((array) $this->msg->parts as $part) { - if ($part->filename) { - $this->attachments[] = $part; + $this->parts[$part->mime_id] = $part; } - $this->parts[$part->mime_id] = $part; } foreach ((array) $params as $idx => $val) { - $this->headers->{$idx} = $val; + $this->{$idx} = $val; } - - $this->headers->size = strlen($content); } + /** + * Returns body of the message part + */ public function get_part_body($id) { if (!$id) { - return $this->msg->body; + return $this->message->body; } - return $this->parts[$id]->body; + return $this->mime_parts[$id]->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, true); + $xml = $this->get_part_body($part->mime_id); } else if ($part->filename || $part->content_id) { $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]; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, '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 + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function send() + { + // @TODO + } + + /** + * 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; + } + + /** + * 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_request.php b/tests/lib/kolab_api_request.php index 470cba7..2411e8a 100644 --- a/tests/lib/kolab_api_request.php +++ b/tests/lib/kolab_api_request.php @@ -1,214 +1,214 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_request { private $request; private $base_url; /** * Class initialization */ public function __construct($base_url, $user, $pass) { require_once 'HTTP/Request2.php'; $this->base_url = $base_url; $this->request = new HTTP_Request2(); $this->request->setConfig(array( 'ssl_verify_peer' => false, 'ssl_verify_host' => false, )); $this->request->setAuth($user, $pass); } /** * Set request header */ public function set_header($name, $value) { $this->request->setHeader($name, $value); } /** * API's GET request. * * @param string URL * @param array URL arguments * * @return HTTP_Request2_Response Response object */ public function get($url, $args = array()) { $url = $this->build_url($url, $args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_GET); return $this->get_response($url); } /** * API's HEAD request. * * @param string URL * @param array URL arguments * * @return HTTP_Request2_Response Response object */ public function head($url, $args = array()) { $url = $this->build_url($url, $args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_HEAD); return $this->get_response($url); } /** * API's DELETE request. * * @param string URL * @param array URL arguments * * @return HTTP_Request2_Response Response object */ public function delete($url, $args = array()) { $url = $this->build_url($url, $args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_DELETE); return $this->get_response($url); } /** * API's POST request. * * @param string URL * @param array URL arguments * @param string POST body * * @return HTTP_Request2_Response Response object */ public function post($url, $url_args = array(), $post = '') { $url = $this->build_url($url, $url_args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_POST); $this->request->setBody($post); return $this->get_response($url); } /** * API's PUT request. * * @param string URL * @param array URL arguments * @param string PUT body * * @return HTTP_Request2_Response Response object */ public function put($url, $url_args = array(), $post = '') { $url = $this->build_url($url, $url_args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_PUT); $this->request->setBody($post); return $this->get_response($url); } public function response_code() { return $this->response->getStatus(); } public function response_header($name) { return $this->response->getHeader($name); } public function response_body() { return $this->response->getBody(); } /** * @param string Action URL * @param array GET parameters (hash array: name => value) * * @return Net_URL2 URL object */ private function build_url($action, $args) { $url = $this->base_url; if ($action) { $url .= '/' . $action; } $url = new Net_URL2($url); if (!empty($args)) { $url->setQueryVariables($args); } return $url; } /** * Reset old request data */ private function reset() { // reset old body $this->request->setBody(''); unset($this->response); } /** * HTTP Response handler. * * @param Net_URL2 URL object * * @return HTTP_Request2_Response Response object */ private function get_response($url) { $this->request->setUrl($url); return $this->response = $this->request->send(); } } diff --git a/tests/lib/kolab_api_tests.php b/tests/lib/kolab_api_tests.php index e0e540d..6f3cdb4 100644 --- a/tests/lib/kolab_api_tests.php +++ b/tests/lib/kolab_api_tests.php @@ -1,131 +1,134 @@ | | | | 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 { /** * Reset backend state */ public static function reset_backend() { // @TODO: reseting real server $rcube = rcube::get_instance(); $temp_dir = $rcube->config->get('temp_dir'); $filename = $temp_dir . '/tests.db'; if (file_exists($filename)) { unlink($filename); } } /** * 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'); $password = $rcube->config->get('tests_password', 'test'); 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) { $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 ($type != 'mail') { $object = $object->to_array($type); } + else { + $object = new kolab_api_message($object); + } $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) { // @TODO: get real UID from IMAP when testing on a real server // and $api_test = true return md5($name); } /** * 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); return kolab_api_filter_mapistore::uid_encode($folder_uid, $msg_uid, $attachment_uid); } }