diff --git a/lib/api/mails.php b/lib/api/mails.php index cff2f51..aec2e56 100644 --- a/lib/api/mails.php +++ b/lib/api/mails.php @@ -1,117 +1,127 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_mails extends kolab_api { protected $model = 'mail'; public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if ($path[0] === 'submit' && $method == 'POST') { + // submit a new message $this->api_message_submit(); } else if (!$path[1] && $path[0] && $method == 'POST') { $this->api_object_create(); } else if ($path[1]) { switch (strtolower($path[2])) { case 'attachments': if ($method == 'HEAD') { $this->api_message_count_attachments(); } else if ($method == 'GET') { $this->api_message_list_attachments(); } break; - +/* + case 'submit': + if ($method == 'POST') { + // submit an existing message + $this->api_message_submit($path[0], $path[1]); + } + break; +*/ case '': if ($method == 'GET') { $this->api_object_info(); } else if ($method == 'PUT') { $this->api_object_update(); } else if ($method == 'HEAD') { $this->api_object_exists(); } else if ($method == 'DELETE') { $this->api_object_delete(); } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Submit a message object into SMTP server */ - protected function api_message_submit() + protected function api_message_submit($folder = null, $uid = null) { // parse input and merge with current data (returns kolab_api_mail) $input = $this->input->input($this->model, false); // send the message - $uid = $input->send(); + $input->send(); - // @TODO: option to save in Sent + // @TODO: option to save message in Sent folder + // @TODO: option to send existing message + // @TODO: option to remove message from Drafts - $this->output->send(array('id' => $uid), $this->model, $folder); + $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Count message attachments */ protected function api_message_count_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $count = count($object->attachments); $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * List message attachments */ protected function api_message_list_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array('folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object); $list = $object->attachments; $this->output->send($list, 'attachment-list', $context, $props); } } diff --git a/lib/filter/mapistore/mail.php b/lib/filter/mapistore/mail.php index 141a958..b11fc21 100644 --- a/lib/filter/mapistore/mail.php +++ b/lib/filter/mapistore/mail.php @@ -1,451 +1,451 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_filter_mapistore_mail { protected $map = array( // MS-OXCMSG properties 'PidTagMessageClass' => '', // PtypString 'PidTagHasAttachments' => '', // PtypBoolean 'PidTagMessageCodepage' => '', // PtypInteger32 'PidTagMessageLocaleId' => '', // PtypInteger32 'PidTagMessageFlags' => '', // PtypInteger32, @TODO 'PidTagMessageSize' => 'size', // PtypInteger32, size in bytes 'PidTagMessageStatus' => '', // PtypInteger32 'PidTagNormalizedSubject' => '', // PtypString 'PidTagImportance' => '', // PtypInteger32 - 'PidTagPriority' => 'priority', // PtypInteger32, @TODO + 'PidTagPriority' => 'priority', // PtypInteger32, 'PidTagSensitivity' => '', // PtypInteger32 'PidLidSmartNoAttach' => '', // PtypBoolean 'PidLidPrivate' => '', // PtypBoolean 'PidLidSideEffects' => '', // PtypInteger32 'PidLidCommonStart' => '', // PtypTime 'PidLidCommonEnd' => '', // PtypTime 'PidTagAutoForwarded' => '', // PtypBoolean 'PidTagAutoForwardComment' => '', // PtypString 'PidTagCategories' => '', // PtypMultipleString 'PidLidClassification' => '', // PtypString 'PidLidClassificationDescription' => '', // PtypString 'PidLidClassified' => '', // PtypBoolean 'PidTagInternetReferences' => '', // PtypString, @TODO 'PidLidInfoPathFormName' => '', // PtypString 'PidTagMimeSkeleton' => '', // PtypBinary 'PidTagTnefCorrelationKey' => '', // PtypBinary 'PidTagAddressBookDisplayNamePrintable' => '', // PtypString 'PidTagCreatorEntryId' => '', // PtypBinary 'PidTagLastModifierEntryId' => '', // PtypBinary 'PidLidAgingDontAgeMe' => '', // PtypBoolean 'PidLidCurrentVersion' => '', // PtypInteger32 'PidLidCurrentVersionName' => '', // PtypString, @TODO: User-Agent? 'PidTagAlternateRecipientAllowed' => '', // PtypBoolean 'PidTagResponsibility' => '', // PtypBoolean 'PidTagRowid' => '', // PtypInteger32 'PidTagHasNamedProperties' => '', // PtypBoolean 'PidTagRecipientOrder' => '', // PtypInteger32 'PidNameContentBase' => '', // PtypString, Content-Base header 'PidNameAcceptLanguage' => '', // PtypString, Accept-Language header 'PidTagPurportedSenderDomain' => '', // PtypString 'PidTagStoreEntryId' => '', // PtypBinary 'PidTagTrustSender' => '', // PtypInteger32 'PidTagSubject' => 'subject', // PtypString 'PidTagMessageRecipients' => '', // PtypObject 'PidNameContentClass' => '', // PtypString, @TODO 'PidTagLocalCommitTime' => '', // PtypTime - 'PidNameContentType' => 'content-type', // PtypString, @TODO: Content-type + 'PidNameContentType' => 'content-type', // PtypString, 'PidTagCreatorName' => '', // PtypString 'PidTagMessageAttachments' => '', // PtypObject 'PidTagRead' => '', // PtypBoolean, @TODO 'PidTagRecipientDisplayName' => '', // PtypString 'PidTagRecipientEntryId' => '', // PtypBinary // body properties 'PidTagBody' => 'text', // PtypString 'PidTagNativeBody' => '', // PtypInteger32 'PidTagBodyHtml' => '', // PtypString 'PidTagRtfCompressed' => '', // PtypBinary 'PidTagRtfInSync' => '', // PtypBoolean 'PidTagInternetCodepage' => '', // PtypInteger32 'PidTagBodyContentId' => '', // PtypString 'PidTagBodyContentLocation' => '', // PtypString 'PidTagHtml' => 'html', // PtypBinary // contact linking properties 'PidLidContactLinkEntry' => '', // PtypBinary 'PidLidContacts' => '', // PtypMultipleStrings 'PidLidContactLinkName' => '', // PtypString 'PidLidContactLinkSearchKey' => '', // PtypBinary // retention and archive properties 'PidTagArchiveTag' => '', // PtypBinary 'PidTagPolicyTag' => '', // PtypBinary 'PidTagRetentionPeriod' => '', // PtypInteger32 'PidTagStartDateEtc' => '', // PtypBinary 'PidTagRetentionDate' => '', // PtypTime 'PidTagRetentionFlags' => '', // PtypInteger32 'PidTagArchivePeriod' => '', // PtypInteger32 'PidTagArchiveDate' => '', // PtypTime // MS-OXOMSG properties 'PidTagBlockStatus' => '', // PtypInteger32 'PidTagConversationId' => '', // PtypBinary, @TODO 'PidTagConversationindex' => '', // PtypBinary 'PidTagConversationindexTracking' => '', // PtypBoolean 'PidTagConversationTopic' => '', // PtypString 'DeferredDeliveryTime' => '', // PtypTime 'PidTagDisplayBcc' => '', // PtypString 'PidTagDisplayCc' => '', // PtypString 'PidTagDisplayTo' => '', // PtypString 'PidTagIconIndex' => '', // PtypInteger32, @TODO 'PidTagInternetMailOverrideFormat' => '', // PtypInteger32 'PidTagInternetMessageId' => 'message-id', // PtypString 'PidTagInReplyToId' => 'in-reply-to', // PtypString, 'PidTagLastVerbExecuted' => '', // PtypInteger32 'PidTagLastVerbExecutionTime' => '', // PtypTime 'PidTagMessageToMe' => '', // PtypBoolean, @TODO 'PidTagMessageCcMe' => '', // PtypBoolean, @TODO 'PidTagMessageRecipientMe' => '', // PtypBoolean, @TODO 'PidTagOriginatorDeliveryReportRequested' => '', // PtypBoolean, @TODO 'PidTagOriginatorNonDeliveryReportRequested' => '', // PtypBoolean 'PidTagOriginalSensitivity' => '', // PtypInteger32 'PidTagReceivedRepresentingAddressType' => '', // PtypString 'PidTagReceivedRepresentingEmailAddress' => '', // PtypString 'PidTagReceivedRepresentingEntryId' => '', // PtypBinary 'PidTagReceivedRepresentingName' => '', // PtypString 'PidTagReceivedRepresentingSearchKey' => '', // PtypBinary 'PidTagReceivedRepresentingSmtpAddress' => '', // PtypString 'PidTagReadReceiptRequested' => '', // PtypBoolean, @TODO 'PidTagReadReceiptSmtpAddress' => '', // PtypString, @TODO 'PidTagNonReceiptNotificationRequested' => '', // PtypBoolean 'PidTagOriginalAuthorEntryId' => '', // PtypBinary 'PidTagOriginalAuthorName' => '', // PtypString 'PidTagReportDisposition' => '', // PtypString, @TODO 'PidTagReportDispositionMode' => '', // PtypString, @TODO 'PidTagReceivedByAddressType' => '', // PtypString 'PidTagReceivedByEmailAddress' => '', // PtypString 'PidTagReceivedByEntryId' => '', // PtypBinary 'PidTagReceivedBySearchKey' => '', // PtypBinary 'PidTagReceivedByName' => '', // PtypString 'PidTagReceivedBySmtpAddress' => '', // PtypString 'PidTagRecipientReassignmentProhibited' => '', // PtypBoolean 'PidTagReplyRecipientEntries' => '', // PtypBinary 'PidTagReplyRecipientNames' => '', // PtypString 'PidTagReplyRequested' => '', // PtypBoolean 'PidTagResponseRequested' => '', // PtypBoolean 'PidTagSendRichInfo' => '', // PtypBoolean 'PidTagSenderAddressType' => '', // PtypString, @TODO 'PidTagSenderEmailAddress' => '', // PtypString 'PidTagSenderEntryId' => '', // PtypBinary 'PidTagSenderSearchKey' => '', // PtypBinary 'PidTagSenderName' => '', // PtypString 'PidTagSenderSmtpAddress' => '', // PtypString 'PidTagSentRepresentingAddressType' => '', // PtypString, @TODO 'PidTagSentRepresentingEmailAddress' => '', // PtypString 'PidTagSentRepresentingEntryId' => '', // PtypBinary 'PidTagSentRepresentingSearchKey' => '', // PtypBinary 'PidTagSentRepresentingName' => '', // PtypString 'PidTagSentRepresentingSmtpAddress' => '', // PtypString 'PidTagSubjectPrefix' => '', // PtypString 'PidTagTransportMessageHeaders' => '', // PtypString 'PidLidInternetAccountName' => '', // PtypString 'PidLidInternetAccountStamp' => '', // PtypString 'PidTagPrimarySendAccount' => '', // PtypString 'PidTagNextSendAcct' => '', // PtypString 'PidLidUseTnef' => '', // PtypBoolean 'PidLidAutoProcessState' => '', // PtypInteger32 'PidLidVerbStream' => '', // PtypBinary 'PidLidVerbResponse' => '', // PtypString 'PidTagTargetEntryId' => '', // PtypBinary 'PidTagAutoResponseSuppress' => '', // PtypInteger32 'PidTagMessageEditorFormat' => '', // PtypInteger32 'PidTagMessageSubmissionId' => '', // PtypBinary 'PidTagSenderIdStatus' => '', // PtypInteger32 'PidTagListHelp' => '', // PtypString, List-Help header 'PidTagListSubscribe' => '', // PtypString, List-Subscribe header 'PidTagListUnsubscribe' => '', // PtypString, List-Unsubscribe header 'PidTagDelegatedByRule' => '', // PtypBoolean 'PidTagOriginalMessageId' => '', // PtypString, @TODO 'PidTagOriginalMessageClass' => '', // PtypString // @TODO MS-OXOMSG 2.2.2 Message Status Reports // @TODO MS-OXOFLAG ); /** * Message flags for PidTagMessageFlags as defined in [MS-OXCMSG] */ protected $flags = array( 'mfRead' => 0x00000001, 'mfUnsent' => 0x00000008, 'mfResend' => 0x00000080, 'mfUnmodified' => 0x00000002, 'mfSubmitted' => 0x00000004, 'mfHasAttach' => 0x00000010, 'mfFromMe' => 0x00000020, 'mfFAI' => 0x00000040, 'mfNotifyRead' => 0x00000100, 'mfNotifyUnread' => 0x00000200, 'mfEventRead' => 0x00000400, 'mfInternet' => 0x00002000, 'mfUntrusted' => 0x00008000, ); /** * Message status for PidTagMessageStatus as defined in [MS-OXCMSG] */ protected $status = array( 'msRemoteDownload' => 0x00001000, 'msInConflict' => 0x00000800, 'msRemoteDelete' => 0x00002000, ); /** * Message importance for PidTagImportance as defined in [MS-OXCMSG] */ protected $importance = array( 'low' => 0x00000000, 'normal' => 0x00000001, 'high' => 0x00000002, ); /** * Message priority for PidTagPriority as defined in [MS-OXCMSG] */ protected $priority = array( 'urgent' => 0x00000001, 'normal' => 0x00000000, 'not-urgent' => 0xFFFFFFFF, ); /** * Message sesnitivity for PidTagSensitivity as defined in [MS-OXCMSG] */ protected $sensitivity = array( 'normal' => 0x00000000, 'personal' => 0x00000001, 'private' => 0x00000002, 'confidential' => 0x00000003, ); /** * Recipient type for PidTagRecipientType as defined in [MS-OXOMSG] */ protected $recipient_types = array( 'to' => 0x00000001, 'cc' => 0x00000002, 'bcc' => 0x00000003, ); protected $body_types = array( 'plain' => 0x00000001, 'rtf' => 0x00000002, 'html' => 0x00000003, 'signed' => 0x00000004, ); /** * Convert Kolab to MAPI * * @param array Data * @param array Context (folder_uid, object_uid, object) * * @return array Data */ public function output($data, $context = null) { $result = array( 'PidTagMessageClass' => 'IPM.Note', // mapistore REST API specific properties 'collection' => 'mails', ); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } $value = kolab_api_filter_mapistore::get_kolab_value($data, $kolab_idx); switch ($mapi_idx) { case 'PidTagInternetMessageId': case 'PidTagInReplyToId': if ($value) { $value = trim($value, '<>'); } break; case 'PidTagHtml': if ($value) { $value = base64_encode($value); } break; case 'PidTagPriority': if ($value == 1 || $value == 2) { $value = $this->priority['urgent']; } else if ($value > 3) { $value = $this->priority['not-urgent']; } else { $value = null; } break; } if ($value === null) { continue; } $result[$mapi_idx] = $value; } // Recipients (To, Cc, Bcc) foreach (array('to', 'cc', 'bcc') as $idx) { foreach ((array) $data[$idx] as $address) { // @TODO: PidTagEntryId, PidTagEmailAddress if ($address['address']) { $recipient = array( 'PidTagSmtpAddress' => $address['address'], 'PidTagAddressType' => 'EX', 'PidTagRecipientType' => $this->recipient_types[$idx], ); if ($address['name']) { $recipient['PidTagDisplayName'] = $address['name']; } $result['recipients'][] = $recipient; } } } if ($data['html']) { $result['PidTagNativeBody'] = $this->body_types['html']; } else { $result['PidTagNativeBody'] = $this->body_types['plain']; } // @TODO: PidTagHasAttachments kolab_api_filter_mapistore::parse_common_props($result, $data, $context); return $result; } /** * Convert from MAPI to Kolab * * @param array Data * @param array Data of the object that is being updated * * @return array Data */ public function input($data, $object = null) { $result = array(); foreach ($this->map as $mapi_idx => $kolab_idx) { if (empty($kolab_idx)) { continue; } if (!array_key_exists($mapi_idx, $data)) { continue; } $value = $data[$mapi_idx]; switch ($mapi_idx) { case 'PidTagInternetMessageId': case 'PidTagInReplyToId': if ($value) { $value = '<' . trim($value, '<>') . '>'; } break; case 'PidTagHtml': if ($value) { $value = base64_decode($value); } break; case 'PidTagPriority': if ($value == $this->priority['urgent']) { $value = 1; } else if ($value == $this->priority['not-urgent']) { $value = 5; } else { $value = null; } break; } $result[$kolab_idx] = $value; } // API supports only html and text, we convert RTF to HTML if needed if ($data['PidTagRtfCompressed'] && empty($data['PidTagHtml']) && class_exists('rtf')) { // The same class is used in kolab-syncroton $rtf = new rtf(); $rtf->loadrtf($data['PidTagRtfCompressed']); // @TODO: Conversion to HTML is broken, convert to text $rtf->output('ascii'); $rtf->parse(); $result['text'] = trim($rtf->out); } // Recipients (To, Cc, Bcc) if (array_key_exists('recipients', $data)) { $types = array_flip($this->recipient_types); foreach ($data['recipients'] as $recip) { // @TODO: PidTagEntryId, PidTagEmailAddress $address = $recip['PidTagSmtpAddress']; $name = $recip['PidTagDisplayName']; if ($address && ($type = $types[$recip['PidTagRecipientType']])) { $result[$type][] = array( 'address' => $address, 'name' => $name, ); } } } kolab_api_filter_mapistore::convert_common_props($result, $data, $object); return $result; } /** * Returns the attributes names mapping */ public function map() { $map = array_filter($this->map); return $map; } } diff --git a/lib/input/json/mail.php b/lib/input/json/mail.php index 885ec64..38bf16f 100644 --- a/lib/input/json/mail.php +++ b/lib/input/json/mail.php @@ -1,58 +1,68 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_input_json_mail { /** * Convert mail input array into an object to store/send * * @param array $data Request body * @param kolab_api_mail $original Original message (on update) */ public function input(&$data, $original = null) { if (empty($data) || !is_array($data)) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } - $result = $original ?: new kolab_api_mail; + if ($original) { + $result = $original; + } + // hack for unit-tests + else if (class_exists('kolab_api_message', false)) { + $result = new kolab_api_message; + } + else { + $result = new kolab_api_mail; + } + $body_fields = array('text', 'html'); $fields = array_merge(kolab_api_mail::$header_fields, $body_fields); foreach ($fields as $field) { if (!array_key_exists($field, $data)) { continue; } $result->{$field} = $data[$field]; } if (!$result->changed() || !$result->valid()) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $data = $result; } } diff --git a/lib/kolab_api.php b/lib/kolab_api.php index 0455276..7f6d7c2 100644 --- a/lib/kolab_api.php +++ b/lib/kolab_api.php @@ -1,421 +1,440 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api extends rcube { const APP_NAME = 'Kolab REST API'; const VERSION = '0.1'; public $backend; public $filter; public $input; public $output; protected $model; /** * This implements the 'singleton' design pattern * * @return kolab_api The one and only instance */ public static function get_instance() { if (!self::$instance || !is_a(self::$instance, 'kolab_api')) { $path = kolab_api_input::request_path(); $request = array_shift($path); $class = 'kolab_api_' . $request; if (!$request || !class_exists($class)) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND, array( 'code' => 500, 'line' => __LINE__, 'file' => __FILE__, 'message' => "Invalid request method: $request" )); } self::$instance = new $class(); self::$instance->startup(); } return self::$instance; } /** * Initial startup function * to register session, create database and imap connections */ protected function startup() { $this->init(self::INIT_WITH_DB | self::INIT_WITH_PLUGINS); // Get list of plugins // WARNING: We can use only plugins that are prepared for this // e.g. are not using output or rcmail objects or // doesn't throw errors when using them $plugins = (array) $this->config->get('kolab_api_plugins', array('kolab_auth')); $plugins = array_unique(array_merge($plugins, array('libkolab'))); // this way we're compatible with Roundcube Framework 1.2 // we can't use load_plugins() here foreach ($plugins as $plugin) { $this->plugins->load_plugin($plugin, true); } /* // start session $this->session_init(); // create user object $this->set_user(new rcube_user($_SESSION['user_id'])); */ } /** * Exception handler * * @param kolab_api_exception Exception */ public static function exception_handler($exception) { $code = $exception->getCode(); $message = $exception->getMessage(); if ($code == 401) { header('WWW-Authenticate: Basic realm="' . self::APP_NAME .'"'); } if (!$exception instanceof kolab_api_exception) { rcube::raise_error($exception, true, false); } header("HTTP/1.1 $code $message"); exit; } /** * Program execution handler */ protected function initialize_handler() { // Handle request input $this->input = kolab_api_input::factory($this); // Get input/output filter $this->filter = $this->input->filter; // Initialize backend $this->backend = kolab_api_backend::get_instance(); // Check authentication $this->authenticate(); // Filter the input, we want this after authentication if ($this->filter) { $this->filter->input($this->input); } // set response output class $this->output = kolab_api_output::factory($this); } /** * Script shutdown handler */ public function shutdown() { parent::shutdown(); // write performance stats to logs/console if ($this->config->get('devel_mode')) { if (function_exists('memory_get_peak_usage')) $mem = memory_get_peak_usage(); else if (function_exists('memory_get_usage')) $mem = memory_get_usage(); $log = trim(kolab_api_input::request_uri() . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '')); if (defined('KOLAB_API_START')) { rcube::print_timer(KOLAB_API_START, $log); } else { rcube::console($log); } } } /** * Authentication request handler (HTTP Auth) */ protected function authenticate() { if (!empty($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; } // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule else if (!isset($_SERVER['PHP_AUTH_USER'])) { // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..." if (isset($_SERVER['REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REMOTE_USER'], 6)); } else if (isset($_SERVER['REDIRECT_REMOTE_USER'])) { $basicAuthData = base64_decode(substr($_SERVER['REDIRECT_REMOTE_USER'], 6)); } else if (isset($_SERVER['Authorization'])) { $basicAuthData = base64_decode(substr($_SERVER['Authorization'], 6)); } else if (isset($_SERVER['HTTP_AUTHORIZATION'])) { $basicAuthData = base64_decode(substr($_SERVER['HTTP_AUTHORIZATION'], 6)); } if (isset($basicAuthData) && !empty($basicAuthData)) { list($username, $password) = explode(':', $basicAuthData); } } if (!empty($username)) { $result = $this->backend->authenticate($username, $password); } if (empty($result)) { throw new kolab_api_exception(kolab_api_exception::UNAUTHORIZED); } $this->user = $this->backend->user; } /** * Handle API request */ public function run() { $this->initialize_handler(); $path = $this->input->path; $method = $this->input->method; if (!$path[1] && $path[0] && $method == 'POST') { $this->api_object_create(); } else if ($path[1]) { switch (strtolower($path[2])) { case 'attachments': if ($method == 'HEAD') { $this->api_object_count_attachments(); } else if ($method == 'GET') { $this->api_object_list_attachments(); } break; case '': if ($method == 'GET') { $this->api_object_info(); } else if ($method == 'PUT') { $this->api_object_update(); } else if ($method == 'HEAD') { $this->api_object_exists(); } else if ($method == 'DELETE') { $this->api_object_delete(); } } } throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /** * Fetch object info */ protected function api_object_info() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array('folder_uid' => $folder, 'object' => $object); $this->output->send($object, $this->model, $context); } /** * Create an object */ protected function api_object_create() { $folder = $this->input->path[0]; $input = $this->input->input($this->model); $context = array('folder_uid' => $folder); $uid = $this->backend->object_create($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context, array('uid')); } /** * Update specified object */ protected function api_object_update() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); // parse input and merge with current data (result is in kolab_format/kolab_api_mail) $input = $this->input->input($this->model, false, $object); // update object on the backend $uid = $this->backend->object_update($folder, $input, $this->model); $this->output->send(array('uid' => $uid), $this->model, $context); } /** * Check if specified object exists */ protected function api_object_exists() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * Remove specified object */ protected function api_object_delete() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $this->backend->objects_delete($folder, array($uid)); $this->output->send_status(kolab_api_output::STATUS_EMPTY); } /** * Count object attachments */ protected function api_object_count_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); $count = !empty($object['_attachments']) ? count($object['_attachments']) : 0; $this->output->headers(array('X-Count' => $count)); $this->output->send_status(kolab_api_output::STATUS_OK); } /** * List object attachments */ protected function api_object_list_attachments() { $folder = $this->input->path[0]; $uid = $this->input->path[1]; $object = $this->backend->object_get($folder, $uid); $props = $this->input->args['properties'] ? explode(',', $this->input->args['properties']) : null; $context = array( 'folder_uid' => $folder, 'object_uid' => $uid, 'object' => $object, ); // @TODO: currently Kolab format (libkolabxml) allows attachments // in events, tasks and notes. We should support them also in contacts $list = $this->get_object_attachments($object); $this->output->send($list, 'attachment-list', $context, $props); } /** * Extract attachments from the object, depending if it's * Kolab object or email message */ protected function get_object_attachments($object) { // this is a kolab_format object data if (is_array($object)) { $list = (array) $object['_attachments']; foreach ($list as $idx => $att) { $attachment = new rcube_message_part; $attachment->mime_id = $att['id']; $attachment->filename = $att['name']; $attachment->mimetype = $att['mimetype']; $attachment->size = $att['size']; $attachment->disposition = 'attachment'; $list[$idx] = $attachment; } } // this is rcube_message(_header) else { $list = (array) $object->attachments; } return $list; } /** * Convert kolab_format object into API format * * @param array Object data in kolab_format * @param string Object type * * @return array Object data in API format */ public function get_object_data($object, $type) { $output = $this->output; if (!$this->output instanceof kolab_api_output_json) { $class = "kolab_api_output_json"; $output = new $class($this); } return $output->convert($object, $type); } + + /** + * Returns RFC2822 formatted current date in user's timezone + * + * @return string Date + */ + public function user_date() + { + // get user's timezone + try { + $tz = new DateTimeZone($this->config->get('timezone')); + $date = new DateTime('now', $tz); + } + catch (Exception $e) { + $date = new DateTime(); + } + + return $date->format('r'); + } } diff --git a/lib/kolab_api_exception.php b/lib/kolab_api_exception.php index 5a2f4bd..e7ef8cf 100644 --- a/lib/kolab_api_exception.php +++ b/lib/kolab_api_exception.php @@ -1,73 +1,76 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Main exception class for Kolab REST API responses */ class kolab_api_exception extends Exception { const UNAUTHORIZED = 401; const FORBIDDEN = 403; const NOT_FOUND = 404; const TIMEOUT = 408; const INVALID_REQUEST = 422; const SERVER_ERROR = 500; const NOT_IMPLEMENTED = 501; const UNAVAILABLE = 503; private $messages = array( self::UNAUTHORIZED => 'Unauthorized', self::FORBIDDEN => 'Forbidden', self::NOT_FOUND => 'Not found', self::TIMEOUT => 'Request timeout', self::INVALID_REQUEST => 'Invalid request', self::SERVER_ERROR => 'Internal server error', self::NOT_IMPLEMENTED => 'Not implemented', self::UNAVAILABLE => 'Service unavailable', ); /** * Constructor * - * @param int HTTP error code (default 500) - * @param array Optional error info to log + * @param int HTTP error code (default 500) + * @param array Optional error info to log + * @param string Optional error message */ - function __construct($code = 0, $error = array()) + function __construct($code = 0, $error = array(), $message = null) { - $message = $this->messages[$code]; + if (!$message) { + $message = $this->messages[$code]; + } if (!$message) { $code = self::SERVER_ERROR; $message = $this->messages[self::SERVER_ERROR]; } if (!empty($error)) { rcube::raise_error($error, true, false); } parent::__construct($message, $code); } } diff --git a/lib/kolab_api_mail.php b/lib/kolab_api_mail.php index 67cf210..57e1294 100644 --- a/lib/kolab_api_mail.php +++ b/lib/kolab_api_mail.php @@ -1,874 +1,1016 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_mail { /** * List of supported header fields * * @var array */ public static $header_fields = array( 'uid', 'subject', 'from', 'sender', 'to', 'cc', 'bcc', 'reply-to', 'in-reply-to', 'message-id', 'references', 'date', 'internaldate', 'content-type', 'priority', 'size', 'flags', 'categories', ); /** * Original message * * @var rcube_message */ protected $message; /** * Modified properties * * @var array */ protected $data = array(); /** * Validity status * * @var bool */ protected $valid = true; /** * Headers-only mode flag * * @var bool */ protected $is_header = false; /** * Line separator * * @var string */ protected $endln = "\r\n"; protected $body_text; protected $body_html; protected $boundary; + 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 'priority': case 'size': if (isset($headers->{$name})) { $value = (int) $headers->{$name}; } break; case 'content-type': $value = $headers->ctype; break; case 'date': case 'internaldate': $value = $headers->{$name}; break; case 'subject': $value = trim(rcube_mime::decode_header($headers->subject, $headers->charset)); break; case 'flags': $value = array_change_key_case((array) $headers->flags); $value = array_filter($value); $value = array_keys($value); break; case 'from': case 'sender': case 'to': case 'cc': case 'bcc': case 'reply-to': $addresses = $headers->{$name == 'reply-to' ? 'replyto' : $name}; $addresses = rcube_mime::decode_address_list($addresses, null, true, $headers->charset); $value = array(); foreach ((array) $addresses as $addr) { $idx = count($value); if ($addr['mailto']) { $value[$idx]['address'] = $addr['mailto']; } if ($addr['name']) { $value[$idx]['name'] = $addr['name']; } } if ($name == 'from' && !empty($value)) { $value = $value[0]; } break; case 'categories': $value = (array) $headers->categories; break; case 'references': case 'in-reply-to': case 'message-id': $value = $headers->get($name); break; case 'text': case 'html': $value = $this->body($name == 'html'); break; } // add the value to the result if ($value !== null && $value !== '' && (!is_array($value) || !empty($value))) { // make sure we have utf-8 here $value = rcube_charset::clean($value); } return $value; } /** * Return message data as an array * * @param array $filetr Optional properties filter * * @return array Message/headers data */ public function data($filter = array()) { $result = array(); $fields = self::$header_fields; if (!empty($filter)) { $fields = array_intersect($fields, $filter); } foreach ($fields as $field) { $value = $this->{$field}; // add the value to the result if ($value !== null && $value !== '') { $result[$field] = $value; } } // complete rcube_message object, we can set more props, e.g. body content if (!$this->is_header) { foreach (array('text', 'html') as $prop) { if ($value = $this->{$prop}) { $result[$prop] = $value; } } } return $result; } /** * Check if the original message has been modified * * @return bool True if the message has been modified * since the object creation */ public function changed() { return !empty($this->data); } /** * Check object validity * * @return bool True if the object is valid */ public function valid() { if (empty($this->message)) { // @TODO: check required properties of a new message? } return $this->valid; } /** * Save the message in specified folder * * @param string $folder IMAP folder name * * @return string New message UID * @throws kolab_api_exception */ public function save($folder = null) { if (empty($this->data)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR, array( - 'file' => __FILE__, - 'line' => __LINE__, + 'file' => __FILE__, + 'line' => __LINE__, 'message' => 'Nothing to save. Did you use kolab_api_mail::changed()?' )); } - $api = kolab_api::get_instance(); - $message = $this->get_message(); - $specials = array('flags', 'categories'); - $diff = array_diff($this->data, $specials); - $headers = array(); - $endln = $this->endln; + $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__, + '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 attachment to the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function attachment_add() + { + // @TODO + if (!($message = $this->get_message())) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + + /** + * Remove attachment from the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function attachment_remove() + { + // @TODO + if (!($message = $this->get_message())) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + + /** + * Update attachment in the message + * + * @return bool True on success, False on failure + * @throws kolab_api_exception + */ + public function attachment_update() + { + // @TODO + if (!($message = $this->get_message())) { + throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + } + } + + /** + * 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; + // header change requested, get old headers if (!empty($diff) && $message) { $api->backend->storage->set_folder($message->folder); $headers = $api->backend->storage->get_raw_headers($message->uid); $headers = self::parse_headers($headers); } foreach ($this->data as $name => $value) { $normalized = self::normalize_header_name($name); unset($headers[$normalized]); switch ($name) { case 'priority': unset($headers['X-Priority']); $priority = intval($value); $priorities = array(1 => 'highest', 2 => 'high', 4 => 'low', 5 => 'lowest'); if ($str_priority = $priorities[$priority]) { $headers['X-Priority'] = sprintf("%d (%s)", $priority, ucfirst($str_priority)); } break; case 'date': // @TODO: date-time format $headers['Date'] = $value; break; - case 'subject': - $headers['Subject'] = $value; - break; - case 'from': if (!empty($value)) { if (empty($value['address']) || !strpos($value['address'], '@')) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $value = format_email_recipient($value['address'], $value['name']); } $headers[$normalized] = $value; break; case 'to': case 'cc': case 'bcc': case 'reply-to': $recipients = array(); foreach ((array) $value as $adr) { if (!is_array($adr) || empty($adr['address']) || !strpos($adr['address'], '@')) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $recipients[] = format_email_recipient($adr['address'], $adr['name']); } $headers[$normalized] = implode(',', $recipients); break; case 'references': case 'in-reply-to': case 'message-id': + 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->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'] = $api->gen_message_id(); } // create new message header if ($stream = fopen('php://temp/maxmemory:10240000', 'r+')) { foreach ($headers as $header_name => $header_value) { if (strlen($header_value)) { + $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 + // Save/update the message body into the stream $this->write_body($stream, $headers, $text, $html); - // Save the message - $uid = $this->save_message($stream, $folder ?: $message->folder); + return $stream; + } - // IMAP flags change requested - if (array_key_exists('flags', $this->data)) { - $old_flags = $this->flags; + /** + * Send the message stream using configured method + */ + protected function send_message_stream($stream) + { + $api = kolab_api::get_instance(); - // 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); - } + // 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); } - // unset remaining old flags - foreach ($old_flags as $flag) { - $flag = 'UN' . strtoupper($flag); - $api->backend->storage->set_flag($uid, $flag, $message->folder); - } - } + rewind($stream); - return $uid; - } + $headers = null; + $sent = $api->smtp->send_mail( + $this->from_address, $this->recipients, $headers, $stream, $smtp_opts); - /** - * Send the message - * - * @return bool True on success, False on failure - * @throws kolab_api_exception - */ - public function send() - { - // @TODO - } + // log error + if (!$sent) { + $smtp_response = $api->smtp->get_response(); +// $smtp_error = $api->smtp->get_error(); - /** - * Add attachment to the message - * - * @return bool True on success, False on failure - * @throws kolab_api_exception - */ - public function attachment_add() - { - // @TODO - if (!($message = $this->get_message())) { - throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + 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), + )); + } } - } - - /** - * Remove attachment from the message - * - * @return bool True on success, False on failure - * @throws kolab_api_exception - */ - public function attachment_remove() - { - // @TODO - if (!($message = $this->get_message())) { - throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + 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.", + )); } - } - /** - * Update attachment in the message - * - * @return bool True on success, False on failure - * @throws kolab_api_exception - */ - public function attachment_update() - { - // @TODO - if (!($message = $this->get_message())) { - throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); + // $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); if (!$body_mod) { return false; } $ctype = $this->data['html'] ? 'multipart/alternative' : 'text/plain'; // Get/set Content-Type header of the modified message if ($old_ctype = $headers['Content-Type']) { if (preg_match('/boundary="?([a-z0-9-\'\(\)+_\,\.\/:=\? ]+)"?/i', $old_ctype, $matches)) { $boundary = $matches[1]; } if ($pos = strpos($old_ctype, ';')) { $old_ctype = substr($old_ctype, 0, $pos); } if ($old_ctype == 'multipart/mixed') { // replace first part (if it is text/plain or multipart/alternative) $ctype = $old_ctype; } } $headers['Content-Type'] = $ctype; if ($ctype == 'text/plain') { $headers['Content-Type'] .= '; charset=' . RCUBE_CHARSET; } else if (!$boundary) { $boundary = '_' . md5(rand() . microtime()); } if ($boundary) { $headers['Content-Type'] .= ';' . $this->endln . " boundary=\"$boundary\""; } // create message body if ($html = $this->data['html']) { $text = $this->data['text']; if ($text === null) { $h2t = new rcube_html2text($html); $text = $h2t->get_text(); } $this->body_text = quoted_printable_encode($text); $this->body_html = quoted_printable_encode($html); } else if ($text = $this->data['text']) { $headers['Content-Transfer-Encoding'] = 'quoted-printable'; $this->body_text = quoted_printable_encode($text); } $this->boundary = $boundary; + // 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(); $modified = array_key_exists('text', $this->data) || array_key_exists('html', $this->data); // @TODO: related parts for inline images // @TODO: attachment parts // nothing changed in the modified message body... if (!$modified && !empty($message)) { // just copy the content to the output stream $api->backend->storage->get_raw_body($message->uid, $stream, 'TEXT'); } // new message creation, or the message does not have any attachments else if (empty($message) || $message->headers->ctype != 'multipart/mixed') { // Here we do not have attachments yet, so we only have two // simple options: multipart/alternative or text/plain $this->write_body_content($stream, $this->boundary); } // body changed, multipart/mixed message else { // get old TEXT of the message $body_stream = fopen('php://temp/maxmemory:10240000', 'r+'); $api->backend->storage->get_raw_body($message->uid, $body_stream, 'TEXT'); rewind($body_stream); $inside = false; $done = false; $regexp = '/^--' . preg_quote($this->boundary, '/') . '(--|)\r?\n$/'; // Go and replace bodies... while (($line = fgets($body_stream, 4096)) !== false) { // boundary line if ($line[0] === '-' && $line[1] === '-' && preg_match($regexp, $line, $m)) { if ($inside) { $headers = null; $inside = false; } else if (!$done) { $headers = ''; $inside = true; } } else if ($inside) { if ($headers !== null) { // parse headers if (!strlen(rtrim($line, "\r\n"))) { // $a_headers = self::parse_headers($headers); $boundary = '_' . md5(rand() . microtime()); $this->write_body_content($stream, $boundary, true); $headers = null; $done = true; } else { $headers .= $line; } } continue; } fwrite($stream, $line); } fclose($body_stream); } } /** * Write configured text/plain or multipart/alternative * part content into message stream */ protected function write_body_content($stream, $boundary, $with_headers = false) { $endln = $this->endln; // multipart/alternative if (strlen($this->body_html)) { if ($with_headers) { fwrite($stream, 'Content-Type: multipart/alternative;' . $endln . " boundary=\"$boundary\"" . $endln . $endln); } fwrite($stream, '--' . $boundary . $endln . 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/plain; charset=UTF-8' . $endln . $endln); fwrite($stream, $this->body_text); fwrite($stream, $endln . '--' . $boundary . $endln . 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/html; charset=UTF-8' . $endln . $endln); fwrite($stream, $this->body_html); fwrite($stream, $endln . '--' . $boundary . '--' . $endln); } // text/plain else if (strlen($this->body_text)) { if ($with_headers) { fwrite($stream, 'Content-Transfer-Encoding: quoted-printable' . $endln . 'Content-Type: text/plain; charset=UTF-8' . $endln . $endln); } + // 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)); 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); + } } diff --git a/tests/API/Mails.php b/tests/API/Mails.php index a08cdc3..1271fec 100644 --- a/tests/API/Mails.php +++ b/tests/API/Mails.php @@ -1,310 +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(4, count($body)); $this->assertSame('1', $body[0]['uid']); $this->assertSame('"test" wurde aktualisiert', $body[0]['subject']); $this->assertSame('2', $body[1]['uid']); $this->assertSame('Re: dsda', $body[1]['subject']); } /** * Test mail existence check */ function test_mail_exists() { self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing mail self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/12345'); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test mail info */ function test_mail_info() { self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/1'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('1', $body['uid']); $this->assertSame('"test" wurde aktualisiert', $body['subject']); $this->assertSame(624, $body['size']); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/6'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame('6', $body['uid']); } /** * Test counting mail attachments */ function test_count_attachments() { self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/2/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); self::$api->head('mails/' . kolab_api_tests::folder_uid('INBOX') . '/6/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-Count'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(2, (int) $count); } /** * Test listing mail attachments */ function test_list_attachments() { self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/2/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/6/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body); $this->assertSame('2', $body[0]['id']); $this->assertSame('text/plain', $body[0]['mimetype']); $this->assertSame('test.txt', $body[0]['filename']); $this->assertSame('attachment', $body[0]['disposition']); $this->assertSame(4, $body[0]['size']); } /** * Test mail create */ function test_mail_create() { $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); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); self::$api->get('mails/' . kolab_api_tests::folder_uid('INBOX') . '/' . $body['uid']); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertRegexp('|now it is HTML|', (string) $body['html']); $this->assertSame('now it is HTML', trim($body['text'])); $this->assertSame(5, $body['priority']); } /** * Test mail submit */ function test_mail_submit() { - // @TODO - $this->markTestIncomplete('TODO'); + // 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'); $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/Mails.php b/tests/Mapistore/Mails.php index 0488ff8..d94e873 100644 --- a/tests/Mapistore/Mails.php +++ b/tests/Mapistore/Mails.php @@ -1,254 +1,277 @@ get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/messages'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(4, count($body)); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, '1'), $body[0]['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body[0]['parent_id']); $this->assertSame('mails', $body[0]['collection']); $this->assertSame('IPM.Note', $body[0]['PidTagMessageClass']); $this->assertSame('"test" wurde aktualisiert', $body[0]['PidTagSubject']); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, '2'), $body[1]['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body[1]['parent_id']); $this->assertSame('IPM.Note', $body[1]['PidTagMessageClass']); $this->assertSame('Re: dsda', $body[1]['PidTagSubject']); // get all messages with properties filter self::$api->get('folders/' . kolab_api_tests::folder_uid('INBOX') . '/messages', array('properties' => 'id')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array('id' => kolab_api_tests::mapi_uid('INBOX', true, '1')), $body[0]); } /** * Test mail existence check */ function test_mail_exists() { self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(200, $code); $this->assertSame('', $body); // and non-existing note self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } /** * Test mail info */ function test_mail_info() { self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, '1'), $body['id']); $this->assertSame(kolab_api_tests::folder_uid('INBOX'), $body['parent_id']); $this->assertSame('"test" wurde aktualisiert', $body['PidTagSubject']); $this->assertSame(624, $body['PidTagMessageSize']); $this->assertSame('IPM.Note', $body['PidTagMessageClass']); } /** * Test mail attachments count */ function test_count_attachments() { self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '2') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(0, (int) $count); self::$api->head('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '6') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $count = self::$api->response_header('X-mapistore-rowcount'); $this->assertEquals(200, $code); $this->assertSame('', $body); $this->assertSame(2, (int) $count); } /** * Test listing mail attachments */ function test_list_attachments() { self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '2') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertSame(array(), $body); self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '6') . '/attachments'); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(2, $body); $this->assertSame(kolab_api_tests::mapi_uid('INBOX', true, '6', '2'), $body[0]['id']); $this->assertSame('attachments', $body[0]['collection']); $this->assertSame('text/plain', $body[0]['PidTagAttachMimeTag']); $this->assertSame('test.txt', $body[0]['PidTagDisplayName']); $this->assertSame('txt', $body[0]['PidTagAttachExtension']); $this->assertSame(4, $body[0]['PidTagAttachSize']); } /** * Test mail create */ function test_mail_create() { $post = json_encode(array( 'PidTagSubject' => 'Test summary', 'PidTagBody' => 'Test description' )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertCount(1, $body); $this->assertTrue(!empty($body['id'])); // folder does not exist $post = json_encode(array( 'PidTagSubject' => 'Test summary 2', )); self::$api->post('mails/' . md5('non-existing'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(404, $code); // invalid object data $post = json_encode(array( 'test' => 'Test summary 2', )); self::$api->post('mails/' . kolab_api_tests::folder_uid('INBOX'), array(), $post); $code = self::$api->response_code(); $this->assertEquals(422, $code); } /** * Test mail update */ function test_mail_update() { $post = json_encode(array( 'PidTagSubject' => 'Modified summary', 'PidTagBody' => 'Modified description' )); self::$api->put('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '2'), array(), $post); $code = self::$api->response_code(); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertEquals(200, $code); $this->assertTrue(!empty($body['id'])); self::$api->get('mails/' . $body['id']); $body = self::$api->response_body(); $body = json_decode($body, true); $this->assertSame('Modified summary', $body['PidTagSubject']); $this->assertSame('Modified description', $body['PidTagBody']); } /** * Test mail submit */ function test_mail_submit() { - $this->markTestIncomplete('TODO'); + // send the message to self + $post = json_encode(array( + 'PidTagSubject' => 'Test summary', + 'PidTagBody' => 'This is the body.', +/* + 'from' => array( + 'name' => "Test' user", + 'address' => self::$api->username, + ), +*/ + 'recipients' => array( + array( + 'PidTagRecipientType' => 1, + 'PidTagDisplayName' => "Test user", + 'PidTagSmtpAddress' => self::$api->username, + ), + ), + )); + + self::$api->post('mails/submit', array(), $post); + + $code = self::$api->response_code(); + + $this->assertEquals(204, $code); } /** * Test mail delete */ function test_mail_delete() { // delete existing note self::$api->delete('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '1')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(204, $code); $this->assertSame('', $body); // and non-existing note self::$api->get('mails/' . kolab_api_tests::mapi_uid('INBOX', true, '12345')); $code = self::$api->response_code(); $body = self::$api->response_body(); $this->assertEquals(404, $code); $this->assertSame('', $body); } } diff --git a/tests/lib/kolab_api_backend.php b/tests/lib/kolab_api_backend.php index 7e2b91d..fd66a74 100644 --- a/tests/lib/kolab_api_backend.php +++ b/tests/lib/kolab_api_backend.php @@ -1,891 +1,899 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_backend { /** * Singleton instace of kolab_api_backend * * @var kolab_api_backend */ static protected $instance; public $delimiter = '/'; public $username = 'user@example.org'; public $storage; public $user; protected $db = array(); protected $folder = array(); protected $data = array(); /** * This implements the 'singleton' design pattern * * @return kolab_api_backend The one and only instance */ static function get_instance() { if (!self::$instance) { self::$instance = new kolab_api_backend; self::$instance->startup(); // init AFTER object was linked with self::$instance } return self::$instance; } /** * Class initialization */ public function startup() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; if (file_exists($db_file)) { $db = file_get_contents($db_file); $this->db = unserialize($db); } $json = file_get_contents(__DIR__ . '/../data/data.json'); $this->data = json_decode($json, true); $this->folders = $this->parse_folders_list($this->data['folders']); if (!array_key_exists('tags', $this->db)) { $this->db['tags'] = $this->data['tags']; } $this->user = new kolab_api_user; $this->storage = $this; } /** * Authenticate a user * * @param string Username * @param string Password * * @return bool */ public function authenticate($username, $password) { return true; } /** * Get list of folders * * @param string $type Folder type * * @return array|bool List of folders, False on backend failure */ public function folders_list($type = null) { return array_values($this->folders); } /** * Returns folder type * * @param string $uid Folder unique identifier * @param string $with_suffix Enable to not remove the subtype * * @return string Folder type */ public function folder_type($uid, $with_suffix = false) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $type = $folder['type'] ?: 'mail'; if (!$with_suffix) { list($type, ) = explode('.', $type); } return $type; } /** * Returns objects in a folder * * @param string $uid Folder unique identifier * * @return array Objects (of type kolab_api_mail or array) * @throws kolab_api_exception */ public function objects_list($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $result = array(); $is_mail = empty($folder['type']) || preg_match('/^mail/', $folder['type']); foreach ((array) $folder['items'] as $id) { $object = $this->object_get($uid, $id); if ($is_mail) { $object = new kolab_api_message($object->headers, array('is_header' => true)); } $result[] = $object; } return $result; } /** * Counts objects in a folder * * @param string $uid Folder unique identifier * * @return int Objects count * @throws kolab_api_exception */ public function objects_count($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return count($folder['items']); } /** * Delete objects in a folder * * @param string $uid Folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_delete($uid, $set) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === '*') { foreach ((array) $this->folders[$uid]['items'] as $i) { unset($this->db['messages'][$i]); } $this->folders[$uid]['items'] = array(); $this->db['items'][$uid] = array(); } else { $this->folders[$uid]['items'] = array_values(array_diff($this->folders[$uid]['items'], $set)); foreach ($set as $i) { unset($this->db['items'][$uid][$i]); unset($this->db['messages'][$i]); } } $this->db['folders'][$uid]['items'] = $this->folders[$uid]['items']; $this->save_db(); } /** * Move objects into another folder * * @param string $uid Folder unique identifier * @param string $target_uid Target folder unique identifier * @param string|array $set List of object IDs or "*" for all * * @throws kolab_api_exception */ public function objects_move($uid, $target_uid, $set) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $target = $this->folders[$target_uid]; if (!$target) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($set === "*") { $set = $this->folders[$uid]['items']; } // @TODO: we should check if all objects from the set exist $diff = array_values(array_diff($this->folders[$uid]['items'], $set)); $this->folders[$uid]['items'] = $diff; $this->db['folders'][$uid]['items'] = $diff; $diff = array_values(array_merge((array) $this->folders[$target_uid]['items'], $set)); $this->folders[$target_uid]['items'] = $diff; $this->db['folders'][$target_uid]['items'] = $diff; foreach ($set as $i) { if ($this->db['items'][$uid][$i]) { $this->db['items'][$target_uid][$i] = $this->db['items'][$uid][$i]; unset($this->db['items'][$uid][$i]); } } $this->save_db(); } /** * Get object data * * @param string $folder_uid Folder unique identifier * @param string $uid Object identifier * * @return kolab_api_mail|array Object data * @throws kolab_api_exception */ public function object_get($folder_uid, $uid) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if (!in_array($uid, (array) $folder['items'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($data = $this->db['items'][$folder_uid][$uid]) { return $data; } list($type,) = explode('.', $folder['type']); $file = $this->get_file_content($uid, $type); if (empty($file)) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } // get message content and parse it $file = str_replace("\r?\n", "\r\n", $file); $params = array('uid' => $uid, 'folder' => $folder_uid); $object = new kolab_api_message($file, $params); // get assigned tag-relations $tags = array(); foreach ($this->db['tags'] as $tag_name => $tag) { if (in_array($uid, (array) $tag['members'])) { $tags[] = $tag_name; } } if ($type != 'mail') { $object = $object->to_array($type); $object['categories'] = $tags; } else { $object = new kolab_api_message($object); $object->categories = $tags; } return $object; } /** * Create an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_create($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ $uid = microtime(true); if (is_array($data)) { $categories = $data['categories']; $data['uid'] = $uid; $this->db['items'][$folder_uid][$uid] = $data; } else { $categories = $data->categories; $uid = $data->save($folder['fullpath']); } if (!empty($categories)) { foreach ($categories as $cat) { if (!$this->db['tags'][$cat]) { $this->db['tags'][$cat] = array(); } if (!in_array($uid, (array) $this->db['tags'][$cat]['members'])) { $this->db['tags'][$cat]['members'][] = $uid; } } } $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); return $uid; } /** * Update an object * * @param string $folder_uid Folder unique identifier * @param string $data Object data * @param string $type Object type * * @throws kolab_api_exception */ public function object_update($folder_uid, $data, $type) { $folder = $this->folders[$folder_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } /* if (strpos($folder['type'], $type) !== 0) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } */ if (is_array($data)) { $uid = $data['uid']; if (array_key_exists('categories', $data)) { foreach ($this->db['tags'] as $tag_name => $tag) { if (($idx = array_search($tag_name, (array) $data['categories'])) !== false) { unset($data['categories'][$idx]); continue; } $this->db['tags'][$tag_name]['members'] = array_diff((array)$this->db['tags'][$tag_name]['members'], array($data['uid'])); } foreach ((array) $data['categories'] as $cat) { $this->db['tags'][$cat] = array('members' => array($data['uid'])); } } // remove _formatobj which is problematic in serialize/unserialize unset($data['_formatobj']); $this->db['items'][$folder_uid][$uid] = $data; $this->save_db(); } else { $uid = $data->save($folder['fullpath']); $this->folders[$folder_uid]['items'][] = $uid; $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); } return $uid; } /** * Get attachment body * * @param mixed $object Object data (from self::object_get()) * @param string $part_id Attachment part identifier * @param mixed $mode NULL to return a string, -1 to print body * or file pointer to save the body into * * @return string Attachment body if $mode=null * @throws kolab_api_exception */ public function attachment_get($object, $part_id, $mode = null) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } // check if it's not deleted if (in_array($msg_uid . ":" . $part_id, (array) $this->db['deleted_attachments'])) { throw new kolab_api_exception(kolab_api_exception::INVALID_REQUEST); } $body = $object->get_part_body($part_id); if (!$mode) { return $body; } else if ($mode === -1) { echo $body; } } /** * Delete an attachment from the message * * @param mixed $object Object data (from self::object_get()) * @param string $id Attachment identifier * * @return boolean|string True or message UID (if changed) * @throws kolab_api_exception */ public function attachment_delete($object, $id) { $msg_uid = is_array($object) ? $object['uid'] : $object->uid; $key = $msg_uid . ":" . $part_id; // check if it's not deleted if (in_array($key, (array) $this->db['deleted_attachments'])) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // object is a mail message if (!($object instanceof kolab_api_message)) { $object = $object['_message']; } if ($object->get_part_body($id) === null) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $this->db['deleted_attachments'][] = $key; $this->save_db(); } /** * Creates a folder * * @param string $name Folder name (UTF-8) * @param string $parent Parent folder identifier * @param string $type Folder type * * @return bool Folder identifier on success */ public function folder_create($name, $parent = null, $type = null) { $folder = $name; if ($parent) { $parent_folder = $this->folders[$parent]; if (!$parent_folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } $folder = $parent_folder['fullpath'] . $this->delimiter . $folder; } $uid = kolab_api_tests::folder_uid($folder, false); // check if folder exists if ($this->folders[$uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $this->folders[$uid] = array( 'name' => $name, 'fullpath' => $folder, 'parent' => $parent ? kolab_api_tests::folder_uid($parent, false) : null, 'uid' => $uid, 'type' => $type ? $type : 'mail', ); $this->db['folders'][$uid] = $this->folders[$uid]; $this->save_db(); return $uid; } /** * Updates a folder * * @param string $uid Folder identifier * @param array $updates Updates (array with keys type, subscribed, active) * * @throws kolab_api_exception */ public function folder_update($uid, $updates) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } foreach ($updates as $idx => $value) { $this->db['folders'][$uid][$idx] = $value; $this->folders[$uid][$idx] = $value; } $this->save_db(); } /** * Renames/moves a folder * * @param string $old_name Folder name (UTF8) * @param string $new_name New folder name (UTF8) * * @throws kolab_api_exception */ public function folder_rename($old_name, $new_name) { $old_uid = kolab_api_tests::folder_uid($old_name, false); $new_uid = kolab_api_tests::folder_uid($new_name, false); $folder = $this->folders[$old_uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } if ($this->folders[$new_uid]) { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $path = explode($this->delimiter, $new_name); $folder['fullpath'] = $new_name; $folder['name'] = array_pop($path); unset($this->folders[$old_uid]); $this->folders[$new_uid] = $folder; $this->db['folders'][$new_uid] = $folder; $this->db['deleted'][] = $old_uid; $this->save_db(); } /** * Deletes folder * * @param string $uid Folder UID * * @return bool True on success, False on failure * @throws kolab_api_exception */ public function folder_delete($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } unset($this->folders[$uid]); $this->db['deleted'][] = $uid; $this->save_db(); } /** * Folder info * * @param string $uid Folder UID * * @return array Folder information * @throws kolab_api_exception */ public function folder_info($uid) { $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } // some info is not very interesting here ;) unset($folder['items']); return $folder; } /** * Returns IMAP folder name with full path * * @param string $uid Folder identifier * * @return string Folder full path (UTF-8) */ public function folder_uid2path($uid) { if ($uid === null || $uid === '') { throw new kolab_api_exception(kolab_api_exception::SERVER_ERROR); } $folder = $this->folders[$uid]; if (!$folder) { throw new kolab_api_exception(kolab_api_exception::NOT_FOUND); } return $folder['fullpath']; } /** * Parse folders list into API format */ protected function parse_folders_list($list) { $folders = array(); foreach ($list as $path => $folder) { $uid = kolab_api_tests::folder_uid($path, false); if (!empty($this->db['deleted']) && in_array($uid, $this->db['deleted'])) { continue; } if (strpos($path, $this->delimiter)) { $list = explode($this->delimiter, $path); $name = array_pop($list); $parent = implode($this->delimiter, $list); $parent_id = kolab_api_tests::folder_uid($parent, false); } else { $parent_id = null; $name = $path; } $data = array( 'name' => $name, 'fullpath' => $path, 'parent' => $parent_id, 'uid' => $uid, ); if (!empty($this->db['folders']) && !empty($this->db['folders'][$uid])) { $data = array_merge($data, $this->db['folders'][$uid]); } $folders[$uid] = array_merge($folder, $data); } foreach ((array) $this->db['folders'] as $uid => $folder) { if (!$folders[$uid]) { $folders[$uid] = $folder; } } // sort folders uasort($folders, array($this, 'sort_folder_comparator')); return $folders; } /** * Callback for uasort() that implements correct * locale-aware case-sensitive sorting */ protected function sort_folder_comparator($str1, $str2) { $path1 = explode($this->delimiter, $str1['fullpath']); $path2 = explode($this->delimiter, $str2['fullpath']); foreach ($path1 as $idx => $folder1) { $folder2 = $path2[$idx]; if ($folder1 === $folder2) { continue; } return strcoll($folder1, $folder2); } } /** * Save current database state */ protected function save_db() { $api = kolab_api::get_instance(); $db_file = $api->config->get('temp_dir') . '/tests.db'; $db = serialize($this->db); file_put_contents($db_file, $db); } /** * Wrapper for rcube_imap::set_flag() */ public function set_flag($uid, $flag) { $flag = strtoupper($flag); $folder_uid = $this->folder_uid($folder); $flags = (array) $this->db['flags'][$uid]; if (strpos($flag, 'UN') === 0) { $flag = substr($flag, 3); $flags = array_values(array_diff($flags, array($flag))); } else { $flags[] = $flag; $flags = array_unique($flags); } $this->db['flags'][$uid] = $flags; $this->save_db(); return true; } /** * Wrapper for rcube_imap::save_message() */ public function save_message($folder, $streams) { $folder_uid = $this->folder_uid($folder); $uid = '3' . count($this->db['messages']) . preg_replace('/^[0-9]+\./', '', microtime(true)); $content = ''; foreach ($streams as $stream) { rewind($stream); $content .= stream_get_contents($stream); } $this->db['messages'][$uid] = base64_encode($content); $this->save_db(); return $uid; } /** * Wrapper for rcube_imap::delete_message() */ public function delete_message($uid, $folder) { $folder_uid = $this->folder_uid($folder); $this->folders[$folder_uid]['items'] = array_values(array_diff($this->folders[$folder_uid]['items'], array($uid))); unset($this->db['items'][$folder_uid][$uid]); unset($this->db['messages'][$uid]); $this->db['folders'][$folder_uid]['items'] = $this->folders[$folder_uid]['items']; $this->save_db(); return true; } /** * Wrapper for rcube_imap::get_raw_body */ public function get_raw_body($uid, $fp = null, $part = null) { $file = $this->get_file_content($uid, 'mail'); $file = explode("\r\n\r\n", $file, 2); // we assume $part=TEXT if ($fp) { fwrite($fp, $file[1]); return true; } else { echo $file[1]; } } /** * Wrapper for rcube_imap::get_raw_headers */ public function get_raw_headers($uid) { $file = $this->get_file_content($uid, 'mail'); $file = explode("\r\n\r\n", $file, 2); return $file[0]; } /** * Wrapper for rcube_imap::set_folder */ public function set_folder($folder) { // do nothing } /** * Find folder UID by its name */ protected function folder_uid($name) { foreach ($this->folders as $uid => $folder) { if ($folder['fullpath'] == $name) { return $uid; } } } /** * Get sample message from tests/data dir */ protected function get_file_content($uid, $type) { if ($file = $this->db['messages'][$uid]) { $file = base64_decode($file); } else { $file = file_get_contents(__DIR__ . '/../data/' . $type . '/' . $uid); } return $file; } } /** * Dummy class imitating rcube_user */ class kolab_api_user { public function get_username() { return 'user@example.org'; } public function get_user_id() { return 10; } + + public function get_identity() + { + return array( + 'email' => 'user@example.org', + 'name' => 'Test User', + ); + } } diff --git a/tests/lib/kolab_api_message.php b/tests/lib/kolab_api_message.php index 0cb6340..0ace403 100644 --- a/tests/lib/kolab_api_message.php +++ b/tests/lib/kolab_api_message.php @@ -1,237 +1,238 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ +/** + * Mock class emulating rcube_message and kolab_api_mail + * With some additional functionality for testing + */ class kolab_api_message extends kolab_api_mail { public $attachments = array(); public $parts = array(); public $mime_parts = array(); public $folder; public $uid; public $headers; /** * Class initialization */ - public function __construct($content, $params = array()) + public function __construct($content = null, $params = array()) { // kolab_api_mail mode if (is_object($content)) { $this->message = $content; $this->headers = $content->headers; $this->mime_parts = $content->mime_parts; $params['folder'] = $content->folder ?: $content->headers->folder; $params['uid'] = $content->uid ?: $content->headers->uid; } // rcube_message mode else if ($content) { $this->message = rcube_mime::parse_message($content); $this->headers = rcube_message_header::from_array($this->message->headers); $this->headers->ctype = $this->message->mimetype; foreach ((array) $params as $idx => $val) { $this->headers->{$idx} = $val; } $this->headers->size = strlen($content); $this->set_mime_parts($this->message); } if ($this->message) { foreach ((array) $this->message->parts as $part) { if ($part->filename) { $this->attachments[] = $part; } $this->parts[$part->mime_id] = $part; } } foreach ((array) $params as $idx => $val) { $this->{$idx} = $val; } } /** * Returns body of the message part */ public function get_part_body($id) { if (!$id) { return $this->message->body; } return $this->mime_parts[$id]->body; } /** * Convert message into Kolab object * * @return array Kolab object */ public function to_array() { $object_type = kolab_format::mime2object_type($this->headers->others['x-kolab-type']); $content_type = kolab_format::KTYPE_PREFIX . $object_type; $attachments = array(); // get XML part foreach ((array)$this->attachments as $part) { if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) { $xml = $this->get_part_body($part->mime_id); } else if ($part->filename || $part->content_id) { $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename; $size = null; // Use Content-Disposition 'size' as for the Kolab Format spec. if (isset($part->d_parameters['size'])) { $size = $part->d_parameters['size']; } // we can trust part size only if it's not encoded else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') { $size = $part->size; } // looks like MimeDecode does not support d_parameters (?) else if (preg_match('/size=([0-9]+)/', $part->headers['content-disposition'], $m)) { $size = $m[1]; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'size' => $size, ); } } // check kolab format version $format_version = $this->headers->others['x-kolab-mime-version']; if (empty($format_version)) { list($xmltype, $subtype) = explode('.', $object_type); $xmlhead = substr($xml, 0, 512); // detect old Kolab 2.0 format if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false) $format_version = '2.0'; else $format_version = '3.0'; // assume 3.0 } // get Kolab format handler for the given type $format = kolab_format::factory($object_type, $format_version, $xml); if (is_a($format, 'PEAR_Error')) { return false; } // load Kolab object from XML part $format->load($xml); if ($format->is_valid()) { $object = $format->to_array(array('_attachments' => $attachments)); $object['_formatobj'] = $format; $object['_type'] = $object_type; $object['_attachments'] = $attachments; $object['_message'] = $this; // $object['_msguid'] = $msguid; // $object['_mailbox'] = $this->name; return $object; } return false; } /** - * Send the message - * - * @return bool True on success, False on failure - * @throws kolab_api_exception + * Send the message stream using configured method */ - public function send() + protected function send_message_stream($stream) { - // @TODO + return true; } /** * Get rcube_message object of the assigned message */ protected function get_message() { return $this->message; } /** * rcube_message::first_html_part() emulation. */ public function first_html_part(&$part = null, $enriched = false) { foreach ((array) $this->mime_parts as $part) { if (!$part->filename && $part->mimetype == 'text/html') { return $this->get_part_body($part->mime_id, true); } } $part = null; } /** * rcube_message::first_text_part() emulation. */ public function first_text_part(&$part = null, $strict = false) { // no message structure, return complete body if (empty($this->mime_parts)) { return $this->message->body; } foreach ((array) $this->mime_parts as $part) { if (!$part->filename && $part->mimetype == 'text/plain') { return $this->get_part_body($part->mime_id, true); } } $part = null; } /** * Fill aflat array with references to all parts, indexed by part numbers */ private function set_mime_parts(&$part) { if (strlen($part->mime_id)) { $this->mime_parts[$part->mime_id] = &$part; } if (is_array($part->parts)) { for ($i = 0; $i < count($part->parts); $i++) { $this->set_mime_parts($part->parts[$i]); } } } } diff --git a/tests/lib/kolab_api_request.php b/tests/lib/kolab_api_request.php index 2411e8a..eea9901 100644 --- a/tests/lib/kolab_api_request.php +++ b/tests/lib/kolab_api_request.php @@ -1,214 +1,217 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_request { + public $username; + private $request; private $base_url; /** * Class initialization */ public function __construct($base_url, $user, $pass) { require_once 'HTTP/Request2.php'; $this->base_url = $base_url; $this->request = new HTTP_Request2(); + $this->username = $user; $this->request->setConfig(array( 'ssl_verify_peer' => false, 'ssl_verify_host' => false, )); $this->request->setAuth($user, $pass); } /** * Set request header */ public function set_header($name, $value) { $this->request->setHeader($name, $value); } /** * API's GET request. * * @param string URL * @param array URL arguments * * @return HTTP_Request2_Response Response object */ public function get($url, $args = array()) { $url = $this->build_url($url, $args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_GET); return $this->get_response($url); } /** * API's HEAD request. * * @param string URL * @param array URL arguments * * @return HTTP_Request2_Response Response object */ public function head($url, $args = array()) { $url = $this->build_url($url, $args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_HEAD); return $this->get_response($url); } /** * API's DELETE request. * * @param string URL * @param array URL arguments * * @return HTTP_Request2_Response Response object */ public function delete($url, $args = array()) { $url = $this->build_url($url, $args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_DELETE); return $this->get_response($url); } /** * API's POST request. * * @param string URL * @param array URL arguments * @param string POST body * * @return HTTP_Request2_Response Response object */ public function post($url, $url_args = array(), $post = '') { $url = $this->build_url($url, $url_args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_POST); $this->request->setBody($post); return $this->get_response($url); } /** * API's PUT request. * * @param string URL * @param array URL arguments * @param string PUT body * * @return HTTP_Request2_Response Response object */ public function put($url, $url_args = array(), $post = '') { $url = $this->build_url($url, $url_args); $this->reset(); $this->request->setMethod(HTTP_Request2::METHOD_PUT); $this->request->setBody($post); return $this->get_response($url); } public function response_code() { return $this->response->getStatus(); } public function response_header($name) { return $this->response->getHeader($name); } public function response_body() { return $this->response->getBody(); } /** * @param string Action URL * @param array GET parameters (hash array: name => value) * * @return Net_URL2 URL object */ private function build_url($action, $args) { $url = $this->base_url; if ($action) { $url .= '/' . $action; } $url = new Net_URL2($url); if (!empty($args)) { $url->setQueryVariables($args); } return $url; } /** * Reset old request data */ private function reset() { // reset old body $this->request->setBody(''); unset($this->response); } /** * HTTP Response handler. * * @param Net_URL2 URL object * * @return HTTP_Request2_Response Response object */ private function get_response($url) { $this->request->setUrl($url); return $this->response = $this->request->send(); } } diff --git a/tests/lib/kolab_api_tests.php b/tests/lib/kolab_api_tests.php index 6f3cdb4..fc7fc48 100644 --- a/tests/lib/kolab_api_tests.php +++ b/tests/lib/kolab_api_tests.php @@ -1,134 +1,134 @@ | | | | This program is free software: you can redistribute it and/or modify | | it under the terms of the GNU Affero General Public License as published | | by the Free Software Foundation, either version 3 of the License, or | | (at your option) any later version. | | | | This program is distributed in the hope that it will be useful, | | but WITHOUT ANY WARRANTY; without even the implied warranty of | | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | | GNU Affero General Public License for more details. | | | | You should have received a copy of the GNU Affero General Public License | | along with this program. If not, see | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class kolab_api_tests { /** * Reset backend state */ public static function reset_backend() { // @TODO: reseting real server $rcube = rcube::get_instance(); $temp_dir = $rcube->config->get('temp_dir'); $filename = $temp_dir . '/tests.db'; if (file_exists($filename)) { unlink($filename); } } /** * Initializes kolab_api_request object * * @param string Accepted response type (xml|json) * * @return kolab_api_request Request object */ public static function get_request($type, $suffix = '') { $rcube = rcube::get_instance(); $base_uri = $rcube->config->get('tests_uri', 'http://localhost/copenhagen-tests'); - $username = $rcube->config->get('tests_username', 'test'); - $password = $rcube->config->get('tests_password', 'test'); + $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 return md5($name); } /** * Build MAPI object identifier */ public static function mapi_uid($folder_name, $api_test, $msg_uid, $attachment_uid = null) { $folder_uid = self::folder_uid($folder_name, $api_test); return kolab_api_filter_mapistore::uid_encode($folder_uid, $msg_uid, $attachment_uid); } }