diff --git a/lib/api/attachments.php b/lib/api/attachments.php index 436535f..1ae78a6 100644 --- a/lib/api/attachments.php +++ b/lib/api/attachments.php @@ -1,436 +1,441 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_attachments extends kolab_api { protected $model = 'attachment'; public function run() { $this->initialize_handler(); // Load required attachments plugin (for uploads handling) if (!class_exists('filesystem_attachments', false)) { $this->plugins->load_plugin('filesystem_attachments', true); } $path = $this->input->path; $method = $this->input->method; if ($path[0] == 'upload' && count($path) == 1 && $method == 'POST') { $this->api_attachment_upload(); } else 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 an 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); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); // parse input, attach uploaded file, set input properties $attachment = $this->handle_attachment_input(); // add the attachment to the message/object $uid = $this->backend->attachment_create($object, $attachment); // @TODO: should we keep the uploaded file for longer? if ($attachment->upload_id) { unset($_SESSION['uploads'][$attachment->upload_id]); } $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]; $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 $old_attachment = $this->get_attachment($object, $attach_uid); // parse input, attach uploaded file, set input properties $attachment = $this->handle_attachment_input(); // merge attachment properties $modified = false; $fields = array('path', 'data', 'size', 'filename', 'mimetype', 'disposition', 'content_id', 'content_location'); foreach ($fields as $idx) { if (isset($attachment->{$idx})) { $old_attachment->{$idx} = $attachment->{$idx}; $modified = true; } } if (!$modified) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } // update the attachment $uid = $this->backend->attachment_update($object, $old_attachment); // @TODO: should we keep the uploaded file for longer? if ($attachment->upload_id) { unset($_SESSION['uploads'][$atttachment->upload_id]); } $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * 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); $context = array( 'folder_uid' => $folder, 'object_uid' => $object_uid, 'object' => $object ); $uid = $this->backend->attachment_delete($object, $attach_uid); $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * 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; } /** * Upload attachment body */ protected function api_attachment_upload() { // Binary file content in POST body if (strtolower(rcube_utils::request_header('Content-Type')) == 'application/octet-stream') { $length = rcube_utils::request_header('Content-Length'); $temp_dir = unslashify($this->config->get('temp_dir')); $path = tempnam($temp_dir, 'kolabUpload'); // Create stream to copy input into a temp file $input = fopen('php://input', 'r'); $file = fopen($path, 'w'); if (!$input || !$file) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Failed opening input or temp file stream", )); } // Create temp file from the input $copied = stream_copy_to_stream($input, $file); fclose($input); fclose($file); if ($copied < $length) { // @FIXME: can we distinguish reliably when size error should be displayed? if ($maxsize = parse_bytes(ini_get('post_max_size'))) { $error = "Maximum file size exceeded (" . $this->show_bytes($maxsize) . ")"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Failed copying file upload into a temp file", )); } $name = rcube_utils::request_header('X-Filename') ?: $this->input->args['filename']; // Use Roundcube attachments functionality to give // redundant_attachments/database_attachments plugins a chance $attachment = array( 'group' => 'kolab_upload', 'name' => $name, 'mimetype' => rcube_mime::file_content_type($path, $name), 'path' => $path, 'size' => filesize($path), ); $attachment = $this->plugins->exec_hook('attachment_save', $attachment); if ($attachment['status']) { unset($attachment['data'], $attachment['status'], $attachment['content_id'], $attachment['abort']); $_SESSION['uploads'][$id = $attachment['id']] = $attachment; } else { @unlink($path); throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Failed handling file upload", )); } } /* else if (($path = $_FILES['file']['tmp_name']) && is_string($path)) { $err = $_FILES['file']['error']; if (!$err) { $attachment = $this->plugins->exec_hook('attachment_upload', array( 'path' => $path, 'size' => $_FILES['file']['size'], 'name' => $_FILES['file']['name'], 'mimetype' => rcube_mime::file_content_type($path, $_FILES['file']['name'], $_FILES['file']['type']), 'group' => 'kolab_upload', )); } if (!$err && $attachment['status']) { $id = $attachment['id']; // store new attachment in session unset($attachment['status'], $attachment['abort']); $_SESSION['uploads'][$id] = $attachment; } else { // upload failed if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) { $maxsize = parse_bytes(ini_get('upload_max_filesize')); $error = "Maximum file size exceeded (" . $this->show_bytes($maxsize) . ")"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Failed handling file upload", )); } } else { // if filesize exceeds post_max_size then $_FILES array is empty, // show filesizeerror instead of fileuploaderror if ($maxsize = parse_bytes(ini_get('post_max_size'))) { $error = "Maximum file size exceeded (" . $this->show_bytes($maxsize) . ")"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'message' => "Failed handling file upload", )); } */ else { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $this->output->send(array('upload-id' => $id), $this->model); } /** * 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); } /** * Create a human readable string for a number of bytes * * @param int Number of bytes * * @return string Byte string */ public static function show_bytes($bytes) { if ($bytes >= 1073741824) { $gb = $bytes/1073741824; $str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . 'GB'; } else if ($bytes >= 1048576) { $mb = $bytes/1048576; $str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . 'MB'; } else if ($bytes >= 1024) { $str = sprintf("%d ", round($bytes/1024)) . 'KB'; } else { $str = sprintf('%d ', $bytes) . 'B'; } return $str; } /** * Handle attachment input data with uploads handling */ protected function handle_attachment_input() { $attachment = $this->input->input('attachment'); // if upload-id is specified check if it exists $upload_id = $attachment->upload_id ?: 0; $session = $_SESSION['uploads'][$upload_id]; if ($upload_id && empty($session)) { $error = "Invalid file upload identifier"; throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, $error); } - else if ($session) { + + if ($session) { // get attachment from attachment storage $session = $this->plugins->exec_hook('attachment_get', $session); $fallback_fields = array( 'name' => 'filename', 'mimetype' => 'mimetype', ); foreach ($fallback_fields as $sess_idx => $attach_idx) { if (!empty($session[$sess_idx])) { $attachment->{$attach_idx} = $session[$sess_idx]; } } $fields = array('path', 'data', 'size'); foreach ($fields as $idx) { $attachment->{$idx} = $session[$idx]; } } + else { + // unset some fields + $attachment->size = null; + } return $attachment; } } diff --git a/lib/filter/mapistore/timezone.php b/lib/filter/mapistore/timezone.php index ea378c1..0a9cde2 100644 --- a/lib/filter/mapistore/timezone.php +++ b/lib/filter/mapistore/timezone.php @@ -1,457 +1,461 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Utility functions for converting Timezone from/to MAPI format(s). */ class kolab_api_filter_mapistore_timezone { public $Bias = 0; public $StandardYear = 0; public $StandardMonth = 0; public $StandardDayOfWeek = 0; public $StandardDay = 0; public $StandardHour = 0; public $StandardMinute = 0; public $StandardSecond = 0; public $StandardMilliseconds = 0; public $StandardBias = 0; public $DaylightYear = 0; public $DaylightMonth = 0; public $DaylightDay = 0; public $DaylightDayOfWeek = 0; public $DaylightHour = 0; public $DaylightMinute = 0; public $DaylightSecond = 0; public $DaylightMilliseconds = 0; public $DaylightBias = 0; /** * PidLidTimeZone property values * * @var array */ protected static $tz_data = array( 0 => array(0, null, null), 1 => array(720, array(10, 0, 5, 2), array(3, 0, 5, 1)), 2 => array(660, array(9, 0, 5, 2), array(3, 0, 5, 1)), 3 => array(660, array(10, 0, 5, 3), array(3, 0, 5, 2)), 4 => array(660, array(10, 0, 5, 3), array(3, 0, 5, 2)), 5 => array(600, array(9, 0, 5, 1), array(3, 0, 5, 0)), 6 => array(660, array(9, 0, 5, 1), array(3, 0, 5, 0)), 7 => array(600, array(10, 0, 5, 4), array(3, 0, 5, 3)), 8 => array(900, array(2, 0, 2, 2), array(10, 0, 3, 2)), 9 => array(960, array(11, 0, 1, 2), array(3, 0, 2, 2)), 10 => array(1020, array(11, 0, 1, 2), array(3, 0, 2, 2)), 11 => array(1080, array(11, 0, 1, 2), array(3, 0, 2, 2)), 12 => array(1140, array(11, 0, 1, 2), array(3, 0, 2, 2)), 13 => array(1200, array(11, 0, 1, 2), array(3, 0, 2, 2)), 14 => array(1260, array(11, 0, 1, 2), array(3, 0, 2, 2)), 15 => array(1320, null, null), 16 => array(1380, null, null), 17 => array(0, array(4, 0, 1, 3), array(9, 0, 5, 2)), 18 => array(120, array(3, 0, 5, 3), array(10, 0, 5, 2)), 19 => array(150, array(3, 0, 5, 3), array(10, 0, 5, 2)), 20 => array(180, null, null), 21 => array(240, null, null), 22 => array(300, null, null), 23 => array(390, null, null), 24 => array(480, null, null), 25 => array(510, array(9, 2, 4, 2), array(3, 0, 1, 2)), 26 => array(540, null, null), 27 => array(600, array(9, 0, 3, 2), array(3, 5, 5, 2)), 28 => array(930, array(11, 0, 1, 0), array(3, 0, 2, 0)), 29 => array(780, array(10, 0, 5, 1), array(3, 0, 5, 0)), 30 => array(840, array(10, 0, 5, 1), array(3, 0, 5, 0)), 31 => array(720, null, null), 32 => array(900, null, null), 33 => array(960, null, null), 34 => array(1020, null, null), 35 => array(10200, null, null), 36 => array(1080, null, null), 37 => array(1080, array(10, 0, 5, 2), array(4, 0, 1, 2)), 38 => array(1140, null, null), 39 => array(1440, null, null), 40 => array(0, null, null), 41 => array(60, null, null), 42 => array(120, array(3, 0, 5, 2), array(10, 0, 1, 2)), 43 => array(120, null, null), 44 => array(150, null, null), 45 => array(240, array(9, 0, 2, 2), array(4, 0, 2, 2)), 46 => array(360, null, null), 47 => array(420, null, null), 48 => array(450, null, null), 49 => array(600, array(9, 4, 5, 2), array(5, 5, 1, 2)), 50 => array(600, null, null), 51 => array(540, array(10, 0, 5, 1), array(3, 0, 5, 0)), 52 => array(120, array(3, 0, 5, 2), array(8, 0, 5, 2)), 53 => array(120, array(4, 0, 1, 3), array(10, 0, 5, 2)), 54 => array(150, array(4, 0, 1, 3), array(10, 0, 5, 2)), 55 => array(120, array(4, 0, 1, 3), array(10, 0, 1, 2)), 56 => array(960, array(3, 6, 2, 23), array(10, 6, 2, 23)), 57 => array(240, array(3, 0, 5, 3), array(10, 0, 5, 2)), 58 => array(1140, array(10, 0, 5, 2), array(4, 0, 1, 2)), 59 => array(1200, array(10, 0, 5, 2), array(4, 0, 1, 2)), ); /** * Create class instance from DateTime object * * @param DateTime $date A date object. * * @return kolab_api_filter_mapistore_timezone */ public static function from_date($date) { $result = new self; list($standard, $daylight) = self::get_transitions($date); if (!empty($standard)) { $result->Bias = $standard['offset'] / 60 * -1; if (!empty($daylight)) { self::set_transition($result, $standard, 'Standard'); self::set_transition($result, $daylight, 'Daylight'); $result->StandardHour += $daylight['offset'] / 3600; $result->DaylightHour += $standard['offset'] / 3600; $result->DaylightBias = ($daylight['offset'] - $standard['offset']) / 60 * -1; } } return $result; } /** * Attempt to guess the timezone identifier from the current object data. * * If preferred timezone is specified and it matches current data * it will be returned, otherwise it returns any matching timezone or UTC. * * @param string $preferred The preferred timezone. * * @return string The timezone identifier */ public function get_timezone($preferred = null) { // Tries to guess the correct start date depending on object // property, falls back to current date. if (!empty($this->StandardYear)) { $datetime = new DateTime($this->StandardYear . '-01-01'); } else { $datetime = new DateTime('1 year ago'); } // If preferred TZ is not specified, try one configured on the server // it's likely better than just selecting the first matching TZ. if (!$preferred) { $preferred = date_default_timezone_get(); } // First check the preferred timezone if ($preferred && $this->check_timezone($preferred, $datetime)) { return $preferred; } // @TODO: for better results we should first check // more common areas of the globe and maybe skip some e.g. Arctica. foreach (DateTimeZone::listIdentifiers() as $timezone) { if ($this->check_timezone($timezone, $datetime)) { return $timezone; } } return 'UTC'; } /** * Get PidLidTimeZone value from PHP DateTime * * @param DateTime $date A date object. * * @return int Timezone identifier, NULL if not found */ public static function get_int_from_date($date) { $tz = self::from_date($date); foreach (self::$tz_data as $int => $data) { if ($tz->Bias == self::from_mapi_offset($data[0]) && $tz->StandardMonth == $data[1][0] && $tz->StandardDayOfWeek == $data[1][1] && $tz->StandardDay == $data[1][2] && $tz->StandardHour == $data[1][3] && $tz->DaylightMonth == $data[2][0] && $tz->DaylightDayOfWeek == $data[2][1] && $tz->DaylightDay == $data[2][2] && $tz->DaylightHour == $data[2][3] ) { return $int; } } } /** * Attempt to guess the timezone identifier from PidLidTimeZone value. * * If preferred timezone is specified and it matches current data * it will be returned, otherwise it returns any matching timezone or UTC. * * @param int $tz_id MAPI PidLidTimeZone property value * @param string $preferred The preferred timezone * * @return string The timezone identifier */ public static function get_timezone_from_int($tz_id, $preferred = null) { $data = self::$tz_data[(int) $tz_id]; if (empty($data)) { return 'UTC'; } $tz = new self; // fill self object with data from timezone definition if (!empty($data[1])) { foreach (array('Standard', 'Daylight') as $idx => $type) { foreach (array('Month', 'DayOfWeek', 'Day', 'Hour') as $i => $item) { $tz->{$type . $item} = $data[$idx + 1][$i]; } } } $tz->Bias = self::from_mapi_offset($data[0]); $tz->DaylightBias = -60; // @FIXME return $tz->get_timezone($preferred); } /** * Converts PidLidTimeZone offset for comparison with Bias value */ protected static function from_mapi_offset($offset) { // MAPI's PidLidTimeZone offset is in minutes from UTC+12 // and it's always positive and <= 24 * 60. if ($offset >= 12 * 60) { $offset -= 12 * 60; } else { $offset = (12 * 60 - $offset) * -1; } return $offset; } /** * Get the transition data for moving from DST to STD time. * * @param DateTime $date The date to start from and specified timezone. * * @return array An array containing the the STD and DST transitions */ protected static function get_transitions($date) { $standard = array(); $daylight = array(); $timezone = $date->getTimezone(); $date_year = $date->format('Y'); // get timezone transitions in specified year $transitions = $timezone->getTransitions( mktime(0, 0, 0, 12, 1, $date_year - 1), mktime(24, 0, 0, 12, 31, $date_year) ); + if ($transitions === false) { + return array(); + } + foreach ($transitions as $i => $transition) { try { $d = new DateTime($transition['time']); } catch (Exception $e) { continue; } $year = $d->format('Y'); if ($year == $date_year && isset($transitions[$i + 1])) { $next = new DateTime($transitions[$i + 1]['time']); if ($year == $next->format('Y')) { $daylight = $transition['isdst'] ? $transition : $transitions[$i + 1]; $standard = $transition['isdst'] ? $transitions[$i + 1] : $transition; } else { $daylight = $transition['isdst'] ? $transition: null; $standard = $transition['isdst'] ? null : $transition; } break; } else if ($i == count($transitions) - 1) { $standard = $transition; } } return array($standard, $daylight); } /** * Calculate and set the offsets for the specified transition * * @param self $object Self class instance * @param array $transition A transition hash from DateTimeZone::getTransitions() * @param string $type Transition type - daylight or standard */ protected static function set_transition($object, $transition, $type) { $date = new DateTime($transition['time']); $object->{$type . 'Year'} = (int) $date->format('Y'); $object->{$type . 'Month'} = (int) $date->format('n'); $object->{$type . 'DayOfWeek'} = (int) $date->format('w'); $object->{$type . 'Hour'} = (int) $date->format('H'); $object->{$type . 'Minute'} = (int) $date->format('i'); for ($i = 5; $i > 0; $i--) { if (self::is_nth_occurrence_in_month($transition['time'], $i)) { $object->{$type . 'Day'} = $i; break; } } } /** * Check if the given timezone matches the current object and also * evaluate the daylight saving time transitions for this timezone if necessary. * * @param string $timezone The timezone identifier * @param DateTime $datetime The date to check * * @return boolean */ protected function check_timezone($timezone, $datetime) { try { $datetime->setTimezone(new DateTimeZone($timezone)); list($standard, $daylight) = self::get_transitions($datetime); return $this->check_transition($standard, $daylight); } catch (Exception $e) { } return false; } /** * Check if the given Standard and Daylight time transitions match * current object data. * * @param array $sandard The Standard time transition data. * @param array $daylight The Daylight time transition data. * * @return boolean */ protected function check_transition($standard, $daylight) { if (empty($standard)) { return false; } $standard_offset = ($this->Bias + $this->StandardBias) * 60 * -1; if ($standard_offset == $standard['offset']) { // There's no DST to compare if (empty($daylight) || empty($daylight['isdst'])) { return empty($this->DaylightMonth); } // the milestone is sending a positive value for DaylightBias while it should send a negative value $daylight_offset = ($this->Bias + $this->DaylightBias) * 60 * -1; $daylight_offset_milestone = ($this->Bias + ($this->DaylightBias * -1)) * 60 * -1; if ($daylight_offset == $daylight['offset'] || $daylight_offset_milestone == $daylight['offset']) { $standard_dt = new DateTime($standard['time']); $daylight_dt = new DateTime($daylight['time']); if ($standard_dt->format('n') == $this->StandardMonth && $daylight_dt->format('n') == $this->DaylightMonth && $standard_dt->format('w') == $this->StandardDayOfWeek && $daylight_dt->format('w') == $this->DaylightDayOfWeek ) { return self::is_nth_occurrence_in_month($daylight['time'], $this->DaylightDay) && self::is_nth_occurrence_in_month($standard['time'], $this->StandardDay); } } } return false; } /** * Test if the weekday of the given timestamp is the nth occurrence of this * weekday within its month, where '5' indicates the last occurrence even if * there is less than five occurences. * * @param string $datetime The datetime string representation * @param integer $occurrence 1 to 5, where 5 indicates the final occurrence * during the month if that day of the week does * not occur 5 times * @return boolean */ protected static function is_nth_occurrence_in_month($datetime, $occurrence) { $original = new DateTime($datetime); $orig_month = $original->format('n'); $modified = clone $original; if ($occurrence == 5) { $modified = $modified->modify('1 week'); $mod_month = $modified->format('n'); // modified month is after the original return $mod_month > $orig_month || ($mod_month == 1 && $orig_month == 12); } $modified->modify(sprintf('-%d weeks', $occurrence - 1)); $mod_month = $modified->format('n'); if ($mod_month != $orig_month) { return false; } $modified = clone($original); $modified->modify(sprintf('-%d weeks', $occurrence)); $mod_month = $modified->format('n'); // modified month is earlier than original return $mod_month < $orig_month || ($mod_month == 12 && $orig_month == 1); } } diff --git a/lib/kolab_api_backend.php b/lib/kolab_api_backend.php index 8bccd57..57eb4d7 100644 --- a/lib/kolab_api_backend.php +++ b/lib/kolab_api_backend.php @@ -1,1332 +1,1341 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $api; public $storage; public $username; public $password; public $user; public $delimiter; protected $icache = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $this->api = kolab_api::get_instance(); $this->storage = $this->api->get_storage(); // @TODO: reset cache? if we do this for every request the cache would be useless // There's no session here //$this->storage->clear_cache('mailboxes.', true); // set additional header used by libkolab $this->storage->set_options(array( // @TODO: there can be Roundcube plugins defining additional headers, // we maybe would need to add them here 'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION', 'skip_deleted' => true, 'threading' => false, )); // Disable paging $this->storage->set_pagesize(999999); $this->delimiter = $this->storage->get_hierarchy_delimiter(); if ($_SESSION['user_id']) { $this->user = new rcube_user($_SESSION['user_id']); $this->api->config->set_user_prefs((array)$this->user->get_prefs()); } } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { $host = $this->select_host($username); // use shared cache for kolab_auth plugin result (username canonification) $cache = $this->api->get_cache_shared('kolab_api_auth'); $cache_key = sha1($username . '::' . $host); if (!$cache || !($auth = $cache->get($cache_key))) { $auth = $this->api->plugins->exec_hook('authenticate', array( 'host' => $host, 'user' => $username, 'pass' => $password, )); if ($cache && !$auth['abort']) { $cache->set($cache_key, array( 'user' => $auth['user'], 'host' => $auth['host'], )); } // LDAP server failure... send 503 error if ($auth['kolab_ldap_error']) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } } else { $auth['pass'] = $password; } // authenticate user against the IMAP server $user_id = $auth['abort'] ? 0 : $this->login($auth['user'], $auth['pass'], $auth['host'], $error); if ($user_id) { $this->username = $auth['user']; $this->password = $auth['pass']; $this->delimiter = $this->storage->get_hierarchy_delimiter(); return true; } // IMAP server failure... send 503 error if ($error == rcube_imap_generic::ERROR_BAD) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } return false; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { $type_keys = array( kolab_storage::CTYPE_KEY_PRIVATE, kolab_storage::CTYPE_KEY, ); // get folder unique identifiers and types $uid_data = $this->folder_uids(); $type_data = $this->storage->get_metadata('*', $type_keys); $folders = array(); if (!is_array($type_data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ($uid_data as $folder => $uid) { $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = null; if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } // parent folder does not exist add it to the list else { for ($i=0; $idelimiter, $parent_arr); if ($folders[$parent]) { $parent_id = $folders[$parent]['uid']; } else { $fid = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $folders[$parent] = array( 'name' => array_pop($parent_arr), 'fullpath' => $parent, 'uid' => $fid, 'parent' => $parent_id, ); $parent_id = $fid; } } } } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); // folder type reset($type_keys); foreach ($type_keys as $key) { - $data['type'] = $type_data[$folder][$key]; - break; + if ($type = $type_data[$folder][$key]) { + $data['type'] = $type; + break; + } } if (empty($data['type'])) { $data['type'] = 'mail'; } $folders[$path] = $data; } // sort folders uksort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folder_uid2name($uid); $type = kolab_storage::folder_type($folder); if ($type === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type rcube_message_header or kolab_format) * @throws kolab_api_exception */ public function objects_list($uid) { $type = $this->folder_type($uid); // use IMAP to fetch mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); - $result = $this->storage->list_messages($folder, 1); + $result = $this->storage->list_messages($folder, 1, '', 'ASC'); foreach ($result as $idx => $mail) { $result[$idx] = new kolab_api_mail($mail); } } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->get_objects(); if ($result === null) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $type = $this->folder_type($uid); // use IMAP to count mail messages if ($type === 'mail') { $folder = $this->folder_uid2name($uid); // @TODO: error checking requires changes in rcube_imap $result = $this->storage->count($folder, 'ALL'); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); $result = $folder->count(); } return $result; } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $type = $this->folder_type($uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); } // delete all if ($set === "*") { if ($is_mail) { $result = $this->storage->clear_folder($folder); } else { $result = $folder->delete_all(); } } else { if ($is_mail) { $result = $this->storage->delete_message($set, $folder); } else { foreach ($set as $uid) { $result = $folder->delete($uid); if ($result === false) { break; } } } } + // @TODO: should we throw exception when deleting non-existing object? + if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Move objects into another folder * * @param string $uid Folder unique identifier * @param string $target_uid Target folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_move($uid, $target_uid, $set) { $type = $this->folder_type($uid); $target_type = $this->folder_type($target_uid); if ($type === 'mail') { $is_mail = true; $folder = $this->folder_uid2name($uid); - $target = $this->folder_uid2name($uid); + $target = $this->folder_uid2name($target_uid); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($uid, $type); - $target = $this->folder_get_by_uid($uid, $type); + $target = $this->folder_get_by_uid($target_uid, $target_type); } if ($is_mail) { if ($set === "*") { $set = '1:*'; } $result = $this->storage->move_messages($set, $target, $folder); } else { if ($set === "*") { $set = $folder->get_uids(); } foreach ($set as $uid) { $result = $folder->move($uid, $target); if ($result === false) { break; } } } if ($result === false) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return kolab_api_mail|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $type = $this->folder_type($folder_uid); if ($type === 'mail') { $folder = $this->folder_uid2name($folder_uid); $object = new rcube_message($uid, $folder); if (!$object || empty($object->headers)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object = new kolab_api_mail($object); } // otherwise use kolab_storage else { $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object = $folder->get_object($uid); if (!$object) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $old_categories = $object['categories']; } // @TODO: Use relations also for events if ($type != 'configuration' && $type != 'event') { // get object categories (tag-relations) $categories = $this->get_tags($object, $old_categories); if ($type === 'mail') { $object->categories = $categories; } else { $object['categories'] = $categories; } } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (an array or kolab_api_mail) * @param string $type Object type * * @return string Object UID * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype !== 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); // @TODO: categories return $data->save($folder); } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // get object categories (tag-relations) $categories = (array) $data['categories']; $data['categories'] = array(); } $folder = $this->folder_get_by_uid($folder_uid, $type); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if (!$folder->save($data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // @TODO: Use relations also for events if (!empty($categories)) { // create/assign categories (tag-relations) $this->set_tags($data['uid'], $categories); } return $data['uid']; } } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param mixed $data Object data (array or kolab_api_mail) * @param string $type Object type * * @return string Object UID (it can change) * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $ftype = $this->folder_type($folder_uid); if ($type === 'mail') { if ($ftype != 'mail') { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $folder = $this->folder_uid2name($folder_uid); return $data->save($folder); } // otherwise use kolab_storage else { // @TODO: Use relations also for events if (!preg_match('/^(event|configuration)/', $type)) { // 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) { + if ($object instanceof kolab_api_mail) { 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, + $this->storage->set_folder($object['_mailbox']); + return $this->storage->get_message_part($object['_msguid'], $part_id, null, $mode === -1, is_resource($mode) ? $mode : null, true, 0, false); } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_delete($object, $id) { // object is a mail message if (is_object($object)) { - $mail = new kolab_api_mail($message); - return $mail->attachment_delete($id); + return $object->attachment_delete($id); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } - if (!$folder->save($data)) { + if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Create an attachment and add to a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attach Attachment data * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_create($object, $attach) { // object is a mail message if (is_object($object)) { - $mail = new kolab_api_mail($message); - return $mail->attachment_create($attach); + return $object->attachment_add($attach); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $object['_attachments'][] = array( - 'name' => $attachment->filename, - 'mimetype' => $attachment->mimetype, - 'path' => $attachment->path, - 'size' => $attachment->size, - 'content' => $attachment->data, + 'name' => $attach->filename, + 'mimetype' => $attach->mimetype, + 'path' => $attach->path, + 'size' => $attach->size, + 'content' => $attach->data, ); if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Update an attachment in a message/object * * @param mixed $object Object data (from self::object_get()) * @param rcube_message_part $attach Attachment data * * @return string Message/Object UID * @throws kolab_api_exception */ public function attachment_update($object, $attach) { // object is a mail message if (is_object($object)) { - $mail = new kolab_api_mail($message); - return $mail->attachment_update($attach); + return $object->attachment_update($attach); } // otherwise use kolab_storage else { $folder = kolab_storage::get_folder($object['_mailbox']); if (!$folder || !$folder->valid) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $found = false; // unset the attachment foreach ((array) $object['_attachments'] as $idx => $att) { if ($att['id'] == $attach->mime_id) { $object['_attachments'][$idx] = false; $found = true; } } if (!$found) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $object['_attachments'][] = array( - 'name' => $attachment->filename, - 'mimetype' => $attachment->mimetype, - 'path' => $attachment->path, - 'size' => $attachment->size, - 'content' => $attachment->data, + 'name' => $attach->filename, + 'mimetype' => $attach->mimetype, + 'path' => $attach->path, + 'size' => $attach->size, + 'content' => $attach->data, ); if (!$folder->save($object)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $object['uid']; } } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $name = rcube_charset::convert($name, RCUBE_CHARSET, 'UTF7-IMAP'); if ($parent) { $parent = $this->folder_uid2name($parent); $name = $parent . $this->delimiter . $name; } if ($this->storage->folder_exists($name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $created = kolab_storage::folder_create($name, $type, false, false); if ($created) { $created = $this->folder_name2uid($name); } return $created; } /** * Subscribes a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folder_uid2name($uid); if (isset($updates['type'])) { $result = kolab_storage::set_folder_type($folder, $updates['type']); if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } if (isset($updates['subscribed'])) { if ($updates['subscribed']) { $result = $this->storage->subscribe($folder); } else { $result = $this->storage->unsubscribe($folder); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } // @TODO: active state } /** * Renames a folder * * @param string $old_name Folder name (UTF-8) * @param string $new_name New folder name (UTF-8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_name = rcube_charset::convert($old_name, RCUBE_CHARSET, 'UTF7-IMAP'); $new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP'); if (!strlen($old_name) || !strlen($new_name) || $old_name === $new_name) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } if ($this->storage->folder_exists($new_name)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } + $type = kolab_storage::folder_type($old_name); + + if ($type === null) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + // don't use kolab_storage for moving mail folders if (preg_match('/^mail/', $type)) { $result = $this->storage->rename_folder($old_name, $new_name); } else { $result = kolab_storage::folder_rename($old_name, $new_name); } if (!$result) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folder_uid2name($uid); $type = $this->folder_type($uid); // don't use kolab_storage for mail folders if ($type === 'mail') { $status = $this->storage->delete_folder($folder); } else { $status = kolab_storage::folder_delete($folder); } if (!$status) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folder_uid2name($uid); // get IMAP folder info $info = $this->storage->folder_info($folder); // get IMAP folder data $data = $this->storage->folder_data($folder); $info['exists'] = $data['EXISTS']; $info['unseen'] = $data['UNSEEN']; $info['modseq'] = $data['HIGHESTMODSEQ']; // add some more parameters (used in folders list response) $path = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); $path = explode($this->delimiter, $path); - $info['name'] = array_pop($path); + $info['name'] = $path[count($path)-1]; $info['fullpath'] = implode($this->delimiter, $path); $info['uid'] = $uid; $info['type'] = kolab_storage::folder_type($folder, true) ?: 'mail'; - if ($info['fullpath'] !== '') { - $parent = $this->folder_name2uid(rcube_charset::convert($info['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP')); + if (count($path) > 1) { + array_pop($path); + $parent = implode($this->delimiter, $path); + $parent = $this->folder_name2uid(rcube_charset::convert($parent, RCUBE_CHARSET, 'UTF7-IMAP')); $info['parent'] = $parent; } // convert some info to be more compact if (!empty($info['rights'])) { $info['rights'] = implode('', $info['rights']); } // @TODO: subscription status, active state // some info is not very interesting here ;) unset($info['attributes']); return $info; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { $folder = $this->folder_uid2name($uid); return strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP'); } /** * Returns IMAP folder name * * @param string $uid Folder identifier * * @return string Folder name (UTF7-IMAP) */ protected function folder_uid2name($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // we store last folder in-memory if (isset($this->icache["folder:$uid"])) { return $this->icache["folder:$uid"]; } $uids = $this->folder_uids(); foreach ($uids as $folder => $_uid) { if ($uid === $_uid) { return $this->icache["folder:$uid"] = $folder; } } - // slowest method, but we need to try it, the full folders list // might contain non-existing folder (not in folder_uids() result) foreach ($this->folders_list() as $folder) { if ($folder['uid'] === $uid) { return rcube_charset::convert($folder['fullpath'], RCUBE_CHARSET, 'UTF7-IMAP'); } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Helper method to get folder UID * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder's UID */ protected function folder_name2uid($folder) { $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array(kolab_storage::UID_KEY_SHARED); // get folder identifiers $metadata = $this->storage->get_metadata($folder, $uid_keys); if (!is_array($metadata) && $this->storage->get_error_code() != rcube_imap_generic::ERROR_NO) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ if (!empty($metadata[$folder])) { foreach ($uid_keys as $key) { if ($uid = $metadata[$folder][$key]) { return $uid; } } } return md5($folder); /* // @TODO: // make sure folder exists // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($folder . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if ($this->storage->set_metadata($folder, array(kolab_storage::UID_KEY_SHARED => $uid))) { return $uid; } // create hash from folder name if we can't write the UID metadata return md5($folder . $this->get_owner()); */ } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1); $path2 = explode($this->delimiter, $str2); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Return UIDs of all folders * * @return array Folder name to UID map */ protected function folder_uids() { $uid_keys = array(kolab_storage::UID_KEY_CYRUS); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } /* // above we assume that cyrus built-in unique identifiers are available // however, if they aren't we'll try kolab folder UIDs if (empty($metadata)) { $uid_keys = array(kolab_storage::UID_KEY_SHARED); // get folder identifiers $metadata = $this->storage->get_metadata('*', $uid_keys); if (!is_array($metadata)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } */ $lambda = function(&$item, $key, $keys) { reset($keys); foreach ($keys as $key) { $item = $item[$key]; return; } }; array_walk($metadata, $lambda, $uid_keys); return $metadata; } /** * Get folder by UID (use only for non-mail folders) * * @param string $uid Folder UID * @param string $type Folder type * * @return kolab_storage_folder Folder object * @throws kolab_api_exception */ protected function folder_get_by_uid($uid, $type = null) { $folder = $this->folder_uid2name($uid); $folder = kolab_storage::get_folder($folder, $type); if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // Check the given storage folder instance for validity and throw // the right exceptions according to the error state. if (!$folder->valid || ($error = $folder->get_error())) { if ($error === kolab_storage::ERROR_IMAP_CONN) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_CACHE_DB) { throw new kolab_api_exception(kolab_api_exception::UNAVAILABLE); } else if ($error === kolab_storage::ERROR_NO_PERMISSION) { throw new kolab_api_exception(kolab_api_exception::FORBIDDEN); } else if ($error === kolab_storage::ERROR_INVALID_FOLDER) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } return $folder; } /** * Storage host selection */ protected function select_host($username) { // Get IMAP host $host = $this->api->config->get('default_host', 'localhost'); if (is_array($host)) { list($user, $domain) = explode('@', $username); // try to select host by mail domain if (!empty($domain)) { foreach ($host as $storage_host => $mail_domains) { if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) { $host = $storage_host; break; } else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) { $host = is_numeric($storage_host) ? $mail_domains : $storage_host; break; } } } // take the first entry if $host is not found if (is_array($host)) { list($key, $val) = each($default_host); $host = is_numeric($key) ? $val : $key; } } return rcube_utils::parse_host($host); } /** * Authenticates a user in IMAP and returns Roundcube user ID. */ protected function login($username, $password, $host, &$error = null) { if (empty($username)) { return null; } $login_lc = $this->api->config->get('login_lc'); $default_port = $this->api->config->get('default_port', 143); // parse $host $a_host = parse_url($host); if ($a_host['host']) { $host = $a_host['host']; $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; if (!empty($a_host['port'])) { $port = $a_host['port']; } else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { $port = 993; } } if (!$port) { $port = $default_port; } // Convert username to lowercase. If storage backend // is case-insensitive we need to store always the same username if ($login_lc) { if ($login_lc == 2 || $login_lc === true) { $username = mb_strtolower($username); } else if (strpos($username, '@')) { // lowercase domain name list($local, $domain) = explode('@', $username); $username = $local . '@' . mb_strtolower($domain); } } // Here we need IDNA ASCII // Only rcube_contacts class is using domain names in Unicode $host = rcube_utils::idn_to_ascii($host); $username = rcube_utils::idn_to_ascii($username); // user already registered? if ($user = rcube_user::query($username, $host)) { $username = $user->data['username']; } // authenticate user in IMAP if (!$this->storage->connect($host, $username, $password, $port, $ssl)) { $error = $this->storage->get_error_code(); return null; } // No user in database, but IMAP auth works if (!is_object($user)) { if ($this->api->config->get('auto_create_user')) { // create a new user record $user = rcube_user::create($username, $host); if (!$user) { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to create a user record", ), true, false); return null; } } else { rcube::raise_error(array( 'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Access denied for new user $username. 'auto_create_user' is disabled", ), true, false); return null; } } // overwrite config with user preferences $this->user = $user; $this->api->config->set_user_prefs((array)$this->user->get_prefs()); $_SESSION['user_id'] = $this->user->ID; $_SESSION['username'] = $this->user->data['username']; $_SESSION['storage_host'] = $host; $_SESSION['storage_port'] = $port; $_SESSION['storage_ssl'] = $ssl; $_SESSION['password'] = $this->api->encrypt($password); $_SESSION['login_time'] = time(); setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); return $user->ID; } /** * Returns list of tag-relation names assigned to kolab object */ 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; + $ident = $object->{'message-id'}; + $folder = $object->folder; + $uid = $object->uid; } if (empty($ident)) { return array(); } $config = kolab_storage_config::get_instance(); $tags = $config->get_tags($ident); $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_mail.php b/lib/kolab_api_mail.php index 4a8a1f6..bb41454 100644 --- a/lib/kolab_api_mail.php +++ b/lib/kolab_api_mail.php @@ -1,1212 +1,1241 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_mail { /** * List of supported header fields * * @var array */ public static $header_fields = array( 'uid', 'subject', 'from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'message-id', 'references', 'date', 'internaldate', 'content-type', 'priority', 'size', 'flags', 'categories', 'has-attach', ); /** * Original message * * @var rcube_message */ protected $message; /** * Modified properties * * @var array */ protected $data = array(); /** * Validity status * * @var bool */ protected $valid = true; /** * Headers-only mode flag * * @var bool */ protected $is_header = false; /** * Line separator * * @var string */ protected $endln = "\r\n"; protected $body_text; protected $body_html; protected $boundary; protected $attach_data = array(); protected $recipients = array(); protected $from_address; /** * Object constructor * * @param rcube_message|rcube_message_header Original message */ public function __construct($message = null) { $this->message = $message; $this->is_header = $this->message instanceof rcube_message_header; } /** * Properties setter * * @param string $name Property name * @param mixed $value Property value */ public function __set($name, $value) { switch ($name) { case 'flags': $value = (array) $value; break; case 'priority': $value = (int) $value; /* values: 1, 2, 4, 5 */ break; case 'date': case 'subject': case 'from': case 'sender': case 'to': case 'cc': case 'bcc': case 'reply-to': case 'in-reply-to': case 'references': case 'categories': case 'message-id': case 'text': case 'html': // make sure the value is utf-8 if ($value !== null && $value !== '' && (!is_array($value) || !empty($value))) { // make sure we have utf-8 here $value = rcube_charset::clean($value); } break; case 'uid': case 'internaldate': case 'content-type': case 'size': // ignore return; default: // unsupported property, log error? return; } if (!$changed && in_array($name, self::$header_fields)) { $changed = $this->{$name} !== $value; } else { $changed = true; } if ($changed) { $this->data[$name] = $value; } } /** * Properties getter * * @param string $name Property name * * @param mixed Property value */ public function __get($name) { if (array_key_exists($name, $this->data)) { return $this->data[$name]; } if (empty($this->message)) { return; } $headers = $this->is_header ? $this->message : $this->message->headers; $value = null; switch ($name) { case 'uid': return (string) $headers->uid; - break; + + case 'folder': + return $headers->folder; case 'priority': case 'size': if (isset($headers->{$name})) { $value = (int) $headers->{$name}; } break; case 'content-type': $value = $headers->ctype; break; case 'date': case 'internaldate': $value = $headers->{$name}; break; case 'subject': $value = trim(rcube_mime::decode_header($headers->subject, $headers->charset)); break; case 'flags': $value = array_change_key_case((array) $headers->flags); $value = array_filter($value); $value = array_keys($value); break; case 'from': case 'sender': case 'to': case 'cc': case 'bcc': case 'reply-to': $addresses = $headers->{$name == 'reply-to' ? 'replyto' : $name}; $addresses = rcube_mime::decode_address_list($addresses, null, true, $headers->charset); $value = array(); foreach ((array) $addresses as $addr) { $idx = count($value); if ($addr['mailto']) { $value[$idx]['address'] = $addr['mailto']; } if ($addr['name']) { $value[$idx]['name'] = $addr['name']; } } if ($name == 'from' && !empty($value)) { $value = $value[0]; } break; case 'categories': $value = (array) $headers->categories; break; case 'references': case 'in-reply-to': case 'message-id': $value = $headers->get($name); break; case 'text': case 'html': $value = $this->body($name == 'html'); break; case 'has-attach': if (isset($this->message->attachments)) { $value = !empty($this->message->attachments); } else if (isset($headers->attachments)) { $value = !empty($headers->attachments); } else { // We don't have the whole structure, // we can only make a guess based on message mimetype $regex = '/^(application\/|multipart\/(m|signed|report))/i'; $value = (bool) preg_match($regex, $headers->ctype); } break; + + case 'attachments': + if ($message = $this->get_message()) { + return $message->attachments; + } + return array(); } // add the value to the result if ($value !== null && $value !== '' && (!is_array($value) || !empty($value))) { // make sure we have utf-8 here $value = rcube_charset::clean($value); } return $value; } /** * Return message data as an array * * @param array $filetr Optional properties filter * * @return array Message/headers data */ public function data($filter = array()) { $result = array(); $fields = self::$header_fields; if (!empty($filter)) { $fields = array_intersect($fields, $filter); } foreach ($fields as $field) { $value = $this->{$field}; // add the value to the result if ($value !== null && $value !== '') { $result[$field] = $value; } } // complete rcube_message object, we can set more props, e.g. body content if (!$this->is_header) { foreach (array('text', 'html') as $prop) { if ($value = $this->{$prop}) { $result[$prop] = $value; } } } return $result; } /** * Check if the original message has been modified * * @return bool True if the message has been modified * since the object creation */ public function changed() { return !empty($this->data); } /** * Check object validity * * @return bool True if the object is valid */ public function valid() { if (empty($this->message)) { // @TODO: check required properties of a new message? } return $this->valid; } + /** + * Get content of a specific part of this message + * + * @param string $mime_id Part ID + * @param boolean $formatted Enables formatting of text/* parts bodies + * @param int $max_bytes Only return/read this number of bytes + * @param mixed $mode NULL to return a string, -1 to print body + * or file pointer to save the body into + * + * @return string|bool Part content or operation status + */ + public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null) + { + if ($message = $this->get_message()) { + return $message->get_part_body($mime_id, $formatted, $max_bytes, $mode); + } + + return false; + } + /** * Save the message in specified folder * * @param string $folder IMAP folder name * * @return string New message UID * @throws kolab_api_exception */ public function save($folder = null) { if (empty($this->data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Nothing to save. Did you use kolab_api_mail::changed()?' )); } $message = $this->get_message(); $api = kolab_api::get_instance(); if (empty($message) && !strlen($folder)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Folder not specified' )); } // Create message content $stream = $this->create_message_stream(); // Save the message $uid = $this->save_message($stream, $folder ?: $message->folder); // IMAP flags change requested if (array_key_exists('flags', $this->data)) { $old_flags = $this->flags; // set new flags foreach ((array) $this->data['flags'] as $flag) { if (($key = array_search($flag, $old_flags)) !== false) { unset($old_flags[$key]); } else { $flag = strtoupper($flag); $api->backend->storage->set_flag($uid, $flag, $message->folder); } } // unset remaining old flags foreach ($old_flags as $flag) { $flag = 'UN' . strtoupper($flag); $api->backend->storage->set_flag($uid, $flag, $message->folder); } } return $uid; } /** * Send the message * * @return bool True on success * @throws kolab_api_exception */ public function send() { // Create message content $stream = $this->create_message_stream(true); if (empty($this->from_address)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "No sender found"); } if (empty($this->recipients)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST, null, "No recipients found"); } $this->send_message_stream($stream); return true; } /** * Add an attachment to the message * * @param rcube_message_part $attachment Attachment data * * @return string New message UID on success * @throws kolab_api_exception */ public function attachment_add($attachment) { if (!($message = $this->get_message())) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->attach_data['create'][] = $attachment; // Create message content $stream = $this->create_message_stream(); // Save the message return $this->save_message($stream, $message->folder); } /** * Remove attachment from the message * * @param string $id Attachment id * * @return string New message UID on success * @throws kolab_api_exception */ public function attachment_delete($id) { if (!($message = $this->get_message())) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } foreach ((array) $message->attachments as $attach) { if ($attach->mime_id == $id) { $attachment = $attach; } } if (empty($attachment)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $this->attach_data['delete'][] = $attachment; // Create message content $stream = $this->create_message_stream(); // Save the message return $this->save_message($stream, $message->folder); } /** * Update specified attachment in the message * * @param rcube_message_part $attachment Attachment data * * @return string New message UID on success * @throws kolab_api_exception */ public function attachment_update($attachment) { if (!($message = $this->get_message())) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->attach_data['update'][] = $attachment; // Create message content $stream = $this->create_message_stream(); // Save the message return $this->save_message($stream, $message->folder); } /** * Create message stream */ protected function create_message_stream($send_mode = false) { $api = kolab_api::get_instance(); $message = $this->get_message(); $specials = array('flags', 'categories'); $diff = array_diff($this->data, $specials); $headers = array(); $endln = $this->endln; $body_mod = $message && (!empty($diff) || !empty($this->attach_data)); // header change requested, get old headers if ($body_mod) { $api->backend->storage->set_folder($message->folder); $headers = $api->backend->storage->get_raw_headers($message->uid); $headers = self::parse_headers($headers); } foreach ($this->data as $name => $value) { $normalized = self::normalize_header_name($name); unset($headers[$normalized]); switch ($name) { case 'priority': unset($headers['X-Priority']); $priority = intval($value); $priorities = array(1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest'); if ($str_priority = $priorities[$priority]) { $headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority)); } break; case 'date': // @TODO: date-time format $headers['Date'] = $value; break; case 'from': if (!empty($value)) { if (empty($value['address']) || !strpos($value['address'], '@')) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $value = format_email_recipient($value['address'], $value['name']); } $headers[$normalized] = $value; break; case 'to': case 'cc': case 'bcc': case 'reply-to': $recipients = array(); foreach ((array) $value as $adr) { if (!is_array($adr) || empty($adr['address']) || !strpos($adr['address'], '@')) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $recipients[] = format_email_recipient($adr['address'], $adr['name']); } $headers[$normalized] = implode(',', $recipients); break; case 'references': case 'in-reply-to': case 'message-id': case 'subject': if ($value) { $headers[$normalized] = $value; } break; } } // Prepare message body $body_mod = $this->prepare_body($headers); // We're going to send this message, we need more data/checks if ($send_mode) { if (empty($headers['From'])) { // get From: address from default identity of the user? if ($identity = $api->backend->user->get_identity()) { $headers['From'] = format_email_recipient( format_email($identity['email']), $identity['name']); } } $addresses = rcube_mime::decode_address_list($headers['From'], null, false, null, true); $this->from_address = array_shift($addresses); // extract mail recipients foreach (array('To', 'Cc', 'Bcc') as $idx) { if ($headers[$idx]) { $addresses = rcube_mime::decode_address_list($headers[$idx], null, false, null, true); $this->recipients = array_merge($this->recipients, $addresses); } } $this->recipients = array_unique($this->recipients); unset($headers['Bcc']); $headers['Date'] = $api->user_date(); /* if ($mdn_enabled) { $headers['Return-Receipt-To'] = $this->from_address; $headers['Disposition-Notification-To'] = $this->from_address; } */ } // Write message headers to the stream if (!empty($headers) || empty($message) || $body_mod) { // Place Received: headers at the beginning of the message // Spam detectors often flag messages with it after the Subject: as spam if (!empty($headers['Received'])) { $received = $headers['Received']; unset($headers['Received']); $headers = array('Received' => $received) + $headers; } if (empty($headers['MIME-Version'])) { $headers['MIME-Version'] = '1.0'; } // always add User-Agent header if (empty($headers['User-Agent'])) { $headers['User-Agent'] .= kolab_api::APP_NAME . ' ' . kolab_api::VERSION; if ($agent = $api->config->get('useragent')) { $headers['User-Agent'] .= '/' . $agent; } } if (empty($headers['Message-ID'])) { $headers['Message-ID'] = $this->gen_message_id(); } // create new message header if ($stream = fopen('php://temp/maxmemory:10240000', 'r+')) { foreach ($headers as $header_name => $header_value) { if (strlen($header_value)) { $header_value = $this->encode_header($header_name, $header_value); fwrite($stream, $header_name . ": " . $header_value . $endln); } } fwrite($stream, $endln); } else { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Failed to open file stream for mail message' )); } } // Save/update the message body into the stream $this->write_body($stream, $headers); return $stream; } /** * Send the message stream using configured method */ protected function send_message_stream($stream) { $api = kolab_api::get_instance(); // send thru SMTP server using custom SMTP library if ($api->config->get('smtp_server')) { // send message if (!is_object($api->smtp)) { $api->smtp_init(true); } rewind($stream); $headers = null; $sent = $api->smtp->send_mail( $this->from_address, $this->recipients, $headers, $stream, $smtp_opts); // log error if (!$sent) { $smtp_response = $api->smtp->get_response(); // $smtp_error = $api->smtp->get_error(); throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'code' => 800, 'type' => 'smtp', 'message' => "SMTP error: " . join("\n", $smtp_response), )); } } else { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'line' => __LINE__, 'file' => __FILE__, 'code' => 800, 'type' => 'smtp', 'message' => "SMTP server not configured. Really need smtp_server option to be set.", )); } // $api->plugins->exec_hook('message_sent', array('headers' => array(), 'body' => $stream)); if ($api->config->get('smtp_log')) { rcube::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s", $api->get_user_name(), $_SERVER['REMOTE_ADDR'], implode(',', $this->recipients), !empty($smtp_response) ? join('; ', $smtp_response) : '')); } return true; } /** * Prepare message body and content headers */ protected function prepare_body(&$headers) { $message = $this->get_message(); $body_mod = array_key_exists('text', $this->data) || array_key_exists('html', $this->data) || !empty($this->attach_data); if (!$body_mod) { return false; } if (!empty($this->attach_data['create']) || !empty($this->attach_data['update'])) { $ctype = 'multipart/mixed'; } else { $ctype = $this->data['html'] ? 'multipart/alternative' : 'text/plain'; } // Get/set Content-Type header of the modified message if ($old_ctype = $headers['Content-Type']) { if (preg_match('/boundary="?([a-z0-9-\'\(\)+_\,\.\/:=\? ]+)"?/i', $old_ctype, $matches)) { $boundary = $matches[1]; } if ($pos = strpos($old_ctype, ';')) { $old_ctype = substr($old_ctype, 0, $pos); } if ($old_ctype == 'multipart/mixed') { // replace first part (if it is text/plain or multipart/alternative) $ctype = $old_ctype; } } $headers['Content-Type'] = $ctype; if ($ctype == 'text/plain') { $headers['Content-Type'] .= '; charset=' . RCUBE_CHARSET; } else if (!$boundary) { $boundary = '_' . md5(rand() . microtime()); } if ($boundary) { $headers['Content-Type'] .= ';' . $this->endln . " boundary=\"$boundary\""; } // create message body if ($html = $this->data['html']) { $text = $this->data['text']; if ($text === null) { $h2t = new rcube_html2text($html); $text = $h2t->get_text(); } $this->body_text = quoted_printable_encode($text); $this->body_html = quoted_printable_encode($html); } else if ($text = $this->data['text']) { $headers['Content-Transfer-Encoding'] = 'quoted-printable'; $this->body_text = quoted_printable_encode($text); } $this->boundary = $boundary; // make sure all line endings are CRLF $this->body_text = preg_replace('/\r?\n/', $this->endln, $this->body_text); $this->body_html = preg_replace('/\r?\n/', $this->endln, $this->body_html); return true; } /** * Write message body to the stream */ protected function write_body($stream, $headers) { $api = kolab_api::get_instance(); $endln = $this->endln; $message = $this->get_message(); $body_mod = array_key_exists('text', $this->data) || array_key_exists('html', $this->data); $modified = $body_mod || !empty($this->attach_data); // @TODO: related parts for inline images // nothing changed in the modified message body... if (!$modified && !empty($message)) { // just copy the content to the output stream $api->backend->storage->get_raw_body($message->uid, $stream, 'TEXT'); } // new message creation, or the message does not have any attachments else if (empty($message) || ($message->headers->ctype != 'multipart/mixed' && empty($this->attach_data))) { // Here we do not have attachments yet, so we only have two // simple options: multipart/alternative or text/plain $this->write_body_content($stream, $this->boundary); } // body changed or attachments added, non-multipart message else if ($message->headers->ctype != 'multipart/mixed') { fwrite($stream, '--' . $this->boundary . $endln); // write body if (array_key_exists('text', $this->data) || array_key_exists('html', $this->data)) { // new body $this->write_body_content($stream, $this->boundary); } else { // existing body, just copy old content to the stream $api->backend->storage->get_raw_body($message->uid, $stream, 'TEXT'); } fwrite($stream, $endln); // write attachments, here we can only have new attachments foreach ((array) $this->attach_data['create'] as $attachment) { fwrite($stream, '--' . $this->boundary . $endln); $this->write_attachment($stream, $attachment); } fwrite($stream, $endln . '--' . $this->boundary . '--' . $endln); } // body or attachments changed, multipart/mixed message else { // get old TEXT of the message $body_stream = fopen('php://temp/maxmemory:10240000', 'r+'); $api->backend->storage->get_raw_body($message->uid, $body_stream, 'TEXT'); rewind($body_stream); $num = 0; $ignore = false; $regexp = '/^--' . preg_quote($this->boundary, '/') . '(--|)\r?\n$/'; // Go and replace bodies... while (($line = fgets($body_stream, 4096)) !== false) { // boundary line if ($line[0] === '-' && $line[1] === '-' && preg_match($regexp, $line, $m)) { // this is the end of the message, add new attachment(s) here if ($m[1] == '--') { foreach ((array) $this->attach_data['create'] as $attachment) { fwrite($stream, '--' . $this->boundary . $endln); $this->write_attachment($stream, $attachment); } fwrite($stream, $line); break; } $num++; $ignore = false; // delete this part foreach ((array) $this->attach_data['delete'] as $attachment) { if ($attachment->mime_id == $num) { $ignore = true; continue 2; } } // update this part foreach ((array) $this->attach_data['update'] as $attachment) { if ($attachment->mime_id == $num) { fwrite($stream, $line); $this->write_attachment($stream, $attachment); $ignore = true; continue 2; } } // find the first part (expected to be the text or alternative if ($num == 1 && $body_mod) { $ignore = true; $boundary = '_' . md5(rand() . microtime()); fwrite($stream, $line); $this->write_body_content($stream, $boundary, true); continue; } } else if ($ignore) { continue; } fwrite($stream, $line); } fclose($body_stream); } } /** * Write configured text/plain or multipart/alternative * part content into message stream */ protected function write_body_content($stream, $boundary, $with_headers = false) { $endln = $this->endln; // multipart/alternative if (strlen($this->body_html)) { if ($with_headers) { fwrite($stream, 'Content-Type: multipart/alternative;' . $endln . " boundary=\"$boundary\"" . $endln . $endln); } fwrite($stream, '--' . $boundary . $endln . 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/plain; charset=UTF-8' . $endln . $endln); fwrite($stream, $this->body_text); fwrite($stream, $endln . '--' . $boundary . $endln . 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/html; charset=UTF-8' . $endln . $endln); fwrite($stream, $this->body_html); fwrite($stream, $endln . '--' . $boundary . '--' . $endln); } // text/plain else if (strlen($this->body_text)) { if ($with_headers) { fwrite($stream, 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/plain; charset=UTF-8' . $endln . $endln); } // make sure all line endings are CRLF $plainTextPart = preg_replace('/\r?\n/', "\r\n", $plainTextPart); fwrite($stream, $this->body_text); } } /** * Get rcube_message object of the assigned message */ protected function get_message() { if ($this->message && !($this->message instanceof rcube_message)) { $this->message = new rcube_message($this->message->uid, $this->message->folder); if (empty($this->message->headers)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } return $this->message; } /** * Parse message source with headers */ protected static function parse_headers($headers) { // Parse headers $headers = str_replace("\r\n", "\n", $headers); $headers = explode("\n", trim($headers)); $ln = 0; $lines = array(); foreach ($headers as $line) { if (ord($line[0]) <= 32) { $lines[$ln] .= (empty($lines[$ln]) ? '' : "\r\n") . $line; } else { $lines[++$ln] = trim($line); } } // Unify char-case of header names $headers = array(); foreach ($lines as $line) { list($field, $string) = explode(':', $line, 2); if ($field = self::normalize_header_name($field)) { $headers[$field] = trim($string); } } return $headers; } /** * Normalize (fix) header names */ protected static function normalize_header_name($name) { $headers_map = array( 'subject' => 'Subject', 'from' => 'From', 'to' => 'To', 'cc' => 'Cc', 'bcc' => 'Bcc', 'date' => 'Date', 'reply-to' => 'Reply-To', 'in-reply-to' => 'In-Reply-To', 'x-priority' => 'X-Priority', 'message-id' => 'Message-ID', 'references' => 'References', 'content-type' => 'Content-Type', 'content-transfer-encoding' => 'Content-Transfer-Encoding', ); $name_lc = strtolower($name); return isset($headers_map[$name_lc]) ? $headers_map[$name_lc] : $name; } /** * Save the message into IMAP folder and delete the old one */ protected function save_message($stream, $folder) { $api = kolab_api::get_instance(); $message = $this->get_message(); // save the message - $saved = $api->backend->storage->save_message($folder, array($stream)); + $data = array($stream); + $saved = $api->backend->storage->save_message($folder, $data); if (empty($saved)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( 'file' => __FILE__, 'line' => __LINE__, 'message' => 'Failed to save the message in storage' )); } // delete the old message if ($saved && $message && $message->uid) { $api->backend->storage->delete_message($message->uid, $folder); } return $saved; } /** * Return message body (in specified format, html or text) */ protected function body($html = true) { if ($message = $this->get_message()) { if ($html) { $html = $message->first_html_part($part, true); if ($html) { // charset was converted to UTF-8 in rcube_storage::get_message_part(), // change/add charset specification in HTML accordingly $meta = ''; // remove old meta tag and add the new one, making sure // that it is placed in the head $html = preg_replace('/]+charset=[a-z0-9-_]+[^>]*>/Ui', '', $html); $html = preg_replace('/(]*>)/Ui', '\\1' . $meta, $html, -1, $rcount); if (!$rcount) { $html = '' . $meta . '' . $html; } } return $html; } $plain = $message->first_text_part($part, true); if ($part === null && $message->body) { $plain = $message->body; } else if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') { $plain = rcube_mime::unfold_flowed($plain); } return $plain; } } /** * Encode header value */ protected function encode_header($name, $value) { $mime_part = new Mail_mimePart; return $mime_part->encodeHeader($name, $value, RCUBE_CHARSET, 'quoted-printable', $this->endln); } /** * Unique Message-ID generator. * * @return string Message-ID */ public function gen_message_id() { $api = kolab_api::get_instance(); $local_part = md5(uniqid('kolab'.mt_rand(), true)); $domain_part = $api->backend->user->get_username('domain'); // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924) if (!preg_match('/\.[a-z]+$/i', $domain_part)) { foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) { $host = preg_replace('/:[0-9]+$/', '', $host); if ($host && preg_match('/\.[a-z]+$/i', $host)) { $domain_part = $host; } } } return sprintf('<%s@%s>', $local_part, $domain_part); } /** * Write attachment body with headers into the output stream */ protected function write_attachment($stream, $attachment) { $api = kolab_api::get_instance(); $message = $this->get_message(); $temp_dir = $api->config->get('temp_dir'); // use Mail_mimePart functionality for simplicity $params = array( 'eol' => $this->endln, 'encoding' => $attachment->mimetype == 'message/rfc822' ? '8bit' : 'base64', 'content_type' => $attachment->mimetype, 'body_file' => $attachment->path, 'disposition' => 'attachment', 'filename' => $attachment->filename, 'name_encoding' => 'quoted-printable', 'headers_charset' => RCUBE_CHARSET, ); if ($attachment->content_id) { $params['cid'] = rtrim($attachment->content_id, '<>'); } if ($attachment->content_location) { $params['location'] = $attachment->content_location; } // Get attachment body if both 'path' and 'data' are NULL // this is the case when we modify only attachment metadata, e.g. filename if (empty($attachment->path) && $attachment->data === null && !empty($message) && !empty($attachment->mime_id) ) { // @TODO: do this with temp files $api->backend->storage->set_folder($message->folder); $body = $api->backend->storage->get_raw_body($message->uid, null, $attachment->mime_id . '.TEXT'); if ($attachment->encoding == 'base64') { $body = base64_decode($body); } else if ($attachment->encoding == 'quoted-printable') { $body = quoted_printable_decode($body); } $attachment->data = $body; } $mime = new Mail_mimePart($attachment->data, $params); $temp_file = tempnam($temp_dir, 'msgPart'); // @TODO: implement encodeToStream() $result = $mime->encodeToFile($temp_file); if ($result instanceof PEAR_Error) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, $result); } if ($fd = fopen($temp_file, 'r')) { stream_copy_to_stream($fd, $stream); fwrite($stream, $this->endln); fclose($fd); @unlink($temp_file); } else { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } } } diff --git a/lib/output/json/attachment.php b/lib/output/json/attachment.php index c7c160c..6c5f5ee 100644 --- a/lib/output/json/attachment.php +++ b/lib/output/json/attachment.php @@ -1,105 +1,110 @@ | +--------------------------------------------------------------------------+ | 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()) { // File upload case if (is_array($data)) { return $data; } $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; + if (isset($data->d_parameters['size'])) { + $value = (int) $data->d_parameters['size']; + } + else { + $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/tests/API/Attachments.php b/tests/API/Attachments.php index a4edbd1..f2fe5cb 100644 --- a/tests/API/Attachments.php +++ b/tests/API/Attachments.php @@ -1,352 +1,352 @@ head('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/6/2'); + self::$api->head('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/2'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing attachment - self::$api->head('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/6/2345'); + self::$api->head('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/2345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // test attachments of non-mail objects self::$api->head('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/3'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); } /** * Test attachment info */ function test_attachment_info() { - self::$api->get('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/6/2'); + self::$api->get('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/2'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('2', $body['id']); $this->assertSame('text/plain', $body['mimetype']); $this->assertSame('test.txt', $body['filename']); $this->assertSame(4, $body['size']); // and non-existing attachment - self::$api->get('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/6/2345'); + self::$api->get('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/2345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // test attachments of kolab objects self::$api->get('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/3'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('3', $body['id']); $this->assertSame('image/jpeg', $body['mimetype']); $this->assertSame('photo-mini.jpg', $body['filename']); $this->assertSame('attachment', $body['disposition']); $this->assertSame(793, $body['size']); } /** * Test attachment body */ function test_attachment_get() { // mail attachment - self::$api->get('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/6/3/get'); + self::$api->get('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/3/get'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame(793, strlen($body)); $this->assertSame('/9j/4AAQSkZJRgAB', substr(base64_encode($body), 0, 16)); // @TODO: headers // test attachments of kolab objects self::$api->get('attachments/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10/3/get'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame(4, strlen($body)); $this->assertSame('test', $body); } /** * Test attachment uploads */ function test_attachment_upload() { // do many uploads that we use later in create/update tests $post = 'Some text file'; self::$api->post('attachments/upload', array(), $post, 'application/octet-stream'); $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['upload-id'])); self::$uploaded[] = $body['upload-id']; $post = 'Some text file'; self::$api->post('attachments/upload', array('filename' => 'some.txt'), $post, 'application/octet-stream'); $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['upload-id'])); self::$uploaded[] = $body['upload-id']; $post = base64_decode('R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7'); self::$api->post('attachments/upload', array(), $post, 'application/octet-stream'); $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['upload-id'])); self::$uploaded[] = $body['upload-id']; $post = base64_decode('R0lGODlhDwAPAIAAAMDAwAAAACH5BAEAAAAALAAAAAAPAA8AQAINhI+py+0Po5y02otnAQA7'); self::$api->post('attachments/upload', array('filename' => 'empty.gif'), $post, 'application/octet-stream'); $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['upload-id'])); self::$uploaded[] = $body['upload-id']; } /** * Test attachment create */ function test_attachment_create() { // attach text file to a calendar event $post = json_encode(array( 'upload-id' => self::$uploaded[0], 'filename' => 'test.txt', 'mimetype' => 'text/plain' )); self::$api->post('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100', array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['uid'])); 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(2, $body); $this->assertSame('4', $body[1]['id']); $this->assertSame('text/plain', $body[1]['mimetype']); $this->assertSame('test.txt', $body[1]['filename']); $this->assertSame('attachment', $body[1]['disposition']); $this->assertSame(14, $body[1]['size']); // attach text file to a mail message $post = json_encode(array( 'upload-id' => self::$uploaded[1], )); - self::$api->post('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/6', array(), $post); + self::$api->post('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); - $this->assertTrue(!empty($body['uid']) && $body['uid'] != 6); + $this->assertTrue(!empty($body['uid']) && $body['uid'] != kolab_api_tests::msg_uid('6')); self::$modified = $body['uid']; self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid'] . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(3, $body); $this->assertSame('4', $body[2]['id']); $this->assertSame('text/plain', $body[2]['mimetype']); $this->assertSame('some.txt', $body[2]['filename']); $this->assertSame('attachment', $body[2]['disposition']); $this->assertSame(14, $body[2]['size']); } /** * Test attachment update */ function test_attachment_update() { $post = json_encode(array( 'upload-id' => self::$uploaded[2], )); self::$api->put('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/4', 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::$api->get('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/4'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('4', $body['id']); $this->assertSame('image/gif', $body['mimetype']); $this->assertSame(null, $body['filename']); $this->assertSame(54, $body['size']); // just rename the existing attachment $post = json_encode(array( 'filename' => 'changed.gif', )); self::$api->put('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/4', 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::$api->get('attachments/' . kolab_api_tests::folder_uid('Calendar') . '/100-100-100-100/4'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('changed.gif', $body['filename']); $this->assertSame(54, $body['size']); } /** * Test attachment delete */ function test_attachment_delete() { // delete existing attachment self::$api->delete('attachments/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10/3'); $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']) && $body['uid'] == '10-10-10-10'); // delete non-existing attachment in an existing object self::$api->delete('attachments/' . kolab_api_tests::folder_uid('Tasks') . '/10-10-10-10/3'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); // delete existing attachment - self::$api->delete('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/7/2'); + self::$api->delete('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('7') . '/2'); $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']) && $body['uid'] != 7); + $this->assertTrue(!empty($body['uid']) && $body['uid'] != kolab_api_tests::msg_uid('7')); // and non-existing attachment - self::$api->delete('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/7/20'); + self::$api->delete('attachments/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('7') . '/20'); $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 695e0f1..09db9fb 100644 --- a/tests/API/Folders.php +++ b/tests/API/Folders.php @@ -1,372 +1,373 @@ 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->assertTrue(count($body) >= 15); $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(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(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(5, (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 } + + /** + * Test deleting objects in a folder + */ + function test_folder_delete_objects() + { + // delete non-existing object + $post = json_encode(array('1-1-1-1')); + 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); + } } diff --git a/tests/API/Mails.php b/tests/API/Mails.php index 00bd267..a5b0979 100644 --- a/tests/API/Mails.php +++ b/tests/API/Mails.php @@ -1,331 +1,331 @@ get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/objects'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(5, count($body)); - $this->assertSame('1', $body[0]['uid']); + $this->assertSame(kolab_api_tests::msg_uid('1'), $body[0]['uid']); $this->assertSame('"test" wurde aktualisiert', $body[0]['subject']); - $this->assertSame('2', $body[1]['uid']); + $this->assertSame(kolab_api_tests::msg_uid('2'), $body[1]['uid']); $this->assertSame('Re: dsda', $body[1]['subject']); } /** * Test mail existence check */ function test_mail_exists() { - self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1'); + self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing mail self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test mail info */ function test_mail_info() { - self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1'); + self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); - $this->assertSame('1', $body['uid']); + $this->assertSame(kolab_api_tests::msg_uid('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'); + self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); - $this->assertSame('6', $body['uid']); + $this->assertSame(kolab_api_tests::msg_uid('6'), $body['uid']); } /** * Test counting mail attachments */ function test_count_attachments() { - self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/2/attachments'); + self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('2') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); - self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/6/attachments'); + self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(2, (int) $count); } /** * Test listing mail attachments */ function test_list_attachments() { - self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/2/attachments'); + self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('2') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); - self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/6/attachments'); + self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body); $this->assertSame('2', $body[0]['id']); $this->assertSame('text/plain', $body[0]['mimetype']); $this->assertSame('test.txt', $body[0]['filename']); $this->assertSame('attachment', $body[0]['disposition']); $this->assertSame(4, $body[0]['size']); } /** * Test mail create */ function test_mail_create() { $post = json_encode(array( 'subject' => 'Test summary', 'text' => 'This is the body.', )); 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( 'subject' => 'Test summary 2', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); // test HTML message creation $post = json_encode(array( 'subject' => 'HTML', 'html' => 'now it iś HTML', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(!empty($body['uid'])); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('HTML', $body['subject']); $this->assertRegexp('|now it iś HTML|', (string) $body['html']); } /** * Test mail update */ function test_mail_update() { $post = json_encode(array( 'subject' => 'Modified summary', 'html' => 'now it is HTML', )); self::$api->put('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . self::$created, array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(!empty($body['uid'])); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('Modified summary', $body['subject']); $this->assertRegexp('|now it is HTML|', (string) $body['html']); $this->assertSame('now it is HTML', trim($body['text'])); // 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); + self::$api->put('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('6'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertRegexp('|now it is HTML|', (string) $body['html']); $this->assertSame('now it is HTML', trim($body['text'])); $this->assertSame(5, $body['priority']); } /** * Test mail submit */ function test_mail_submit() { // send the message to self $post = json_encode(array( 'subject' => 'Test summary', 'text' => 'This is the body.', 'from' => array( 'name' => "Test' user", 'address' => self::$api->username, ), 'to' => array( array( 'name' => "Test' user", 'address' => self::$api->username, ), ), )); self::$api->post('mails/submit', array(), $post); $code = self::$api->response_code(); $this->assertEquals(204, $code); // @TODO: test submitting an existing message } /** * Test mail delete */ function test_mail_delete() { // delete existing mail - self::$api->delete('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1'); + self::$api->delete('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . kolab_api_tests::msg_uid('1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing mail self::$api->delete('mails/' . kolab_api_tests::folder_uid('INBOX') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/Mapistore/Folders.php b/tests/Mapistore/Folders.php index 8ce1bae..451107e 100644 --- a/tests/Mapistore/Folders.php +++ b/tests/Mapistore/Folders.php @@ -1,296 +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->assertTrue(count($body) >= 12); $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']); + $this->assertSame('IPF.Appointment', $body[1]['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(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(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(5, (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); } + + /** + * Test folder create + */ + function test_folder_delete_objects() + { + $post = json_encode(array(array('id' => '1-1-1-1'))); + 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); + } } diff --git a/tests/data/data.json b/tests/data/data.json index a295db4..5a3a910 100644 --- a/tests/data/data.json +++ b/tests/data/data.json @@ -1,72 +1,72 @@ { "folders": { "INBOX": { "type": "mail.inbox", "items": ["1","2","5","6","7"] }, "Trash": { "type": "mail.wastebasket", - "items": ["3","4"] + "items": [] }, "Drafts": { "type": "mail.drafts" }, "Sent": { "type": "mail.sentitems" }, "Junk": { "type": "mail.junkemail" }, "Calendar": { "type": "event.default", "items": ["100-100-100-100","101-101-101-101"] }, "Calendar/Personal Calendar": { "type": "event" }, "Contacts": { "type": "contact.default", "items": ["a-b-c-d"] }, "Files": { "type": "file.default" }, "Files2": { "type": "file" }, "Notes": { "type": "note.default", "items": ["1-1-1-1","2-2-2-2"] }, "Tasks": { "type": "task.default", "items":["10-10-10-10","20-20-20-20"] }, "Configuration": { - "type": "configuration", + "type": "configuration.default", "items": ["98-98-98-98","99-99-99-99"] }, "Mail-Test": { "type": "mail" }, "Mail-Test2": { "type": "mail" } }, "tags": { "tag1": { "members": ["1", "10-10-10-10", "1-1-1-1", "a-b-c-d"] }, "tag2": { } } } diff --git a/tests/index.php b/tests/index.php index 915375f..4e4adb6 100644 --- a/tests/index.php +++ b/tests/index.php @@ -1,48 +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'; 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')) { - // 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'; - } + // Load some helper/mockup methods for tests + require_once __DIR__ . '/lib/kolab_api_tests.php'; + kolab_api_tests::init(); $API->run(); } catch (Exception $e) { kolab_api::exception_handler($e); } diff --git a/tests/lib/bootstrap.php b/tests/lib/bootstrap.php index 2ace955..e33e8af 100644 --- a/tests/lib/bootstrap.php +++ b/tests/lib/bootstrap.php @@ -1,40 +1,32 @@ | | | | 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 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); +kolab_api_tests::init(); diff --git a/tests/lib/kolab_api_tests.php b/tests/lib/kolab_api_tests.php index fc7fc48..33775c2 100644 --- a/tests/lib/kolab_api_tests.php +++ b/tests/lib/kolab_api_tests.php @@ -1,134 +1,393 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_tests { + static $items_map; + static $folders_map; + /** * 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); } + + $username = $rcube->config->get('tests_username'); + $password = $rcube->config->get('tests_password'); + + if (!$username) { + return; + } + + $authenticated = self::login($username, $password); + + if (!$authenticated) { + throw new Exception("IMAP login failed for user $username"); + } + + // get all existing folders + $imap = $rcube->get_storage(); + $old_folders = $imap->list_folders('', '*'); + $old_subscribed = $imap->list_folders_subscribed('', '*'); + + // get configured folders + $json = file_get_contents(__DIR__ . '/../data/data.json'); + $data = json_decode($json, true); + + $items = array(); + $uids = array(); + + // initialize/update content in existing folders + // create configured folders if they do not exists + foreach ($data['folders'] as $folder_name => $folder) { + if (($idx = array_search($folder_name, $old_folders)) !== false) { + // cleanup messages in the folder + $imap->delete_message('*', $folder_name); + + unset($old_folders[$idx]); + + // make sure it's subscribed + if (!in_array($folder_name, $old_subscribed)) { + $imap->subscribe($folder_name); + } + } + else { + // create the folder + $imap->create_folder($folder_name, true); + } + + // set folder type + kolab_storage::set_folder_type($folder_name, $folder['type']); + + list($type, ) = explode('.', $folder['type']); + + // append messages + foreach ((array) $folder['items'] as $uid) { + $file = file_get_contents(__DIR__ . "/../data/$type/$uid"); + $res = $imap->save_message($folder_name, $file); + + if (is_numeric($uid)) { + $items[$uid] = $res; + } + } + } + + // remove extra folders + $deleted = array(); + foreach ($old_folders as $folder) { + // ...but only personal + if ($imap->folder_namespace($folder) == 'personal') { + $path = explode('/', $folder); + while (array_pop($path) !== null) { + if (in_array(implode('/', $path), $deleted)) { + $deleted[] = $folder; + continue 2; + } + } + + if (!$imap->delete_folder($folder)) { + throw new Exception("Failed removing '$folder'"); + } + + $deleted[] = $folder; + } + else { + } + } + + // get folder UIDs map + $uid_keys = array(kolab_storage::UID_KEY_CYRUS); + + // get folder identifiers + $metadata = $imap->get_metadata('*', $uid_keys); + + if (!is_array($metadata)) { + throw new Exception("Failed to get folders metadata"); + } + + foreach ($metadata as $folder => $meta) { + $uids[$folder] = $meta[kolab_storage::UID_KEY_CYRUS]; + } + + self::$items_map = $items; + self::$folders_map = $uids; + } + + /** + * Initialize testing environment + */ + public static function init() + { + $rcube = rcube::get_instance(); + + // If tests_username is set we use real Kolab server + // otherwise use dummy backend class which emulates a real server + if (!$rcube->config->get('tests_username')) { + // Load backend wrappers for tests + // @TODO: maybe we could replace kolab_storage and rcube_imap instead? + require_once __DIR__ . '/kolab_api_backend.php'; + } + + // Message wrapper for unit tests + require_once __DIR__ . '/kolab_api_message.php'; + + // load HTTP_Request2 wrapper for functional/integration tests + require_once __DIR__ . '/kolab_api_request.php'; + + // extend include path with kolab_format/kolab_storage classes + $include_path = __DIR__ . '/../../lib/ext/plugins/libkolab/lib' . PATH_SEPARATOR . ini_get('include_path'); + set_include_path($include_path); } /** * Initializes kolab_api_request object * * @param string Accepted response type (xml|json) * * @return kolab_api_request Request object */ public static function get_request($type, $suffix = '') { $rcube = rcube::get_instance(); $base_uri = $rcube->config->get('tests_uri', 'http://localhost/copenhagen-tests'); $username = $rcube->config->get('tests_username', 'test@example.org'); $password = $rcube->config->get('tests_password', 'test@example.org'); if ($suffix) { $base_uri .= $suffix; } $request = new kolab_api_request($base_uri, $username, $password); // set expected response type $request->set_header('Accept', $type == 'xml' ? 'application/xml' : 'application/json'); return $request; } /** * Get data object */ public static function get_data($uid, $folder_name, $type, $format = '', &$context = null) { $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 + if ($api_test && !empty(self::$folders_map)) { + if (self::$folders_map[$name]) { + return self::$folders_map[$name]; + } + + // it maybe is a newly created folder? check the metadata again + $rcube = rcube::get_instance(); + $imap = $rcube->get_storage(); + $uid_keys = array(kolab_storage::UID_KEY_CYRUS); + $metadata = $imap->get_metadata($name, $uid_keys); + + if ($uid = $metadata[$name][kolab_storage::UID_KEY_CYRUS]) { + return self::$folders_map[$name] = $uid; + } + } + return md5($name); } + /** + * Get message UID + */ + public static function msg_uid($uid, $api_test = true) + { + if ($uid && $api_test && !empty(self::$items_map)) { + if (self::$items_map[$uid]) { + return self::$items_map[$uid]; + } + } + + return $uid; + } + /** * Build MAPI object identifier */ public static function mapi_uid($folder_name, $api_test, $msg_uid, $attachment_uid = null) { $folder_uid = self::folder_uid($folder_name, $api_test); + $msg_uid = self::msg_uid($msg_uid, $api_test); return kolab_api_filter_mapistore::uid_encode($folder_uid, $msg_uid, $attachment_uid); } + + protected static function login($username, $password) + { + $rcube = rcube::get_instance(); + $login_lc = $rcube->config->get('login_lc'); + $host = $rcube->config->get('default_host'); + $default_port = $rcube->config->get('default_port', 143); + + $rcube->storage = null; + $storage = $rcube->get_storage(); + + // parse $host + $a_host = parse_url($host); + if ($a_host['host']) { + $host = $a_host['host']; + $ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null; + if (!empty($a_host['port'])) { + $port = $a_host['port']; + } + else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) { + $port = 993; + } + } + + if (!$port) { + $port = $default_port; + } + + // Convert username to lowercase. If storage backend + // is case-insensitive we need to store always the same username + if ($login_lc) { + if ($login_lc == 2 || $login_lc === true) { + $username = mb_strtolower($username); + } + else if (strpos($username, '@')) { + // lowercase domain name + list($local, $domain) = explode('@', $username); + $username = $local . '@' . mb_strtolower($domain); + } + } + + // Here we need IDNA ASCII + // Only rcube_contacts class is using domain names in Unicode + $host = rcube_utils::idn_to_ascii($host); + $username = rcube_utils::idn_to_ascii($username); + + // user already registered? + if ($user = rcube_user::query($username, $host)) { + $username = $user->data['username']; + } + + // authenticate user in IMAP + if (!$storage->connect($host, $username, $password, $port, $ssl)) { + throw new Exception("Unable to connect to IMAP"); + } + + // No user in database, but IMAP auth works + if (!is_object($user)) { + if ($rcube->config->get('auto_create_user')) { + // create a new user record + $user = rcube_user::create($username, $host); + + if (!$user) { + throw new Exception("Failed to create a user record"); + } + } + else { + throw new Exception("Access denied for new user $username. 'auto_create_user' is disabled"); + } + } + + // overwrite config with user preferences + $rcube->user = $user; + $rcube->config->set_user_prefs((array)$user->get_prefs()); +/* + $_SESSION['user_id'] = $user->ID; + $_SESSION['username'] = $user->data['username']; + $_SESSION['storage_host'] = $host; + $_SESSION['storage_port'] = $port; + $_SESSION['storage_ssl'] = $ssl; + $_SESSION['password'] = $rcube->encrypt($password); + $_SESSION['login_time'] = time(); +*/ + setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8'); + + // clear the cache + $storage->clear_cache('mailboxes', true); + + // to clear correctly the cache index in testing environments + // (where we call self::reset_backend() many times in one go) + // we need to also close() the cache + if ($ctype = $rcube->config->get('imap_cache')) { + $cache = $rcube->get_cache('IMAP', $ctype, $rcube->config->get('imap_cache_ttl', '10d')); + $cache->close(); + } + + // clear also libkolab cache + $db = $rcube->get_dbh(); + $db->query('DELETE FROM `kolab_folders`'); + + return true; + } }