diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php index 318dce87..55f5f585 100644 --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -1,1671 +1,1667 @@ * @author Aleksander Machniak * * Copyright (C) 2012-2014, Kolab Systems AG * * 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 . */ class kolab_storage { const CTYPE_KEY = '/shared/vendor/kolab/folder-type'; const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type'; const COLOR_KEY_SHARED = '/shared/vendor/kolab/color'; const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color'; const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname'; const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname'; const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid'; const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid'; const ERROR_IMAP_CONN = 1; const ERROR_CACHE_DB = 2; const ERROR_NO_PERMISSION = 3; const ERROR_INVALID_FOLDER = 4; public static $version = '3.0'; public static $last_error; public static $encode_ids = false; private static $ready = false; private static $with_tempsubs = true; private static $subscriptions; private static $typedata = array(); private static $states; private static $config; private static $imap; private static $ldap; // Default folder names private static $default_folders = array( 'event' => 'Calendar', 'contact' => 'Contacts', 'task' => 'Tasks', 'note' => 'Notes', 'file' => 'Files', 'configuration' => 'Configuration', 'journal' => 'Journal', 'mail.inbox' => 'INBOX', 'mail.drafts' => 'Drafts', 'mail.sentitems' => 'Sent', 'mail.wastebasket' => 'Trash', 'mail.outbox' => 'Outbox', 'mail.junkemail' => 'Junk', ); /** * Setup the environment needed by the libs */ public static function setup() { if (self::$ready) return true; $rcmail = rcube::get_instance(); self::$config = $rcmail->config; self::$version = strval($rcmail->config->get('kolab_format_version', self::$version)); self::$imap = $rcmail->get_storage(); self::$ready = class_exists('kolabformat') && (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2')); if (self::$ready) { - // set imap options - self::$imap->set_options(array( - 'skip_deleted' => true, - 'threading' => false, - )); + // do nothing } else if (!class_exists('kolabformat')) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "required kolabformat module not found" ), true); } else { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "IMAP server doesn't support METADATA or ANNOTATEMORE" ), true); } // adjust some configurable settings if ($event_scheduling_prop = $rcmail->config->get('kolab_event_scheduling_properties', null)) { kolab_format_event::$scheduling_properties = (array)$event_scheduling_prop; } // adjust some configurable settings if ($task_scheduling_prop = $rcmail->config->get('kolab_task_scheduling_properties', null)) { kolab_format_task::$scheduling_properties = (array)$task_scheduling_prop; } return self::$ready; } /** * Initializes LDAP object to resolve Kolab users */ public static function ldap() { if (self::$ldap) { return self::$ldap; } self::setup(); $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook')); if (!is_array($config)) { $ldap_config = (array)self::$config->get('ldap_public'); $config = $ldap_config[$config]; } if (empty($config)) { return null; } // overwrite filter option if ($filter = self::$config->get('kolab_users_filter')) { self::$config->set('kolab_auth_filter', $filter); } // re-use the LDAP wrapper class from kolab_auth plugin require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php'; self::$ldap = new kolab_auth_ldap($config); return self::$ldap; } /** * Get a list of storage folders for the given data type * * @param string Data type to list folders for (contact,distribution-list,event,task,note) * @param boolean Enable to return subscribed folders only (null to use configured subscription mode) * * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP) */ public static function get_folders($type, $subscribed = null) { $folders = $folderdata = array(); if (self::setup()) { foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) { $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]); } } return $folders; } /** * Getter for the storage folder for the given type * * @param string Data type to list folders for (contact,distribution-list,event,task,note) * @return object kolab_storage_folder The folder object */ public static function get_default_folder($type) { if (self::setup()) { foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) { return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]); } } return null; } /** * Getter for a specific storage folder * * @param string IMAP folder to access (UTF7-IMAP) * @param string Expected folder type * * @return object kolab_storage_folder The folder object */ public static function get_folder($folder, $type = null) { return self::setup() ? new kolab_storage_folder($folder, $type) : null; } /** * Getter for a single Kolab object, identified by its UID. * This will search all folders storing objects of the given type. * * @param string Object UID * @param string Object type (contact,event,task,journal,file,note,configuration) * @return array The Kolab object represented as hash array or false if not found */ public static function get_object($uid, $type) { self::setup(); $folder = null; foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) { if (!$folder) $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]); else $folder->set_folder($foldername, $type, $folderdata[$foldername]); if ($object = $folder->get_object($uid)) return $object; } return false; } /** * Execute cross-folder searches with the given query. * * @param array Pseudo-SQL query as list of filter parameter triplets * @param string Folder type (contact,event,task,journal,file,note,configuration) * @param int Expected number of records or limit (for performance reasons) * * @return array List of Kolab data objects (each represented as hash array) * @see kolab_storage_format::select() */ public static function select($query, $type, $limit = null) { self::setup(); $folder = null; $result = array(); foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) { $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]); if ($limit) { $folder->set_order_and_limit(null, $limit); } foreach ($folder->select($query) as $object) { $result[] = $object; } } return $result; } /** * Returns Free-busy server URL */ public static function get_freebusy_server() { self::setup(); $url = 'https://' . $_SESSION['imap_host'] . '/freebusy'; $url = self::$config->get('kolab_freebusy_server', $url); $url = rcube_utils::resolve_url($url); return unslashify($url); } /** * Compose an URL to query the free/busy status for the given user * * @param string Email address of the user to get free/busy data for * @param object DateTime Start of the query range (optional) * @param object DateTime End of the query range (optional) * * @return string Fully qualified URL to query free/busy data */ public static function get_freebusy_url($email, $start = null, $end = null) { $query = ''; $param = array(); $utc = new \DateTimeZone('UTC'); if ($start instanceof \DateTime) { $start->setTimezone($utc); $param['dtstart'] = $start->format('Ymd\THis\Z'); } if ($end instanceof \DateTime) { $end->setTimezone($utc); $param['dtend'] = $end->format('Ymd\THis\Z'); } if (!empty($param)) { $query = '?' . http_build_query($param); } return self::get_freebusy_server() . '/' . $email . '.ifb' . $query; } /** * Creates folder ID from folder name * * @param string $folder Folder name (UTF7-IMAP) * @param boolean $enc Use lossless encoding * @return string Folder ID string */ public static function folder_id($folder, $enc = null) { return $enc == true || ($enc === null && self::$encode_ids) ? self::id_encode($folder) : asciiwords(strtr($folder, '/.-', '___')); } /** * Encode the given ID to a safe ascii representation * * @param string $id Arbitrary identifier string * * @return string Ascii representation */ public static function id_encode($id) { return rtrim(strtr(base64_encode($id), '+/', '-_'), '='); } /** * Convert the given identifier back to it's raw value * * @param string $id Ascii identifier * @return string Raw identifier string */ public static function id_decode($id) { return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT)); } /** * Return the (first) path of the requested IMAP namespace * * @param string Namespace name (personal, shared, other) * @return string IMAP root path for that namespace */ public static function namespace_root($name) { foreach ((array)self::$imap->get_namespace($name) as $paths) { if (strlen($paths[0]) > 1) { return $paths[0]; } } return ''; } /** * Deletes IMAP folder * * @param string $name Folder name (UTF7-IMAP) * * @return bool True on success, false on failure */ public static function folder_delete($name) { // clear cached entries first if ($folder = self::get_folder($name)) $folder->cache->purge(); $rcmail = rcube::get_instance(); $plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name)); $success = self::$imap->delete_folder($name); self::$last_error = self::$imap->get_error_str(); return $success; } /** * Creates IMAP folder * * @param string $name Folder name (UTF7-IMAP) * @param string $type Folder type * @param bool $subscribed Sets folder subscription * @param bool $active Sets folder state (client-side subscription) * * @return bool True on success, false on failure */ public static function folder_create($name, $type = null, $subscribed = false, $active = false) { self::setup(); $rcmail = rcube::get_instance(); $plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array( 'name' => $name, 'subscribe' => $subscribed, ))); if ($saved = self::$imap->create_folder($name, $subscribed)) { // set metadata for folder type if ($type) { $saved = self::set_folder_type($name, $type); // revert if metadata could not be set if (!$saved) { self::$imap->delete_folder($name); } // activate folder else if ($active) { self::set_state($name, true); } } } if ($saved) { return true; } self::$last_error = self::$imap->get_error_str(); return false; } /** * Renames IMAP folder * * @param string $oldname Old folder name (UTF7-IMAP) * @param string $newname New folder name (UTF7-IMAP) * * @return bool True on success, false on failure */ public static function folder_rename($oldname, $newname) { self::setup(); $rcmail = rcube::get_instance(); $plugin = $rcmail->plugins->exec_hook('folder_rename', array( 'oldname' => $oldname, 'newname' => $newname)); $oldfolder = self::get_folder($oldname); $active = self::folder_is_active($oldname); $success = self::$imap->rename_folder($oldname, $newname); self::$last_error = self::$imap->get_error_str(); // pass active state to new folder name if ($success && $active) { self::set_state($oldname, false); self::set_state($newname, true); } // assign existing cache entries to new resource uri if ($success && $oldfolder) { $oldfolder->cache->rename($newname); } return $success; } /** * Rename or Create a new IMAP folder. * * Does additional checks for permissions and folder name restrictions * * @param array Hash array with folder properties and metadata * - name: Folder name * - oldname: Old folder name when changed * - parent: Parent folder to create the new one in * - type: Folder type to create * - subscribed: Subscribed flag (IMAP subscription) * - active: Activation flag (client-side subscription) * @return mixed New folder name or False on failure */ public static function folder_update(&$prop) { self::setup(); $folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP'); $oldfolder = $prop['oldname']; // UTF7 $parent = $prop['parent']; // UTF7 $delimiter = self::$imap->get_hierarchy_delimiter(); if (strlen($oldfolder)) { $options = self::$imap->folder_info($oldfolder); } if (!empty($options) && ($options['norename'] || $options['protected'])) { } // sanity checks (from steps/settings/save_folder.inc) else if (!strlen($folder)) { self::$last_error = 'cannotbeempty'; return false; } else if (strlen($folder) > 128) { self::$last_error = 'nametoolong'; return false; } else { // these characters are problematic e.g. when used in LIST/LSUB foreach (array($delimiter, '%', '*') as $char) { if (strpos($folder, $char) !== false) { self::$last_error = 'forbiddencharacter'; return false; } } } if (!empty($options) && ($options['protected'] || $options['norename'])) { $folder = $oldfolder; } else if (strlen($parent)) { $folder = $parent . $delimiter . $folder; } else { // add namespace prefix (when needed) $folder = self::$imap->mod_folder($folder, 'in'); } // Check access rights to the parent folder if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) { $parent_opts = self::$imap->folder_info($parent); if ($parent_opts['namespace'] != 'personal' && (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights']))) ) { self::$last_error = 'No permission to create folder'; return false; } } // update the folder name if (strlen($oldfolder)) { if ($oldfolder != $folder) { $result = self::folder_rename($oldfolder, $folder); } else { $result = true; } } // create new folder else { $result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']); } if ($result) { self::set_folder_props($folder, $prop); } return $result ? $folder : false; } /** * Getter for human-readable name of Kolab object (folder) * with kolab_custom_display_names support. * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference * * @param string $folder IMAP folder name (UTF7-IMAP) * @param string $folder_ns Will be set to namespace name of the folder * * @return string Name of the folder-object */ public static function object_name($folder, &$folder_ns=null) { // find custom display name in folder METADATA if ($name = self::custom_displayname($folder)) { return $name; } return self::object_prettyname($folder, $folder_ns); } /** * Get custom display name (saved in metadata) for the given folder */ public static function custom_displayname($folder) { // find custom display name in folder METADATA if (self::$config->get('kolab_custom_display_names', true) && self::setup()) { // For performance reasons ask for all folders, it will be cached as one cache entry $metadata = self::$imap->get_metadata("*", array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED)); if ($data = $metadata[$folder]) { if (($name = $data[self::NAME_KEY_PRIVATE]) || ($name = $data[self::NAME_KEY_SHARED])) { return $name; } } } return false; } /** * Getter for human-readable name of Kolab object (folder) * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference * * @param string $folder IMAP folder name (UTF7-IMAP) * @param string $folder_ns Will be set to namespace name of the folder * * @return string Name of the folder-object */ public static function object_prettyname($folder, &$folder_ns=null) { self::setup(); $found = false; $namespace = self::$imap->get_namespace(); if (!empty($namespace['shared'])) { foreach ($namespace['shared'] as $ns) { if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { $prefix = ''; $folder = substr($folder, strlen($ns[0])); $delim = $ns[1]; $found = true; $folder_ns = 'shared'; break; } } } if (!$found && !empty($namespace['other'])) { foreach ($namespace['other'] as $ns) { if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { // remove namespace prefix $folder = substr($folder, strlen($ns[0])); $delim = $ns[1]; // get username $pos = strpos($folder, $delim); if ($pos) { $prefix = '('.substr($folder, 0, $pos).')'; $folder = substr($folder, $pos+1); } else { $prefix = '('.$folder.')'; $folder = ''; } $found = true; $folder_ns = 'other'; break; } } } if (!$found && !empty($namespace['personal'])) { foreach ($namespace['personal'] as $ns) { if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) { // remove namespace prefix $folder = substr($folder, strlen($ns[0])); $prefix = ''; $delim = $ns[1]; $found = true; break; } } } if (empty($delim)) $delim = self::$imap->get_hierarchy_delimiter(); $folder = rcube_charset::convert($folder, 'UTF7-IMAP'); $folder = html::quote($folder); $folder = str_replace(html::quote($delim), ' » ', $folder); if ($prefix) $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : ''); if (!$folder_ns) $folder_ns = 'personal'; return $folder; } /** * Helper method to generate a truncated folder name to display. * Note: $origname is a string returned by self::object_name() */ public static function folder_displayname($origname, &$names) { $name = $origname; // find folder prefix to truncate for ($i = count($names)-1; $i >= 0; $i--) { if (strpos($name, $names[$i] . ' » ') === 0) { $length = strlen($names[$i] . ' » '); $prefix = substr($name, 0, $length); $count = count(explode(' » ', $prefix)); $diff = 1; // check if prefix folder is in other users namespace for ($n = count($names)-1; $n >= 0; $n--) { if (strpos($prefix, '(' . $names[$n] . ') ') === 0) { $diff = 0; break; } } $name = str_repeat('   ', $count - $diff) . '» ' . substr($name, $length); break; } // other users namespace and parent folder exists else if (strpos($name, '(' . $names[$i] . ') ') === 0) { $length = strlen('(' . $names[$i] . ') '); $prefix = substr($name, 0, $length); $count = count(explode(' » ', $prefix)); $name = str_repeat('   ', $count) . '» ' . substr($name, $length); break; } } $names[] = $origname; return $name; } /** * Creates a SELECT field with folders list * * @param string $type Folder type * @param array $attrs SELECT field attributes (e.g. name) * @param string $current The name of current folder (to skip it) * * @return html_select SELECT object */ public static function folder_selector($type, $attrs, $current = '') { // get all folders of specified type (sorted) $folders = self::get_folders($type, true); $delim = self::$imap->get_hierarchy_delimiter(); $names = array(); $len = strlen($current); if ($len && ($rpos = strrpos($current, $delim))) { $parent = substr($current, 0, $rpos); $p_len = strlen($parent); } // Filter folders list foreach ($folders as $c_folder) { $name = $c_folder->name; // skip current folder and it's subfolders if ($len) { if ($name == $current) { // Make sure parent folder is listed (might be skipped e.g. if it's namespace root) if ($p_len && !isset($names[$parent])) { $names[$parent] = self::object_name($parent); } continue; } if (strpos($name, $current.$delim) === 0) { continue; } } // always show the parent of current folder if ($p_len && $name == $parent) { } // skip folders where user have no rights to create subfolders else if ($c_folder->get_owner() != $_SESSION['username']) { $rights = $c_folder->get_myrights(); if (!preg_match('/[ck]/', $rights)) { continue; } } $names[$name] = $c_folder->get_name(); } // Build SELECT field of parent folder $attrs['is_escaped'] = true; $select = new html_select($attrs); $select->add('---', ''); $listnames = array(); foreach (array_keys($names) as $imap_name) { $name = $origname = $names[$imap_name]; // find folder prefix to truncate for ($i = count($listnames)-1; $i >= 0; $i--) { if (strpos($name, $listnames[$i].' » ') === 0) { $length = strlen($listnames[$i].' » '); $prefix = substr($name, 0, $length); $count = count(explode(' » ', $prefix)); $name = str_repeat('  ', $count-1) . '» ' . substr($name, $length); break; } } $listnames[] = $origname; $select->add($name, $imap_name); } return $select; } /** * Returns a list of folder names * * @param string Optional root folder * @param string Optional name pattern * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration) * @param boolean Enable to return subscribed folders only (null to use configured subscription mode) * @param array Will be filled with folder-types data * * @return array List of folders */ public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array()) { if (!self::setup()) { return null; } // use IMAP subscriptions if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) { $subscribed = true; } if (!$filter) { // Get ALL folders list, standard way if ($subscribed) { $folders = self::_imap_list_subscribed($root, $mbox); } else { $folders = self::_imap_list_folders($root, $mbox); } return $folders; } $prefix = $root . $mbox; $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/'; // get folders types for all folders if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) { $folderdata = self::folders_typedata($prefix); } else { // fetch folder types for the effective list of (subscribed) folders when post-filtering $folderdata = array(); } if (!is_array($folderdata)) { return array(); } // In some conditions we can skip LIST command (?) if (!$subscribed && $filter != 'mail' && $prefix == '*') { foreach ($folderdata as $folder => $type) { if (!preg_match($regexp, $type)) { unset($folderdata[$folder]); } } return self::$imap->sort_folder_list(array_keys($folderdata), true); } // Get folders list if ($subscribed) { $folders = self::_imap_list_subscribed($root, $mbox); } else { $folders = self::_imap_list_folders($root, $mbox); } // In case of an error, return empty list (?) if (!is_array($folders)) { return array(); } // Filter folders list foreach ($folders as $idx => $folder) { // lookup folder type if (!array_key_exists($folder, $folderdata)) { $folderdata[$folder] = self::folder_type($folder); } $type = $folderdata[$folder]; if ($filter == 'mail' && empty($type)) { continue; } if (empty($type) || !preg_match($regexp, $type)) { unset($folders[$idx]); } } return $folders; } /** * Wrapper for rcube_imap::list_folders() with optional post-filtering */ protected static function _imap_list_folders($root, $mbox) { $postfilter = null; // compose a post-filter expression for the excluded namespaces if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) { $excludes = array(); foreach ((array)$skip_ns as $ns) { if ($ns_root = self::namespace_root($ns)) { $excludes[] = $ns_root; } } if (count($excludes)) { $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!'; } } // use normal LIST command to return all folders, it's fast enough $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter)); if (!empty($postfilter)) { $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); }); $folders = self::$imap->sort_folder_list($folders); } return $folders; } /** * Wrapper for rcube_imap::list_folders_subscribed() * with support for temporarily subscribed folders */ protected static function _imap_list_subscribed($root, $mbox) { $folders = self::$imap->list_folders_subscribed($root, $mbox); // add temporarily subscribed folders if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) { $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders'])); } return $folders; } /** * Search for shared or otherwise not listed groupware folders the user has access * * @param string Folder type of folders to search for * @param string Search string * @param array Namespace(s) to exclude results from * * @return array List of matching kolab_storage_folder objects */ public static function search_folders($type, $query, $exclude_ns = array()) { if (!self::setup()) { return array(); } $folders = array(); $query = str_replace('*', '', $query); // find unsubscribed IMAP folders of the given type foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) { // FIXME: only consider the last part of the folder path for searching? $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP')); if (($query == '' || strpos($realname, $query) !== false) && !self::folder_is_subscribed($foldername, true) && !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns) ) { $folders[] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]); } } return $folders; } /** * Sort the given list of kolab folders by namespace/name * * @param array List of kolab_storage_folder objects * @return array Sorted list of folders */ public static function sort_folders($folders) { $pad = ' '; $out = array(); $nsnames = array('personal' => array(), 'shared' => array(), 'other' => array()); foreach ($folders as $folder) { $_folders[$folder->name] = $folder; $ns = $folder->get_namespace(); $nsnames[$ns][$folder->name] = strtolower(html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode » } // $folders is a result of get_folders() we can assume folders were already sorted foreach (array_keys($nsnames) as $ns) { asort($nsnames[$ns], SORT_LOCALE_STRING); foreach (array_keys($nsnames[$ns]) as $utf7name) { $out[] = $_folders[$utf7name]; } } return $out; } /** * Check the folder tree and add the missing parents as virtual folders * * @param array $folders Folders list * @param object $tree Reference to the root node of the folder tree * * @return array Flat folders list */ public static function folder_hierarchy($folders, &$tree = null) { $_folders = array(); $delim = self::$imap->get_hierarchy_delimiter(); $other_ns = rtrim(self::namespace_root('other'), $delim); $tree = new kolab_storage_folder_virtual('', '', ''); // create tree root $refs = array('' => $tree); foreach ($folders as $idx => $folder) { $path = explode($delim, $folder->name); array_pop($path); $folder->parent = join($delim, $path); $folder->children = array(); // reset list // skip top folders or ones with a custom displayname if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) { $tree->children[] = $folder; } else { $parents = array(); $depth = $folder->get_namespace() == 'personal' ? 1 : 2; while (count($path) >= $depth && ($parent = join($delim, $path))) { array_pop($path); $parent_parent = join($delim, $path); if (!$refs[$parent]) { if ($folder->type && self::folder_type($parent) == $folder->type) { $refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type); $refs[$parent]->parent = $parent_parent; } else if ($parent_parent == $other_ns) { $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent); } else { $name = kolab_storage::object_name($parent); $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent); } $parents[] = $refs[$parent]; } } if (!empty($parents)) { $parents = array_reverse($parents); foreach ($parents as $parent) { $parent_node = $refs[$parent->parent] ?: $tree; $parent_node->children[] = $parent; $_folders[] = $parent; } } $parent_node = $refs[$folder->parent] ?: $tree; $parent_node->children[] = $folder; } $refs[$folder->name] = $folder; $_folders[] = $folder; unset($folders[$idx]); } return $_folders; } /** * Returns folder types indexed by folder name * * @param string $prefix Folder prefix (Default '*' for all folders) * * @return array|bool List of folders, False on failure */ public static function folders_typedata($prefix = '*') { if (!self::setup()) { return false; } // return cached result if (is_array(self::$typedata[$prefix])) { return self::$typedata[$prefix]; } $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE); // fetch metadata from *some* folders only if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) { $delimiter = self::$imap->get_hierarchy_delimiter(); $folderdata = $blacklist = array(); foreach ((array)$skip_ns as $ns) { if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) { $blacklist[] = $ns_root; } } foreach (array('personal','other','shared') as $ns) { if (!in_array($ns, (array)$skip_ns)) { $ns_root = rtrim(self::namespace_root($ns), $delimiter); // list top-level folders and their childs one by one // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would if ($ns_root == '') { foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) { if (!in_array($folder, $blacklist)) { $folderdata[$folder] = $metadata; $opts = self::$imap->folder_attributes($folder); if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) { $folderdata += $data; } } } } else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) { $folderdata += $data; } } } } else { $folderdata = self::$imap->get_metadata($prefix, $type_keys); } if (!is_array($folderdata)) { return false; } // keep list in memory self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata); return self::$typedata[$prefix]; } /** * Callback for array_map to select the correct annotation value */ public static function folder_select_metadata($types) { if (!empty($types[self::CTYPE_KEY_PRIVATE])) { return $types[self::CTYPE_KEY_PRIVATE]; } else if (!empty($types[self::CTYPE_KEY])) { list($ctype, ) = explode('.', $types[self::CTYPE_KEY]); return $ctype; } return null; } /** * Returns type of IMAP folder * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder type */ public static function folder_type($folder) { self::setup(); // return in-memory cached result foreach (self::$typedata as $typedata) { if (array_key_exists($folder, $typedata)) { return $typedata[$folder]; } } $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE)); if (!is_array($metadata)) { return null; } if (!empty($metadata[$folder])) { return self::folder_select_metadata($metadata[$folder]); } return 'mail'; } /** * Sets folder content-type. * * @param string $folder Folder name * @param string $type Content type * * @return boolean True on success */ public static function set_folder_type($folder, $type='mail') { self::setup(); list($ctype, $subtype) = explode('.', $type); $success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null)); if (!$success) // fallback: only set private annotation $success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type)); return $success; } /** * Check subscription status of this folder * * @param string $folder Folder name * @param boolean $temp Include temporary/session subscriptions * * @return boolean True if subscribed, false if not */ public static function folder_is_subscribed($folder, $temp = false) { if (self::$subscriptions === null) { self::setup(); self::$with_tempsubs = false; self::$subscriptions = self::$imap->list_folders_subscribed(); self::$with_tempsubs = true; } return in_array($folder, self::$subscriptions) || ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders'])); } /** * Change subscription status of this folder * * @param string $folder Folder name * @param boolean $temp Only subscribe temporarily for the current session * * @return True on success, false on error */ public static function folder_subscribe($folder, $temp = false) { self::setup(); // temporary/session subscription if ($temp) { if (self::folder_is_subscribed($folder)) { return true; } else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) { $_SESSION['kolab_subscribed_folders'][] = $folder; return true; } } else if (self::$imap->subscribe($folder)) { self::$subscriptions = null; return true; } return false; } /** * Change subscription status of this folder * * @param string $folder Folder name * @param boolean $temp Only remove temporary subscription * * @return True on success, false on error */ public static function folder_unsubscribe($folder, $temp = false) { self::setup(); // temporary/session subscription if ($temp) { if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) { unset($_SESSION['kolab_subscribed_folders'][$i]); } return true; } else if (self::$imap->unsubscribe($folder)) { self::$subscriptions = null; return true; } return false; } /** * Check activation status of this folder * * @param string $folder Folder name * * @return boolean True if active, false if not */ public static function folder_is_active($folder) { $active_folders = self::get_states(); return in_array($folder, $active_folders); } /** * Change activation status of this folder * * @param string $folder Folder name * * @return True on success, false on error */ public static function folder_activate($folder) { // activation implies temporary subscription self::folder_subscribe($folder, true); return self::set_state($folder, true); } /** * Change activation status of this folder * * @param string $folder Folder name * * @return True on success, false on error */ public static function folder_deactivate($folder) { // remove from temp subscriptions, really? self::folder_unsubscribe($folder, true); return self::set_state($folder, false); } /** * Return list of active folders */ private static function get_states() { if (self::$states !== null) { return self::$states; } $rcube = rcube::get_instance(); $folders = $rcube->config->get('kolab_active_folders'); if ($folders !== null) { self::$states = !empty($folders) ? explode('**', $folders) : array(); } // for backward-compatibility copy server-side subscriptions to activation states else { self::setup(); if (self::$subscriptions === null) { self::$with_tempsubs = false; self::$subscriptions = self::$imap->list_folders_subscribed(); self::$with_tempsubs = true; } self::$states = self::$subscriptions; $folders = implode(self::$states, '**'); $rcube->user->save_prefs(array('kolab_active_folders' => $folders)); } return self::$states; } /** * Update list of active folders */ private static function set_state($folder, $state) { self::get_states(); // update in-memory list $idx = array_search($folder, self::$states); if ($state && $idx === false) { self::$states[] = $folder; } else if (!$state && $idx !== false) { unset(self::$states[$idx]); } // update user preferences $folders = implode(self::$states, '**'); $rcube = rcube::get_instance(); return $rcube->user->save_prefs(array('kolab_active_folders' => $folders)); } /** * Creates default folder of specified type * To be run when none of subscribed folders (of specified type) is found * * @param string $type Folder type * @param string $props Folder properties (color, etc) * * @return string Folder name */ public static function create_default_folder($type, $props = array()) { if (!self::setup()) { return; } $folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE)); // from kolab_folders config $folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default'; $default_name = self::$config->get('kolab_folders_' . $folder_type); $folder_type = str_replace('_', '.', $folder_type); // check if we have any folder in personal namespace // folder(s) may exist but not subscribed foreach ((array)$folders as $f => $data) { if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) { $folder = $f; break; } } if (!$folder) { if (!$default_name) { $default_name = self::$default_folders[$type]; } if (!$default_name) { return; } $folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP'); $prefix = self::$imap->get_namespace('prefix'); // add personal namespace prefix if needed if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') { $folder = $prefix . $folder; } if (!self::$imap->folder_exists($folder)) { if (!self::$imap->create_folder($folder)) { return; } } self::set_folder_type($folder, $folder_type); } self::folder_subscribe($folder); if ($props['active']) { self::set_state($folder, true); } if (!empty($props)) { self::set_folder_props($folder, $props); } return $folder; } /** * Sets folder metadata properties * * @param string $folder Folder name * @param array $prop Folder properties */ public static function set_folder_props($folder, &$prop) { if (!self::setup()) { return; } // TODO: also save 'showalarams' and other properties here $ns = self::$imap->folder_namespace($folder); $supported = array( 'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE), 'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE), ); foreach ($supported as $key => $metakeys) { if (array_key_exists($key, $prop)) { $meta_saved = false; if ($ns == 'personal') // save in shared namespace for personal folders $meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key])); if (!$meta_saved) // try in private namespace $meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key])); if ($meta_saved) unset($prop[$key]); // unsetting will prevent fallback to local user prefs } } } /** * * @param mixed $query Search value (or array of field => value pairs) * @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*) * @param array $required List of fields that shall ot be empty * @param int $limit Maximum number of records * @param int $count Returns the number of records found * * @return array List or false on error */ public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0) { $query = str_replace('*', '', $query); // requires a working LDAP setup if (!self::ldap() || strlen($query) == 0) { return array(); } // search users using the configured attributes $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count); // exclude myself if ($_SESSION['kolab_dn']) { unset($results[$_SESSION['kolab_dn']]); } // resolve to IMAP folder name $root = self::namespace_root('other'); $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); array_walk($results, function(&$user, $dn) use ($root, $user_attrib) { list($localpart, ) = explode('@', $user[$user_attrib]); $user['kolabtargetfolder'] = $root . $localpart; }); return $results; } /** * Returns a list of IMAP folders shared by the given user * * @param array User entry from LDAP * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration) * @param int 1 - subscribed folders only, 0 - all folders, 2 - all non-active * @param array Will be filled with folder-types data * * @return array List of folders */ public static function list_user_folders($user, $type, $subscribed = 0, &$folderdata = array()) { self::setup(); $folders = array(); // use localpart of user attribute as root for folder listing $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail')); if (!empty($user[$user_attrib])) { list($mbox) = explode('@', $user[$user_attrib]); $delimiter = self::$imap->get_hierarchy_delimiter(); $other_ns = self::namespace_root('other'); $prefix = $other_ns . $mbox . $delimiter; $subscribed = (int) $subscribed; $subs = $subscribed < 2 ? (bool) $subscribed : false; $folders = self::list_folders($prefix, '*', $type, $subs, $folderdata); if ($subscribed === 2 && !empty($folders)) { $active = self::get_states(); if (!empty($active)) { $folders = array_diff($folders, $active); } } } return $folders; } /** * Get a list of (virtual) top-level folders from the other users namespace * * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration) * @param boolean Enable to return subscribed folders only (null to use configured subscription mode) * * @return array List of kolab_storage_folder_user objects */ public static function get_user_folders($type, $subscribed) { $folders = $folderdata = array(); if (self::setup()) { $delimiter = self::$imap->get_hierarchy_delimiter(); $other_ns = rtrim(self::namespace_root('other'), $delimiter); $path_len = count(explode($delimiter, $other_ns)); foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) { if ($foldername == 'INBOX') // skip INBOX which is added by default continue; $path = explode($delimiter, $foldername); // compare folder type if a subfolder is listed if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) { continue; } // truncate folder path to top-level folders of the 'other' namespace $foldername = join($delimiter, array_slice($path, 0, $path_len + 1)); if (!$folders[$foldername]) { $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns); } } // for every (subscribed) user folder, list all (unsubscribed) subfolders foreach ($folders as $userfolder) { foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) { if (!$folders[$foldername]) { $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]); $userfolder->children[] = $folders[$foldername]; } } } } return $folders; } /** * Handler for user_delete plugin hooks * * Remove all cache data from the local database related to the given user. */ public static function delete_user_folders($args) { $db = rcmail::get_instance()->get_dbh(); $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%'; $db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix); } /** * Get folder METADATA for all supported keys * Do this in one go for better caching performance */ public static function folder_metadata($folder) { if (self::setup()) { $keys = array( // For better performance we skip displayname here, see (self::custom_displayname()) // self::NAME_KEY_PRIVATE, // self::NAME_KEY_SHARED, self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE, self::COLOR_KEY_PRIVATE, self::COLOR_KEY_SHARED, self::UID_KEY_SHARED, self::UID_KEY_CYRUS, ); $metadata = self::$imap->get_metadata($folder, $keys); return $metadata[$folder]; } } } diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php index 5f8d3fe5..3a73d982 100644 --- a/plugins/libkolab/lib/kolab_storage_cache.php +++ b/plugins/libkolab/lib/kolab_storage_cache.php @@ -1,1209 +1,1237 @@ * * Copyright (C) 2012-2013, Kolab Systems AG * * 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 . */ class kolab_storage_cache { const DB_DATE_FORMAT = 'Y-m-d H:i:s'; const MAX_RECORDS = 500; public $sync_complete = false; protected $db; protected $imap; protected $folder; protected $uid2msg; protected $objects; protected $metadata = array(); protected $folder_id; protected $resource_uri; protected $enabled = true; protected $synched = false; protected $synclock = false; protected $ready = false; protected $cache_table; protected $cache_refresh = 3600; protected $folders_table; protected $max_sql_packet; protected $max_sync_lock_time = 600; protected $binary_items = array(); protected $extra_cols = array(); protected $order_by = null; protected $limit = null; protected $error = 0; protected $server_timezone; /** * Factory constructor */ public static function factory(kolab_storage_folder $storage_folder) { $subclass = 'kolab_storage_cache_' . $storage_folder->type; if (class_exists($subclass)) { return new $subclass($storage_folder); } else { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'" ), true); return new kolab_storage_cache($storage_folder); } } /** * Default constructor */ public function __construct(kolab_storage_folder $storage_folder = null) { $rcmail = rcube::get_instance(); $this->db = $rcmail->get_dbh(); $this->imap = $rcmail->get_storage(); $this->enabled = $rcmail->config->get('kolab_cache', false); $this->folders_table = $this->db->table_name('kolab_folders'); $this->cache_refresh = get_offset_sec($rcmail->config->get('kolab_cache_refresh', '12h')); $this->server_timezone = new DateTimeZone(date_default_timezone_get()); if ($this->enabled) { // always read folder cache and lock state from DB master $this->db->set_table_dsn('kolab_folders', 'w'); // remove sync-lock on script termination $rcmail->add_shutdown_function(array($this, '_sync_unlock')); } if ($storage_folder) $this->set_folder($storage_folder); } /** * Direct access to cache by folder_id * (only for internal use) */ public function select_by_id($folder_id) { $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id)); if ($sql_arr) { $this->metadata = $sql_arr; $this->folder_id = $sql_arr['folder_id']; $this->folder = new StdClass; $this->folder->type = $sql_arr['type']; $this->resource_uri = $sql_arr['resource']; $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']); $this->ready = true; } } /** * Connect cache with a storage folder * * @param kolab_storage_folder The storage folder instance to connect with */ public function set_folder(kolab_storage_folder $storage_folder) { $this->folder = $storage_folder; if (empty($this->folder->name) || !$this->folder->valid) { $this->ready = false; return; } // compose fully qualified ressource uri for this instance $this->resource_uri = $this->folder->get_resource_uri(); $this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type); $this->ready = $this->enabled && !empty($this->folder->type); $this->folder_id = null; } /** * Returns true if this cache supports query by type */ public function has_type_col() { return in_array('type', $this->extra_cols); } /** * Getter for the numeric ID used in cache tables */ public function get_folder_id() { $this->_read_folder_data(); return $this->folder_id; } /** * Returns code of last error * * @return int Error code */ public function get_error() { return $this->error; } /** * Synchronize local cache data with remote */ public function synchronize() { // only sync once per request cycle if ($this->synched) return; // increase time limit @set_time_limit($this->max_sync_lock_time - 60); // get effective time limit we have for synchronization (~70% of the execution time) $time_limit = ini_get('max_execution_time') * 0.7; $sync_start = time(); // assume sync will be completed $this->sync_complete = true; if (!$this->ready) { // kolab cache is disabled, synchronize IMAP mailbox cache only + $this->imap_mode(true); $this->imap->folder_sync($this->folder->name); + $this->imap_mode(false); } else { // read cached folder metadata $this->_read_folder_data(); // check cache status ($this->metadata is set in _read_folder_data()) if ( empty($this->metadata['ctag']) || empty($this->metadata['changed']) || $this->metadata['objectcount'] === null || $this->metadata['changed'] < date(self::DB_DATE_FORMAT, time() - $this->cache_refresh) || $this->metadata['ctag'] != $this->folder->get_ctag() || intval($this->metadata['objectcount']) !== $this->count() ) { // lock synchronization for this folder or wait if locked $this->_sync_lock(); // disable messages cache if configured to do so - $this->bypass(true); + $this->imap_mode(true); // synchronize IMAP mailbox cache $this->imap->folder_sync($this->folder->name); // compare IMAP index with object cache index $imap_index = $this->imap->index($this->folder->name, null, null, true, true); + $this->imap_mode(false); + // determine objects to fetch or to invalidate if (!$imap_index->is_error()) { $imap_index = $imap_index->get(); // read cache index $sql_result = $this->db->query( "SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?", $this->folder_id ); $old_index = array(); while ($sql_arr = $this->db->fetch_assoc($sql_result)) { $old_index[] = $sql_arr['msguid']; } // fetch new objects from imap $i = 0; foreach (array_diff($imap_index, $old_index) as $msguid) { if ($object = $this->folder->read_object($msguid, '*')) { $this->_extended_insert($msguid, $object); // check time limit and abort sync if running too long if (++$i % 50 == 0 && time() - $sync_start > $time_limit) { $this->sync_complete = false; break; } } } $this->_extended_insert(0, null); // delete invalid entries from local DB $del_index = array_diff($old_index, $imap_index); if (!empty($del_index)) { $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index)); $this->db->query( "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)", $this->folder_id ); } // update ctag value (will be written to database in _sync_unlock()) if ($this->sync_complete) { $this->metadata['ctag'] = $this->folder->get_ctag(); $this->metadata['changed'] = date(self::DB_DATE_FORMAT, time()); // remember the number of cache entries linked to this folder $this->metadata['objectcount'] = $this->count(); } } - $this->bypass(false); - // remove lock $this->_sync_unlock(); } } $this->check_error(); $this->synched = time(); } /** * Read a single entry from cache or from IMAP directly * * @param string Related IMAP message UID * @param string Object type to read * @param string IMAP folder name the entry relates to * @param array Hash array with object properties or null if not found */ public function get($msguid, $type = null, $foldername = null) { // delegate to another cache instance if ($foldername && $foldername != $this->folder->name) { $success = false; if ($targetfolder = kolab_storage::get_folder($foldername)) { $success = $targetfolder->cache->get($msguid, $type); $this->error = $targetfolder->cache->get_error(); } return $success; } // load object if not in memory if (!isset($this->objects[$msguid])) { if ($this->ready) { $this->_read_folder_data(); $sql_result = $this->db->query( "SELECT * FROM `{$this->cache_table}` ". "WHERE `folder_id` = ? AND `msguid` = ?", $this->folder_id, $msguid ); if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827) } } // fetch from IMAP if not present in cache if (empty($this->objects[$msguid])) { if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) { $this->objects = array($msguid => $object); $this->set($msguid, $object); } } } $this->check_error(); return $this->objects[$msguid]; } /** * Getter for a single Kolab object identified by its UID * * @param string $uid Object UID * * @return array The Kolab object represented as hash array */ public function get_by_uid($uid) { $old_order_by = $this->order_by; $old_limit = $this->limit; // set order to make sure we get most recent object version // set limit to skip count query $this->order_by = '`msguid` DESC'; $this->limit = array(1, 0); $list = $this->select(array(array('uid', '=', $uid))); // set the order/limit back to defined value $this->order_by = $old_order_by; $this->limit = $old_limit; if (!empty($list) && !empty($list[0])) { return $list[0]; } } /** * Insert/Update a cache entry * * @param string Related IMAP message UID * @param mixed Hash array with object properties to save or false to delete the cache entry * @param string IMAP folder name the entry relates to */ public function set($msguid, $object, $foldername = null) { if (!$msguid) { return; } // delegate to another cache instance if ($foldername && $foldername != $this->folder->name) { if ($targetfolder = kolab_storage::get_folder($foldername)) { $targetfolder->cache->set($msguid, $object); $this->error = $targetfolder->cache->get_error(); } return; } // remove old entry if ($this->ready) { $this->_read_folder_data(); $this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?", $this->folder_id, $msguid); } if ($object) { // insert new object data... $this->save($msguid, $object); } else { // ...or set in-memory cache to false $this->objects[$msguid] = $object; } $this->check_error(); } /** * Insert (or update) a cache entry * * @param int Related IMAP message UID * @param mixed Hash array with object properties to save or false to delete the cache entry * @param int Optional old message UID (for update) */ public function save($msguid, $object, $olduid = null) { // write to cache if ($this->ready) { $this->_read_folder_data(); $sql_data = $this->_serialize($object); $sql_data['folder_id'] = $this->folder_id; $sql_data['msguid'] = $msguid; $sql_data['uid'] = $object['uid']; $args = array(); $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words'); $cols = array_merge($cols, $this->extra_cols); foreach ($cols as $idx => $col) { $cols[$idx] = $this->db->quote_identifier($col); $args[] = $sql_data[$col]; } if ($olduid) { foreach ($cols as $idx => $col) { $cols[$idx] = "$col = ?"; } $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols) . " WHERE `folder_id` = ? AND `msguid` = ?"; $args[] = $this->folder_id; $args[] = $olduid; } else { $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols) . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")"; } $result = $this->db->query($query, $args); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } } // keep a copy in memory for fast access $this->objects = array($msguid => $object); $this->uid2msg = array($object['uid'] => $msguid); $this->check_error(); } /** * Move an existing cache entry to a new resource * * @param string Entry's IMAP message UID * @param string Entry's Object UID * @param kolab_storage_folder Target storage folder instance * @param string Target entry's IMAP message UID */ public function move($msguid, $uid, $target, $new_msguid = null) { if ($this->ready && $target) { // clear cached uid mapping and force new lookup unset($target->cache->uid2msg[$uid]); // resolve new message UID in target folder if (!$new_msguid) { $new_msguid = $target->cache->uid2msguid($uid); } if ($new_msguid) { $this->_read_folder_data(); $this->db->query( "UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ". "WHERE `folder_id` = ? AND `msguid` = ?", $target->cache->get_folder_id(), $new_msguid, $this->folder_id, $msguid ); $result = $this->db->affected_rows(); } } if (empty($result)) { // just clear cache entry $this->set($msguid, false); } unset($this->uid2msg[$uid]); $this->check_error(); } /** * Remove all objects from local cache */ public function purge() { if (!$this->ready) { return true; } $this->_read_folder_data(); $result = $this->db->query( "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?", $this->folder_id ); return $this->db->affected_rows($result); } /** * Update resource URI for existing cache entries * * @param string Target IMAP folder to move it to */ public function rename($new_folder) { if (!$this->ready) { return; } if ($target = kolab_storage::get_folder($new_folder)) { // resolve new message UID in target folder $this->db->query( "UPDATE `{$this->folders_table}` SET `resource` = ? ". "WHERE `resource` = ?", $target->get_resource_uri(), $this->resource_uri ); $this->check_error(); } else { $this->error = kolab_storage::ERROR_IMAP_CONN; } } /** * Select Kolab objects filtered by the given query * * @param array Pseudo-SQL query as list of filter parameter triplets * triplet: array('', '', '') * @param boolean Set true to only return UIDs instead of complete objects * @return array List of Kolab data objects (each represented as hash array) or UIDs */ public function select($query = array(), $uids = false) { $result = $uids ? array() : new kolab_storage_dataset($this); // read from local cache DB (assume it to be synchronized) if ($this->ready) { $this->_read_folder_data(); // fetch full object data on one query if a small result set is expected $fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS; // skip SELECT if we know it will return nothing if ($count === 0) { return $result; } $sql_query = "SELECT " . ($fetchall ? '*' : "`msguid` AS `_msguid`, `uid`") . " FROM `{$this->cache_table}` WHERE `folder_id` = ?" . $this->_sql_where($query) . (!empty($this->order_by) ? " ORDER BY " . $this->order_by : ''); $sql_result = $this->limit ? $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) : $this->db->query($sql_query, $this->folder_id); if ($this->db->is_error($sql_result)) { if ($uids) { return null; } $result->set_error(true); return $result; } while ($sql_arr = $this->db->fetch_assoc($sql_result)) { if ($uids) { $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid']; $result[] = $sql_arr['uid']; } else if ($fetchall && ($object = $this->_unserialize($sql_arr))) { $result[] = $object; } else if (!$fetchall) { // only add msguid to dataset index $result[] = $sql_arr; } } } // use IMAP else { $filter = $this->_query2assoc($query); + $this->imap_mode(true); + if ($filter['type']) { $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type']; $index = $this->imap->search_once($this->folder->name, $search); } else { $index = $this->imap->index($this->folder->name, null, null, true, true); } + $this->imap_mode(false); + if ($index->is_error()) { $this->check_error(); if ($uids) { return null; } $result->set_error(true); return $result; } $index = $index->get(); $result = $uids ? $index : $this->_fetch($index, $filter['type']); // TODO: post-filter result according to query } // We don't want to cache big results in-memory, however // if we select only one object here, there's a big chance we will need it later if (!$uids && count($result) == 1) { if ($msguid = $result[0]['_msguid']) { $this->uid2msg[$result[0]['uid']] = $msguid; $this->objects = array($msguid => $result[0]); } } $this->check_error(); return $result; } /** * Get number of objects mathing the given query * * @param array $query Pseudo-SQL query as list of filter parameter triplets * @return integer The number of objects of the given type */ public function count($query = array()) { // read from local cache DB (assume it to be synchronized) if ($this->ready) { $this->_read_folder_data(); $sql_result = $this->db->query( "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ". "WHERE `folder_id` = ?" . $this->_sql_where($query), $this->folder_id ); if ($this->db->is_error($sql_result)) { return null; } $sql_arr = $this->db->fetch_assoc($sql_result); $count = intval($sql_arr['numrows']); } // use IMAP else { $filter = $this->_query2assoc($query); + $this->imap_mode(true); + if ($filter['type']) { $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type']; $index = $this->imap->search_once($this->folder->name, $search); } else { $index = $this->imap->index($this->folder->name, null, null, true, true); } + $this->imap_mode(false); + if ($index->is_error()) { $this->check_error(); return null; } // TODO: post-filter result according to query $count = $index->count(); } $this->check_error(); return $count; } /** * Define ORDER BY clause for cache queries */ public function set_order_by($sortcols) { if (!empty($sortcols)) { $sortcols = array_map(function($v) { list($column, $order) = explode(' ', $v, 2); return "`$column`" . ($order ? " $order" : ''); }, (array) $sortcols); $this->order_by = join(', ', $sortcols); } else { $this->order_by = null; } } /** * Define LIMIT clause for cache queries */ public function set_limit($length, $offset = 0) { $this->limit = array($length, $offset); } /** * Helper method to compose a valid SQL query from pseudo filter triplets */ protected function _sql_where($query) { $sql_where = ''; foreach ((array) $query as $param) { if (is_array($param[0])) { $subq = array(); foreach ($param[0] as $q) { $subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q))); } if (!empty($subq)) { $sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')'; } continue; } else if ($param[1] == '=' && is_array($param[2])) { $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')'; $param[1] = 'IN'; } else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') { $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : ''; $param[1] = $not . 'LIKE'; $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%'); } else if ($param[0] == 'tags') { $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE'; $qvalue = $this->db->quote('% '.$param[2].' %'); } else { $qvalue = $this->db->quote($param[2]); } $sql_where .= sprintf(' AND %s %s %s', $this->db->quote_identifier($param[0]), $param[1], $qvalue ); } return $sql_where; } /** * Helper method to convert the given pseudo-query triplets into * an associative filter array with 'equals' values only */ protected function _query2assoc($query) { // extract object type from query parameter $filter = array(); foreach ($query as $param) { if ($param[1] == '=') $filter[$param[0]] = $param[2]; } return $filter; } /** * Fetch messages from IMAP * * @param array List of message UIDs to fetch * @param string Requested object type or * for all * @param string IMAP folder to read from * @return array List of parsed Kolab objects */ protected function _fetch($index, $type = null, $folder = null) { $results = new kolab_storage_dataset($this); foreach ((array)$index as $msguid) { if ($object = $this->folder->read_object($msguid, $type, $folder)) { $results[] = $object; $this->set($msguid, $object); } } return $results; } /** * Helper method to convert the given Kolab object into a dataset to be written to cache */ protected function _serialize($object) { $sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => ''); if ($object['changed']) { $sql_data['changed'] = date(self::DB_DATE_FORMAT, is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']); } if ($object['_formatobj']) { $sql_data['xml'] = preg_replace('!()[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write(3.0)); $sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' '; } // extract object data $data = array(); foreach ($object as $key => $val) { // skip empty properties if ($val === "" || $val === null) { continue; } // mark binary data to be extracted from xml on unserialize() if (isset($this->binary_items[$key])) { $data[$key] = true; } else if ($key[0] != '_') { $data[$key] = $val; } else if ($key == '_attachments') { foreach ($val as $k => $att) { unset($att['content'], $att['path']); if ($att['id']) $data[$key][$k] = $att; } } } // use base64 encoding (Bug #1912, #2662) $sql_data['data'] = base64_encode(serialize($data)); return $sql_data; } /** * Helper method to turn stored cache data into a valid storage object */ protected function _unserialize($sql_arr) { // check if data is a base64-encoded string, for backward compat. if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) { $sql_arr['data'] = base64_decode($sql_arr['data']); } $object = unserialize($sql_arr['data']); // de-serialization failed if ($object === false) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object." ), true); return null; } // decode binary properties foreach ($this->binary_items as $key => $regexp) { if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) { $object[$key] = base64_decode($m[1]); } } $object_type = $sql_arr['type'] ?: $this->folder->type; $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type; // add meta data $object['_type'] = $object_type; $object['_msguid'] = $sql_arr['msguid']; $object['_mailbox'] = $this->folder->name; $object['_size'] = strlen($sql_arr['xml']); $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']); return $object; } /** * Write records into cache using extended inserts to reduce the number of queries to be executed * * @param int Message UID. Set 0 to commit buffered inserts * @param array Kolab object to cache */ protected function _extended_insert($msguid, $object) { static $buffer = ''; $line = ''; if ($object) { $sql_data = $this->_serialize($object); // Skip multifolder insert for Oracle, we can't put long data inline if ($this->db->db_provider == 'oracle') { $extra_cols = ''; if ($this->extra_cols) { $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols); $extra_cols = ', ' . join(', ', $extra_cols); $extra_args = str_repeat(', ?', count($this->extra_cols)); } $params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'], $sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']); foreach ($this->extra_cols as $col) { $params[] = $sql_data[$col]; } $result = $this->db->query( "INSERT INTO `{$this->cache_table}` " . " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)" . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)", $params ); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } return; } $values = array( $this->db->quote($this->folder_id), $this->db->quote($msguid), $this->db->quote($object['uid']), $this->db->now(), $this->db->quote($sql_data['changed']), $this->db->quote($sql_data['data']), $this->db->quote($sql_data['xml']), $this->db->quote($sql_data['tags']), $this->db->quote($sql_data['words']), ); foreach ($this->extra_cols as $col) { $values[] = $this->db->quote($sql_data[$col]); } $line = '(' . join(',', $values) . ')'; } if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) { $extra_cols = ''; if ($this->extra_cols) { $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols); $extra_cols = ', ' . join(', ', $extra_cols); } $result = $this->db->query( "INSERT INTO `{$this->cache_table}` ". " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)". " VALUES $buffer" ); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } $buffer = ''; } $buffer .= ($buffer ? ',' : '') . $line; } /** * Returns max_allowed_packet from mysql config */ protected function max_sql_packet() { if (!$this->max_sql_packet) { // mysql limit or max 4 MB $value = $this->db->get_variable('max_allowed_packet', 1048500); $this->max_sql_packet = min($value, 4*1024*1024) - 2000; } return $this->max_sql_packet; } /** * Read this folder's ID and cache metadata */ protected function _read_folder_data() { // already done if (!empty($this->folder_id) || !$this->ready) return; $sql_arr = $this->db->fetch_assoc($this->db->query( "SELECT `folder_id`, `synclock`, `ctag`, `changed`, `objectcount`" . " FROM `{$this->folders_table}` WHERE `resource` = ?", $this->resource_uri )); if ($sql_arr) { $this->metadata = $sql_arr; $this->folder_id = $sql_arr['folder_id']; } else { $this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)" . " VALUES (?, ?)", $this->resource_uri, $this->folder->type); $this->folder_id = $this->db->insert_id('kolab_folders'); $this->metadata = array(); } } /** * Check lock record for this folder and wait if locked or set lock */ protected function _sync_lock() { if (!$this->ready) return; $this->_read_folder_data(); // abort if database is not set-up if ($this->db->is_error()) { $this->check_error(); $this->ready = false; return; } $read_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?"; $write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?"; // wait if locked (expire locks after 10 minutes) ... // ... or if setting lock fails (another process meanwhile set it) while ( (intval($this->metadata['synclock']) + $this->max_sync_lock_time > time()) || (($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock']))) && !($affected = $this->db->affected_rows($res))) ) { usleep(500000); $this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id)); } $this->synclock = $affected > 0; } /** * Remove lock for this folder */ public function _sync_unlock() { if (!$this->ready || !$this->synclock) return; $this->db->query( "UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ?, `changed` = ?, `objectcount` = ? WHERE `folder_id` = ?", $this->metadata['ctag'], $this->metadata['changed'], $this->metadata['objectcount'], $this->folder_id ); $this->synclock = false; } /** * Check IMAP connection error state */ protected function check_error() { if (($err_code = $this->imap->get_error_code()) < 0) { $this->error = kolab_storage::ERROR_IMAP_CONN; if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) { $this->error = kolab_storage::ERROR_NO_PERMISSION; } } else if ($this->db->is_error()) { $this->error = kolab_storage::ERROR_CACHE_DB; } } /** * Resolve an object UID into an IMAP message UID * * @param string Kolab object UID * @param boolean Include deleted objects * @return int The resolved IMAP message UID */ public function uid2msguid($uid, $deleted = false) { // query local database if available if (!isset($this->uid2msg[$uid]) && $this->ready) { $this->_read_folder_data(); $sql_result = $this->db->query( "SELECT `msguid` FROM `{$this->cache_table}` ". "WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC", $this->folder_id, $uid ); if ($sql_arr = $this->db->fetch_assoc($sql_result)) { $this->uid2msg[$uid] = $sql_arr['msguid']; } } if (!isset($this->uid2msg[$uid])) { // use IMAP SEARCH to get the right message $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . rcube_imap_generic::escape($uid)); $results = $index->get(); $this->uid2msg[$uid] = end($results); } return $this->uid2msg[$uid]; } /** * Getter for protected member variables */ public function __get($name) { if ($name == 'folder_id') { $this->_read_folder_data(); } return $this->$name; } /** - * Bypass Roundcube messages cache. - * Roundcube cache duplicates information already stored in kolab_cache. + * Set Roundcube storage options and bypass messages cache. * - * @param bool $disable True disables, False enables messages cache + * We use skip_deleted and threading settings specific to Kolab, + * we have to change these global settings only temporarily. + * Roundcube cache duplicates information already stored in kolab_cache, + * that's why we can disable it for better performance. + * + * @param bool $force True to start Kolab mode, False to stop it. */ - public function bypass($disable = false) + public function imap_mode($force = false) { + // remember current IMAP settings + if ($force) { + $this->imap_options = array( + 'skip_deleted' => $this->imap->get_option('skip_deleted'), + 'threading' => $this->imap->get_threading(), + ); + } + + // re-set IMAP settings + $this->imap->set_threading($force ? false : $this->imap_options['threading']); + $this->imap->set_options(array( + 'skip_deleted' => $force ? true : $this->imap_options['skip_deleted'], + )); + // if kolab cache is disabled do nothing if (!$this->enabled) { return; } static $messages_cache, $cache_bypass; if ($messages_cache === null) { $rcmail = rcube::get_instance(); $messages_cache = (bool) $rcmail->config->get('messages_cache'); $cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass'); } if ($messages_cache) { // handle recurrent (multilevel) bypass() calls - if ($disable) { + if ($force) { $this->cache_bypassed += 1; if ($this->cache_bypassed > 1) { return; } } else { $this->cache_bypassed -= 1; if ($this->cache_bypassed > 0) { return; } } switch ($cache_bypass) { case 2: // Disable messages cache completely - $this->imap->set_messages_caching(!$disable); + $this->imap->set_messages_caching(!$force); break; case 1: // We'll disable messages cache, but keep index cache. // Default mode is both (MODE_INDEX | MODE_MESSAGE) $mode = rcube_imap_cache::MODE_INDEX; - if (!$disable) { + if (!$force) { $mode |= rcube_imap_cache::MODE_MESSAGE; } $this->imap->set_messages_caching(true, $mode); } } } /** * Converts DateTime or unix timestamp into sql date format * using server timezone. */ protected function _convert_datetime($datetime) { if (is_object($datetime)) { $dt = clone $datetime; $dt->setTimeZone($this->server_timezone); return $dt->format(self::DB_DATE_FORMAT); } else if ($datetime) { return date(self::DB_DATE_FORMAT, $datetime); } } } diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index 6f338ba7..4018b7df 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -1,1142 +1,1141 @@ * @author Aleksander Machniak * * Copyright (C) 2012-2013, Kolab Systems AG * * 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 . */ class kolab_storage_folder extends kolab_storage_folder_api { /** * The kolab_storage_cache instance for caching operations * @var object */ public $cache; /** * Indicate validity status * @var boolean */ public $valid = false; protected $error = 0; protected $resource_uri; /** * Default constructor * * @param string The folder name/path * @param string Expected folder type */ function __construct($name, $type = null, $type_annotation = null) { parent::__construct($name); - $this->imap->set_options(array('skip_deleted' => true)); $this->set_folder($name, $type, $type_annotation); } /** * Set the IMAP folder this instance connects to * * @param string The folder name/path * @param string Expected folder type * @param string Optional folder type if known */ public function set_folder($name, $type = null, $type_annotation = null) { $this->name = $name; if (empty($type_annotation)) { $type_annotation = $this->get_type(); } $oldtype = $this->type; list($this->type, $suffix) = explode('.', $type_annotation); $this->default = $suffix == 'default'; $this->subtype = $this->default ? '' : $suffix; $this->id = kolab_storage::folder_id($name); $this->valid = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type); if (!$this->valid) { $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER; } // reset cached object properties $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null; // get a new cache instance if folder type changed if (!$this->cache || $this->type != $oldtype) $this->cache = kolab_storage_cache::factory($this); else $this->cache->set_folder($this); $this->imap->set_folder($this->name); } /** * Returns code of last error * * @return int Error code */ public function get_error() { return $this->error ?: $this->cache->get_error(); } /** * Check IMAP connection error state */ public function check_error() { if (($err_code = $this->imap->get_error_code()) < 0) { $this->error = kolab_storage::ERROR_IMAP_CONN; if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) { $this->error = kolab_storage::ERROR_NO_PERMISSION; } } return $this->error; } /** * Compose a unique resource URI for this IMAP folder */ public function get_resource_uri() { if (!empty($this->resource_uri)) { return $this->resource_uri; } // strip namespace prefix from folder name $ns = $this->get_namespace(); $nsdata = $this->imap->get_namespace($ns); if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) { $subpath = substr($this->name, strlen($nsdata[0][0])); if ($ns == 'other') { list($user, $suffix) = explode($nsdata[0][1], $subpath, 2); $subpath = $suffix; } } else { $subpath = $this->name; } // compose fully qualified ressource uri for this instance $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath; return $this->resource_uri; } /** * Helper method to extract folder UID metadata * * @return string Folder's UID */ public function get_uid() { // UID is defined in folder METADATA $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_CYRUS); $metadata = $this->get_metadata(); if ($metadata !== null) { foreach ($metakeys as $key) { if ($uid = $metadata[$key]) { return $uid; } } // generate a folder UID and set it to IMAP $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-'); if ($this->set_uid($uid)) { return $uid; } } $this->check_error(); // create hash from folder name if we can't write the UID metadata return md5($this->name . $this->get_owner()); } /** * Helper method to set an UID value to the given IMAP folder instance * * @param string Folder's UID * @return boolean True on succes, False on failure */ public function set_uid($uid) { $success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)); $this->check_error(); return $success; } /** * Compose a folder Etag identifier */ public function get_ctag() { $fdata = $this->get_imap_data(); $this->check_error(); return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']); } /** * Check activation status of this folder * * @return boolean True if enabled, false if not */ public function is_active() { return kolab_storage::folder_is_active($this->name); } /** * Change activation status of this folder * * @param boolean The desired subscription status: true = active, false = not active * * @return True on success, false on error */ public function activate($active) { return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name); } /** * Check subscription status of this folder * * @return boolean True if subscribed, false if not */ public function is_subscribed() { return kolab_storage::folder_is_subscribed($this->name); } /** * Change subscription status of this folder * * @param boolean The desired subscription status: true = subscribed, false = not subscribed * * @return True on success, false on error */ public function subscribe($subscribed) { return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name); } /** * Get number of objects stored in this folder * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * * @return integer The number of objects of the given type * @see self::select() */ public function count($query = null) { if (!$this->valid) { return 0; } // synchronize cache first $this->cache->synchronize(); return $this->cache->count($this->_prepare_query($query)); } /** * List Kolab objects matching the given query * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * * @return array List of Kolab data objects (each represented as hash array) * @deprecated Use select() */ public function get_objects($query = array()) { return $this->select($query); } /** * Select Kolab objects matching the given query * * @param mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * * @return array List of Kolab data objects (each represented as hash array) */ public function select($query = array()) { if (!$this->valid) { return array(); } // synchronize caches $this->cache->synchronize(); // fetch objects from cache return $this->cache->select($this->_prepare_query($query)); } /** * Getter for object UIDs only * * @param array Pseudo-SQL query as list of filter parameter triplets * @return array List of Kolab object UIDs */ public function get_uids($query = array()) { if (!$this->valid) { return array(); } // synchronize caches $this->cache->synchronize(); // fetch UIDs from cache return $this->cache->select($this->_prepare_query($query), true); } /** * Setter for ORDER BY and LIMIT parameters for cache queries * * @param array List of columns to order by * @param integer Limit result set to this length * @param integer Offset row */ public function set_order_and_limit($sortcols, $length = null, $offset = 0) { $this->cache->set_order_by($sortcols); if ($length !== null) { $this->cache->set_limit($length, $offset); } } /** * Helper method to sanitize query arguments */ private function _prepare_query($query) { // string equals type query // FIXME: should not be called this way! if (is_string($query)) { return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array(); } foreach ((array)$query as $i => $param) { if ($param[0] == 'type' && !$this->cache->has_type_col()) { unset($query[$i]); } else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) { if (is_object($param[2]) && is_a($param[2], 'DateTime')) $param[2] = $param[2]->format('U'); if (is_numeric($param[2])) $query[$i][2] = date('Y-m-d H:i:s', $param[2]); } } return $query; } /** * Getter for a single Kolab object identified by its UID * * @param string $uid Object UID * * @return array The Kolab object represented as hash array */ public function get_object($uid) { if (!$this->valid || !$uid) { return false; } // synchronize caches $this->cache->synchronize(); return $this->cache->get_by_uid($uid); } /** * Fetch a Kolab object attachment which is stored in a separate part * of the mail MIME message that represents the Kolab record. * * @param string Object's UID * @param string The attachment's mime number * @param string IMAP folder where message is stored; * If set, that also implies that the given UID is an IMAP UID * @param bool True to print the part content * @param resource File pointer to save the message part * @param boolean Disables charset conversion * * @return mixed The attachment content as binary string */ public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false) { if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) { $this->imap->set_folder($mailbox ? $mailbox : $this->name); if (substr($part, 0, 2) == 'i:') { // attachment data is stored in XML if ($object = $this->cache->get($msguid)) { // load data from XML (attachment content is not stored in cache) if ($object['_formatobj'] && isset($object['_size'])) { $object['_attachments'] = array(); $object['_formatobj']->get_attachments($object); } foreach ($object['_attachments'] as $attach) { if ($attach['id'] == $part) { if ($print) echo $attach['content']; else if ($fp) fwrite($fp, $attach['content']); else return $attach['content']; return true; } } } } else { // return message part from IMAP directly return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv); } } return null; } /** * Fetch the mime message from the storage server and extract * the Kolab groupware object from it * * @param string The IMAP message UID to fetch * @param string The object type expected (use wildcard '*' to accept all types) * @param string The folder name where the message is stored * * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found */ public function read_object($msguid, $type = null, $folder = null) { if (!$this->valid) { return false; } if (!$type) $type = $this->type; if (!$folder) $folder = $this->name; $this->imap->set_folder($folder); - $this->cache->bypass(true); + $this->cache->imap_mode(true); $message = new rcube_message($msguid); - $this->cache->bypass(false); + $this->cache->imap_mode(false); // Message doesn't exist? if (empty($message->headers)) { return false; } // extract the X-Kolab-Type header from the XML attachment part if missing if (empty($message->headers->others['x-kolab-type'])) { foreach ((array)$message->attachments as $part) { if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) { $message->headers->others['x-kolab-type'] = $part->mimetype; break; } } } // fix buggy messages stating the X-Kolab-Type header twice else if (is_array($message->headers->others['x-kolab-type'])) { $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']); } // no object type header found: abort if (empty($message->headers->others['x-kolab-type'])) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "No X-Kolab-Type information found in message $msguid ($this->name).", ), true); return false; } $object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']); $content_type = kolab_format::KTYPE_PREFIX . $object_type; // check object type header and abort on mismatch if ($type != '*' && $object_type != $type) return false; $attachments = array(); // get XML part foreach ((array)$message->attachments as $part) { if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!i', $part->mimetype))) { $xml = $message->get_part_body($part->mime_id, true); } 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; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'size' => $size, ); } } if (!$xml) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not find Kolab data part in message $msguid ($this->name).", ), true); return false; } // check kolab format version $format_version = $message->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); 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['_type'] = $object_type; $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; $object['_formatobj'] = $format; return $object; } else { // try to extract object UID from XML block if (preg_match('!(.+)!Uims', $xml, $m)) $msgadd = " UID = " . trim(strip_tags($m[1])); rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd, ), true); } return false; } /** * Save an object in this folder. * * @param array $object The array that holds the data of the object. * @param string $type The type of the kolab object. * @param string $uid The UID of the old object if it existed before * * @return mixed False on error or IMAP message UID on success */ public function save(&$object, $type = null, $uid = null) { if (!$this->valid) { return false; } if (!$type) $type = $this->type; // copy attachments from old message $copyfrom = $object['_copyfrom'] ?: $object['_msguid']; if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) { foreach ((array)$old['_attachments'] as $key => $att) { if (!isset($object['_attachments'][$key])) { $object['_attachments'][$key] = $old['_attachments'][$key]; } // unset deleted attachment entries if ($object['_attachments'][$key] == false) { unset($object['_attachments'][$key]); } // load photo.attachment from old Kolab2 format to be directly embedded in xcard block else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) { if (!isset($object['photo'])) $object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']); unset($object['_attachments'][$key]); } } } // save contact photo to attachment for Kolab2 format if (kolab_storage::$version == '2.0' && $object['photo']) { $attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp $object['_attachments'][$attkey] = array( 'mimetype'=> rcube_mime::image_content_type($object['photo']), 'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']), ); } // process attachments if (is_array($object['_attachments'])) { $numatt = count($object['_attachments']); foreach ($object['_attachments'] as $key => $attachment) { // FIXME: kolab_storage and Roundcube attachment hooks use different fields! if (empty($attachment['content']) && !empty($attachment['data'])) { $attachment['content'] = $attachment['data']; unset($attachment['data'], $object['_attachments'][$key]['data']); } // make sure size is set, so object saved in cache contains this info if (!isset($attachment['size'])) { if (!empty($attachment['content'])) { if (is_resource($attachment['content'])) { // this need to be a seekable resource, otherwise // fstat() failes and we're unable to determine size // here nor in rcube_imap_generic before IMAP APPEND $stat = fstat($attachment['content']); $attachment['size'] = $stat ? $stat['size'] : 0; } else { $attachment['size'] = strlen($attachment['content']); } } else if (!empty($attachment['path'])) { $attachment['size'] = filesize($attachment['path']); } $object['_attachments'][$key] = $attachment; } // generate unique keys (used as content-id) for attachments if (is_numeric($key) && $key < $numatt) { // derrive content-id from attachment file name $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null; $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii if (!$basename) $basename = 'noname'; $cid = $basename . '.' . microtime(true) . $key . $ext; $object['_attachments'][$cid] = $attachment; unset($object['_attachments'][$key]); } } } // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) { $this->save_recurrence_exceptions($object, $type); } // check IMAP BINARY extension support for 'file' objects // allow configuration to workaround bug in Cyrus < 2.4.17 $rcmail = rcube::get_instance(); $binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY'); // generate and save object message if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) { // resolve old msguid before saving if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) { $object['_msguid'] = $msguid; $object['_mailbox'] = $this->name; } $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary); // update cache with new UID if ($result) { $old_uid = $object['_msguid']; $object['_msguid'] = $result; $object['_mailbox'] = $this->name; if ($old_uid) { // delete old message - $this->cache->bypass(true); + $this->cache->imap_mode(true); $this->imap->delete_message($old_uid, $object['_mailbox']); - $this->cache->bypass(false); + $this->cache->imap_mode(false); } // insert/update message in cache $this->cache->save($result, $object, $old_uid); } // remove temp file if ($body_file) { @unlink($body_file); } } return $result; } /** * Save recurrence exceptions as individual objects. * The Kolab v2 format doesn't allow us to save fully embedded exception objects. * * @param array Hash array with event properties * @param string Object type */ private function save_recurrence_exceptions(&$object, $type = null) { if ($object['recurrence']['EXCEPTIONS']) { $exdates = array(); foreach ((array)$object['recurrence']['EXDATE'] as $exdate) { $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate); $exdates[$key] = 1; } // save every exception as individual object foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) { $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd')); $exception['sequence'] = $object['sequence'] + 1; if ($exception['thisandfuture']) { $exception['recurrence'] = $object['recurrence']; // adjust the recurrence duration of the exception if ($object['recurrence']['COUNT']) { $recurrence = new kolab_date_recurrence($object['_formatobj']); if ($end = $recurrence->end()) { unset($exception['recurrence']['COUNT']); $exception['recurrence']['UNTIL'] = $end; } } // set UNTIL date if we have a thisandfuture exception $untildate = clone $exception['start']; $untildate->sub(new DateInterval('P1D')); $object['recurrence']['UNTIL'] = $untildate; unset($object['recurrence']['COUNT']); } else { if (!$exdates[$exception['start']->format('Y-m-d')]) $object['recurrence']['EXDATE'][] = clone $exception['start']; unset($exception['recurrence']); } unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']); $this->save($exception, $type, $exception['uid']); } unset($object['recurrence']['EXCEPTIONS']); } } /** * Generate an object UID with the given recurrence-ID in a way that it is * unique (the original UID is not a substring) but still recoverable. */ private static function recurrence_exception_uid($uid, $recurrence_id) { $offset = -2; return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset); } /** * Delete the specified object from this folder. * * @param mixed $object The Kolab object to delete or object UID * @param boolean $expunge Should the folder be expunged? * * @return boolean True if successful, false on error */ public function delete($object, $expunge = true) { if (!$this->valid) { return false; } $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object); $success = false; - $this->cache->bypass(true); + $this->cache->imap_mode(true); if ($msguid && $expunge) { $success = $this->imap->delete_message($msguid, $this->name); } else if ($msguid) { $success = $this->imap->set_flag($msguid, 'DELETED', $this->name); } - $this->cache->bypass(false); + $this->cache->imap_mode(false); if ($success) { $this->cache->set($msguid, false); } return $success; } /** * */ public function delete_all() { if (!$this->valid) { return false; } $this->cache->purge(); - $this->cache->bypass(true); + $this->cache->imap_mode(true); $result = $this->imap->clear_folder($this->name); - $this->cache->bypass(false); + $this->cache->imap_mode(false); return $result; } /** * Restore a previously deleted object * * @param string Object UID * @return mixed Message UID on success, false on error */ public function undelete($uid) { if (!$this->valid) { return false; } if ($msguid = $this->cache->uid2msguid($uid, true)) { - $this->cache->bypass(true); + $this->cache->imap_mode(true); $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name); - $this->cache->bypass(false); + $this->cache->imap_mode(false); if ($result) { return $msguid; } } return false; } /** * Move a Kolab object message to another IMAP folder * * @param string Object UID * @param string IMAP folder to move object to * @return boolean True on success, false on failure */ public function move($uid, $target_folder) { if (!$this->valid) { return false; } if (is_string($target_folder)) $target_folder = kolab_storage::get_folder($target_folder); if ($msguid = $this->cache->uid2msguid($uid)) { - $this->cache->bypass(true); + $this->cache->imap_mode(true); $result = $this->imap->move_message($msguid, $target_folder->name, $this->name); - $this->cache->bypass(false); + $this->cache->imap_mode(false); if ($result) { $new_uid = ($copyuid = $this->imap->conn->data['COPYUID']) ? $copyuid[1] : null; $this->cache->move($msguid, $uid, $target_folder, $new_uid); return true; } else { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(), ), true); } } return false; } /** * Creates source of the configuration object message * * @param array $object The array that holds the data of the object. * @param string $type The type of the kolab object. * @param bool $binary Enables use of binary encoding of attachment(s) * @param string $body_file Reference to filename of message body * * @return mixed Message as string or array with two elements * (one for message file path, second for message headers) */ private function build_message(&$object, $type, $binary, &$body_file) { // load old object to preserve data we don't understand/process if (is_object($object['_formatobj'])) $format = $object['_formatobj']; else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) $format = $old['_formatobj']; // create new kolab_format instance if (!$format) $format = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) return false; $format->set($object); $xml = $format->write(kolab_storage::$version); $object['uid'] = $format->uid; // read UID from format $object['_formatobj'] = $format; if (empty($xml) || !$format->is_valid() || empty($object['uid'])) { return false; } $mime = new Mail_mime("\r\n"); $rcmail = rcube::get_instance(); $headers = array(); $files = array(); $part_id = 1; $encoding = $binary ? 'binary' : 'base64'; if ($user_email = $rcmail->get_user_email()) { $headers['From'] = $user_email; $headers['To'] = $user_email; } $headers['Date'] = date('r'); $headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type; $headers['X-Kolab-Mime-Version'] = kolab_storage::$version; $headers['Subject'] = $object['uid']; // $headers['Message-ID'] = $rcmail->gen_message_id(); $headers['User-Agent'] = $rcmail->config->get('useragent'); // Check if we have enough memory to handle the message in it // It's faster than using files, so we'll do this if we only can if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) { $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB foreach ($object['_attachments'] as $attachment) { $memory += $attachment['size']; } // 1.33 is for base64, we need at least 4x more memory than the message size if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) { $marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%'; $is_file = true; $temp_dir = unslashify($rcmail->config->get('temp_dir')); $mime->setParam('delay_file_io', true); } } $mime->headers($headers); $mime->setTXTBody("This is a Kolab Groupware object. " . "To view this object you will need an email client that understands the Kolab Groupware format. " . "For a list of such email clients please visit http://www.kolab.org/\n\n"); $ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE; // Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines" // when APPENDing from temp file $xml = preg_replace('/\r?\n/', "\r\n", $xml); $mime->addAttachment($xml, // file $ctype, // content-type 'kolab.xml', // filename false, // is_file '8bit', // encoding 'attachment', // disposition RCUBE_CHARSET // charset ); $part_id++; // save object attachments as separate parts foreach ((array)$object['_attachments'] as $key => $att) { if (empty($att['content']) && !empty($att['id'])) { // @TODO: use IMAP CATENATE to skip attachment fetch+push operation $msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']); if ($is_file) { $att['path'] = tempnam($temp_dir, 'rcmAttmnt'); if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) { fclose($fp); } else { return false; } } else { $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true); } } $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable')); $name = !empty($att['name']) ? $att['name'] : $key; // To store binary files we can use faster method // without writting full message content to a temporary file but // directly to IMAP, see rcube_imap_generic::append(). // I.e. use file handles where possible if (!empty($att['path'])) { if ($is_file && $binary) { $files[] = fopen($att['path'], 'r'); $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } else { $mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } } else { if (is_resource($att['content']) && $is_file && $binary) { $files[] = $att['content']; $mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } else { if (is_resource($att['content'])) { @rewind($att['content']); $att['content'] = stream_get_contents($att['content']); } $mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers); } } $object['_attachments'][$key]['id'] = ++$part_id; } if (!$is_file || !empty($files)) { $message = $mime->getMessage(); } // parse message and build message array with // attachment file pointers in place of file markers if (!empty($files)) { $message = explode($marker, $message); $tmp = array(); foreach ($message as $msg_part) { $tmp[] = $msg_part; if ($file = array_shift($files)) { $tmp[] = $file; } } $message = $tmp; } // write complete message body into temp file else if ($is_file) { // use common temp dir $body_file = tempnam($temp_dir, 'rcmMsg'); if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) { rcube::raise_error(array('code' => 650, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Could not create message: ".$mime_result->getMessage()), true, false); return false; } $message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r')); } return $message; } /** * Triggers any required updates after changes within the * folder. This is currently only required for handling free/busy * information with Kolab. * * @return boolean|PEAR_Error True if successfull. */ public function trigger() { $owner = $this->get_owner(); $result = false; switch($this->type) { case 'event': if ($this->get_namespace() == 'personal') { $result = $this->trigger_url( sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), urlencode($owner), urlencode($this->imap->mod_folder($this->name)) ), $this->imap->options['user'], $this->imap->options['password'] ); } break; default: return true; } if ($result && is_object($result) && is_a($result, 'PEAR_Error')) { return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s", $this->name, $result->getMessage())); } return $result; } /** * Triggers a URL. * * @param string $url The URL to be triggered. * @param string $auth_user Username to authenticate with * @param string $auth_passwd Password for basic auth * @return boolean|PEAR_Error True if successfull. */ private function trigger_url($url, $auth_user = null, $auth_passwd = null) { try { $request = libkolab::http_request($url); // set authentication credentials if ($auth_user && $auth_passwd) $request->setAuth($auth_user, $auth_passwd); $result = $request->send(); // rcube::write_log('trigger', $result->getBody()); } catch (Exception $e) { return PEAR::raiseError($e->getMessage()); } return true; } }