Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
194 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php
index 849b8c2..c93e02f 100644
--- a/lib/kolab_sync_data.php
+++ b/lib/kolab_sync_data.php
@@ -1,1652 +1,1641 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Base class for Syncroton data backends
*/
abstract class kolab_sync_data implements Syncroton_Data_IData
{
/**
* ActiveSync protocol version
*
* @var int
*/
protected $asversion = 0;
/**
* information about the current device
*
* @var Syncroton_Model_IDevice
*/
protected $device;
/**
* timestamp to use for all sync requests
*
* @var DateTime
*/
protected $syncTimeStamp;
/**
* name of model to use
*
* @var string
*/
protected $modelName;
/**
* type of the default folder
*
* @var int
*/
protected $defaultFolderType;
/**
* default container for new entries
*
* @var string
*/
protected $defaultFolder;
/**
* type of user created folders
*
* @var int
*/
protected $folderType;
/**
* Internal cache for storage folders list
*
* @var array
*/
protected $folders = [];
/**
* Logger instance.
*
* @var kolab_sync_logger
*/
protected $logger;
/**
* Timezone
*
* @var string
*/
protected $timezone;
/**
* List of device types with multiple folders support
*
* @var array
*/
protected $ext_devices = array(
'iphone',
'ipad',
'thundertine',
'windowsphone',
'wp',
'wp8',
'playbook',
);
protected $lastsync_folder = null;
protected $lastsync_time = null;
const RESULT_OBJECT = 0;
const RESULT_UID = 1;
const RESULT_COUNT = 2;
/**
* Recurrence types
*/
const RECUR_TYPE_DAILY = 0; // Recurs daily.
const RECUR_TYPE_WEEKLY = 1; // Recurs weekly
const RECUR_TYPE_MONTHLY = 2; // Recurs monthly
const RECUR_TYPE_MONTHLY_DAYN = 3; // Recurs monthly on the nth day
const RECUR_TYPE_YEARLY = 5; // Recurs yearly
const RECUR_TYPE_YEARLY_DAYN = 6; // Recurs yearly on the nth day
/**
* Day of week constants
*/
const RECUR_DOW_SUNDAY = 1;
const RECUR_DOW_MONDAY = 2;
const RECUR_DOW_TUESDAY = 4;
const RECUR_DOW_WEDNESDAY = 8;
const RECUR_DOW_THURSDAY = 16;
const RECUR_DOW_FRIDAY = 32;
const RECUR_DOW_SATURDAY = 64;
const RECUR_DOW_LAST = 127; // The last day of the month. Used as a special value in monthly or yearly recurrences.
/**
* Mapping of recurrence types
*
* @var array
*/
protected $recurTypeMap = array(
self::RECUR_TYPE_DAILY => 'DAILY',
self::RECUR_TYPE_WEEKLY => 'WEEKLY',
self::RECUR_TYPE_MONTHLY => 'MONTHLY',
self::RECUR_TYPE_MONTHLY_DAYN => 'MONTHLY',
self::RECUR_TYPE_YEARLY => 'YEARLY',
self::RECUR_TYPE_YEARLY_DAYN => 'YEARLY',
);
/**
* Mapping of weekdays
* NOTE: ActiveSync uses a bitmask
*
* @var array
*/
protected $recurDayMap = array(
'SU' => self::RECUR_DOW_SUNDAY,
'MO' => self::RECUR_DOW_MONDAY,
'TU' => self::RECUR_DOW_TUESDAY,
'WE' => self::RECUR_DOW_WEDNESDAY,
'TH' => self::RECUR_DOW_THURSDAY,
'FR' => self::RECUR_DOW_FRIDAY,
'SA' => self::RECUR_DOW_SATURDAY,
);
/**
* the constructor
*
* @param Syncroton_Model_IDevice $device
* @param DateTime $syncTimeStamp
*/
public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp)
{
$this->backend = kolab_sync::storage();
$this->device = $device;
$this->asversion = floatval($device->acsversion);
$this->syncTimeStamp = $this->backend->syncTimeStamp = $syncTimeStamp;
$this->logger = Syncroton_Registry::get(Syncroton_Registry::LOGGERBACKEND);
$this->defaultRootFolder = $this->defaultFolder . '::Syncroton';
// set internal timezone of kolab_format to user timezone
try {
$this->timezone = rcube::get_instance()->config->get('timezone', 'GMT');
kolab_format::$timezone = new DateTimeZone($this->timezone);
}
catch (Exception $e) {
//rcube::raise_error($e, true);
$this->timezone = 'GMT';
kolab_format::$timezone = new DateTimeZone('GMT');
}
}
/**
* return list of supported folders for this backend
*
* @return array
*/
public function getAllFolders()
{
$list = array();
// device supports multiple folders ?
if ($this->isMultiFolder()) {
// get the folders the user has access to
$list = $this->listFolders();
}
else if ($default = $this->getDefaultFolder()) {
$list = array($default['serverId'] => $default);
}
// getAllFolders() is called only in FolderSync
// throw Syncroton_Exception_Status_FolderSync exception
if (!is_array($list)) {
throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR);
}
foreach ($list as $idx => $folder) {
$list[$idx] = new Syncroton_Model_Folder($folder);
}
return $list;
}
/**
* Retrieve folders which were modified since last sync
*
* @param DateTime $startTimeStamp
* @param DateTime $endTimeStamp
*
* @return array List of folders
*/
public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp)
{
// FIXME/TODO: Can we get mtime of a DAV folder?
// Without this, we have a problem if folder ID does not change on rename
return array();
}
/**
* Returns true if the device supports multiple folders or it was configured so
*/
protected function isMultiFolder()
{
$config = rcube::get_instance()->config;
$blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName);
if (!is_array($blacklist)) {
$blacklist = $config->get('activesync_multifolder_blacklist');
}
if (is_array($blacklist)) {
return !$this->deviceTypeFilter($blacklist);
}
return in_array_nocase($this->device->devicetype, $this->ext_devices);
}
/**
* Returns default folder for current class type.
*/
protected function getDefaultFolder()
{
// Check if there's any folder configured for sync
$folders = $this->listFolders();
if (empty($folders)) {
return $folders;
}
foreach ($folders as $folder) {
if ($folder['type'] == $this->defaultFolderType) {
$default = $folder;
break;
}
}
// Return first on the list if there's no default
if (empty($default)) {
$default = array_first($folders);
// make sure the type is default here
$default['type'] = $this->defaultFolderType;
}
// Remember real folder ID and set ID/name to root folder
$default['realid'] = $default['serverId'];
$default['serverId'] = $this->defaultRootFolder;
$default['displayName'] = $this->defaultFolder;
return $default;
}
/**
* Creates a folder
*/
public function createFolder(Syncroton_Model_IFolder $folder)
{
$result = $this->backend->folder_create($folder->displayName, $folder->type, $this->device->deviceid, $folder->parentId);
if ($result) {
$folder->serverId = $result;
return $folder;
}
// Note: Looks like Outlook 2013 ignores any errors on FolderCreate command
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::UNKNOWN_ERROR);
}
/**
* Updates a folder
*/
public function updateFolder(Syncroton_Model_IFolder $folder)
{
$result = $this->backend->folder_rename($folder->serverId, $this->device->deviceid, $folder->displayName, $folder->parentId);
if ($result) {
return $folder;
}
// @TODO: throw exception
}
/**
* Deletes a folder
*/
public function deleteFolder($folder)
{
if ($folder instanceof Syncroton_Model_IFolder) {
$folder = $folder->serverId;
}
// @TODO: throw exception
return $this->backend->folder_delete($folder, $this->device->deviceid);
}
/**
* Empty folder (remove all entries and optionally subfolders)
*
* @param string $folderId Folder identifier
* @param array $options Options
*/
public function emptyFolderContents($folderid, $options)
{
// ActiveSync spec.: Clients use EmptyFolderContents to empty the Deleted Items folder.
// The client can clear out all items in the Deleted Items folder when the user runs out of storage quota
// (indicated by the return of an MailboxQuotaExceeded (113) status code from the server.
// FIXME: Does that mean we don't need this to work on any other folder?
// TODO: Respond with MailboxQuotaExceeded status. Where exactly?
foreach ($this->extractFolders($folderid) as $folderid) {
if (!$this->backend->folder_empty($folderid, $this->device->deviceid, !empty($options['deleteSubFolders']))) {
throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
}
}
}
/**
* Moves object into another location (folder)
*
* @param string $srcFolderId Source folder identifier
* @param string $serverId Object identifier
* @param string $dstFolderId Destination folder identifier
*
* @throws Syncroton_Exception_Status
* @return string New object identifier
*/
public function moveItem($srcFolderId, $serverId, $dstFolderId)
{
$item = $this->getObject($srcFolderId, $serverId);
if (!$item) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
// TODO
/*
$dstname = $this->backend->folder_id2name($dstFolderId, $this->device->deviceid);
if ($dstname === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if (!$folder->move($serverId, $dstname)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
return $item['uid'];
*/
}
/**
* Add entry
*
* @param string $folderId Folder identifier
* @param Syncroton_Model_IEntry $entry Entry object
*
* @return string ID of the created entry
*/
public function createEntry($folderId, Syncroton_Model_IEntry $entry)
{
$entry = $this->toKolab($entry, $folderId);
$entry = $this->createObject($folderId, $entry);
if (empty($entry)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $entry['_serverId'];
}
/**
* update existing entry
*
* @param string $folderId
* @param string $serverId
* @param SimpleXMLElement $entry
*
* @return string ID of the updated entry
*/
public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
{
$oldEntry = $this->getObject($folderId, $serverId);
if (empty($oldEntry)) {
throw new Syncroton_Exception_NotFound('entry not found');
}
$entry = $this->toKolab($entry, $folderId, $oldEntry);
$entry = $this->updateObject($folderId, $serverId, $entry);
if (empty($entry)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $entry['_serverId'];
}
/**
* delete entry
*
* @param string $folderId
* @param string $serverId
* @param array $collectionData
*/
public function deleteEntry($folderId, $serverId, $collectionData)
{
$deleted = $this->deleteObject($folderId, $serverId);
if (!$deleted) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
}
/**
* Get attachment data from the server.
*
* @param string $fileReference
*
* @return Syncroton_Model_FileReference
*/
public function getFileReference($fileReference)
{
// to be implemented by Email data class
// @TODO: throw "unimplemented" exception here?
}
/**
* Search for existing entries
*
* @param string $folderid Folder identifier
* @param array $filter Search filter
* @param int $result_type Type of the result (see RESULT_* constants)
*
- * @return array|int Search result as count or array of uids/objects
+ * @return array|int Search result as count or array of uids/objects
*/
protected function searchEntries($folderid, $filter = array(), $result_type = self::RESULT_UID)
{
- $folders = $this->extractFolders($folderid);
-
- if ($folders === null) {
- throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
- }
-
- $result = $result_type == self::RESULT_COUNT ? 0 : [];
- $ts = time();
- $force = $this->lastsync_folder != $folderid || $this->lastsync_time <= $ts - Syncroton_Registry::getPingTimeout();
- $tags = isset($this->tag_categories) && !$this->tag_categories;
- $found = false;
-
- foreach ($folders as $folderid) {
- $search = $this->backend->searchEntries($folderid, $this->device->id, $this->modelName, $filter, $result_type, $force, $tags);
+ $result = $result_type == self::RESULT_COUNT ? 0 : [];
+ $ts = time();
+ $force = $this->lastsync_folder != $folderid || $this->lastsync_time <= $ts - Syncroton_Registry::getPingTimeout();
+ $tags = isset($this->tag_categories) && !$this->tag_categories;
+ $found = false;
+
+ foreach ($this->extractFolders($folderid) as $fid) {
+ $search = $this->backend->searchEntries($fid, $this->device->id, $this->modelName, $filter, $result_type, $force, $tags);
$found = true;
switch ($result_type) {
case self::RESULT_COUNT:
$result += $search;
break;
case self::RESULT_UID:
foreach ($search as $idx => $uid) {
- $search[$idx] = $this->serverId($uid, $folderid);
+ $search[$idx] = $this->serverId($uid, $fid);
}
+
$result = array_unique(array_merge($result, $search));
break;
}
}
if (!$found) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$this->lastsync_folder = $folderid;
$this->lastsync_time = $ts;
return $result;
}
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
// overwrite by child class according to specified type
return array();
}
/**
* get all entries changed between two dates
*
* @param string $folderId
* @param DateTime $start
* @param DateTime $end
* @param int $filterType
*
* @return array
*/
public function getChangedEntries($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
{
$filter = $this->filter($filter_type);
$filter[] = array('changed', '>', $start);
if ($end) {
$filter[] = array('changed', '<=', $end);
}
return $this->searchEntries($folderId, $filter, self::RESULT_UID);
}
/**
* Get count of entries changed between two dates
*
* @param string $folderId
* @param DateTime $start
* @param DateTime $end
* @param int $filterType
*
* @return int
*/
public function getChangedEntriesCount($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
{
$filter = $this->filter($filter_type);
$filter[] = array('changed', '>', $start);
if ($end) {
$filter[] = array('changed', '<=', $end);
}
return $this->searchEntries($folderId, $filter, self::RESULT_COUNT);
}
/**
* get id's of all entries available on the server
*
* @param string $folderId
* @param int $filterType
*
* @return array
*/
public function getServerEntries($folder_id, $filter_type)
{
$filter = $this->filter($filter_type);
$result = $this->searchEntries($folder_id, $filter, self::RESULT_UID);
return $result;
}
/**
* get count of all entries available on the server
*
* @param string $folderId
* @param int $filterType
*
* @return int
*/
public function getServerEntriesCount($folder_id, $filter_type)
{
$filter = $this->filter($filter_type);
$result = $this->searchEntries($folder_id, $filter, self::RESULT_COUNT);
return $result;
}
/**
* Returns number of changed objects in the backend folder
*
* @param Syncroton_Backend_IContent $contentBackend
* @param Syncroton_Model_IFolder $folder
* @param Syncroton_Model_ISyncState $syncState
*
* @return int
*/
public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
$allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter);
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$changedEntries = $this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
return count($addedEntries) + count($deletedEntries) + $changedEntries;
}
/**
* Returns true if any data got modified in the backend folder
*
* @param Syncroton_Backend_IContent $contentBackend
* @param Syncroton_Model_IFolder $folder
* @param Syncroton_Model_ISyncState $syncState
*
* @return bool
*/
public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
{
try {
if ($this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype)) {
return true;
}
$allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter);
// @TODO: Consider looping over all folders here, not in getServerEntries() and
// getChangedEntriesCount(). This way we could break the loop and not check all folders
// or at least skip redundant cache sync of the same folder
$allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
$addedEntries = array_diff($allServerEntries, $allClientEntries);
$deletedEntries = array_diff($allClientEntries, $allServerEntries);
return count($addedEntries) > 0 || count($deletedEntries) > 0;
}
catch (Exception $e) {
// return "no changes" if something failed
return false;
}
}
/**
* Fetches the entry from the backend
*/
protected function getObject($folderid, $entryid)
{
- $folders = $this->extractFolders($folderid);
-
- if (empty($folders)) {
- return null;
- }
-
- foreach ($folders as $folderid) {
+ foreach ($this->extractFolders($folderid) as $fid) {
$crc = null;
$uid = $entryid;
// See self::serverId() for full explanation
// Use (slower) UID prefix matching...
if (preg_match('/^CRC([0-9A-Fa-f]{8})(.+)$/', $uid, $matches)) {
$crc = $matches[1];
$uid = $matches[2];
if (strlen($entryid) >= 64) {
- $objects = $this->backend->getObjectsByUidPrefix($folderid, $this->device->deviceid, $this->modelName, $uid);
+ $objects = $this->backend->getObjectsByUidPrefix($fid, $this->device->deviceid, $this->modelName, $uid);
foreach ($objects as $object) {
if (($object['uid'] === $uid || strpos($object['uid'], $uid) === 0)
- && $crc == $this->objectCRC($object['uid'], $folderid)
+ && $crc == $this->objectCRC($object['uid'], $fid)
) {
- $object['folderId'] = $folderid;
+ $object['folderId'] = $fid;
return $object;
}
}
continue;
}
}
// Or (faster) strict UID matching...
- $object = $this->backend->getObject($folderid, $this->device->deviceid, $this->modelName, $uid);
+ $object = $this->backend->getObject($fid, $this->device->deviceid, $this->modelName, $uid);
- if (!empty($object) && ($crc === null || $crc == $this->objectCRC($object['uid'], $folderid))) {
- $object['folderId'] = $folderid;
+ if (!empty($object) && ($crc === null || $crc == $this->objectCRC($object['uid'], $fid))) {
+ $object['folderId'] = $fid;
return $object;
}
}
}
/**
* Saves the entry on the backend
*/
protected function createObject($folderid, $data)
{
if ($folderid == $this->defaultRootFolder) {
$default = $this->getDefaultFolder();
if (!is_array($default)) {
return null;
}
$folderid = isset($default['realid']) ? $default['realid'] : $default['serverId'];
}
// convert categories into tags, save them after creating an object
if (!empty($data['categories']) && isset($this->tag_categories) && $this->tag_categories) {
$tags = $data['categories'];
unset($data['categories']);
}
$folder = $this->backend->getFolder($folderid, $this->device->deviceid, $this->modelName);
// Set User-Agent for saved objects
$app = kolab_sync::get_instance();
$app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
if ($folder && $folder->valid && $folder->save($data)) {
if (!empty($tags)) {
$this->backend->setCategories($data['uid'], $tags);
}
$data['_serverId'] = $this->serverId($data['uid'], $folderid);
return $data;
}
}
/**
* Updates the entry on the backend
*/
protected function updateObject($folderid, $entryid, $data)
{
$object = $this->getObject($folderid, $entryid);
if ($object) {
$folder = $this->backend->getFolder($object['folderId'], $this->device->deviceid, $this->modelName);
// convert categories into tags, save them after updating an object
if (isset($this->tag_categories) && $this->tag_categories && array_key_exists('categories', $data)) {
$tags = (array) $data['categories'];
unset($data['categories']);
}
// Set User-Agent for saved objects
$app = kolab_sync::get_instance();
$app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
if ($folder && $folder->valid && $folder->save($data, $this->modelName, $object['uid'])) {
if (isset($tags)) {
$this->backend->setCategories($data['uid'], $tags);
}
$data['_serverId'] = $this->serverId($object['uid'], $object['folderId']);
return $data;
}
}
}
/**
* Removes the entry from the backend
*/
protected function deleteObject($folderid, $entryid)
{
$object = $this->getObject($folderid, $entryid);
if ($object) {
$folder = $this->backend->getFolder($object['folderId'], $this->device->deviceid, $this->modelName);
if ($folder && $folder->valid && $folder->delete($object['uid'])) {
if (isset($this->tag_categories) && $this->tag_categories) {
$this->backend->setCategories($object['uid'], []);
}
return true;
}
return false;
}
// object doesn't exist, confirm deletion
return true;
}
/**
* Returns internal folder IDs
*
* @param string $folderid Folder identifier
*
* @return array List of folder identifiers
*/
protected function extractFolders($folderid)
{
if ($folderid instanceof Syncroton_Model_IFolder) {
$folderid = $folderid->serverId;
}
- if ($folderid == $this->defaultRootFolder) {
+ if ($folderid === $this->defaultRootFolder) {
$folders = $this->listFolders();
if (!is_array($folders)) {
- return null;
+ throw new Syncroton_Exception_NotFound('Folder not found');
}
$folders = array_keys($folders);
}
else {
$folders = array($folderid);
}
return $folders;
}
/**
* List of all IMAP folders (or subtree)
*
* @param string $parentid Parent folder identifier
*
* @return array List of folder identifiers
*/
protected function listFolders($parentid = null)
{
if (empty($this->folders)) {
$this->folders = $this->backend->folders_list(
$this->device->deviceid, $this->modelName, $this->isMultiFolder());
}
if ($parentid === null || !is_array($this->folders)) {
return $this->folders;
}
$folders = [];
$parents = [$parentid];
foreach ($this->folders as $folder_id => $folder) {
if ($folder['parentId'] && in_array($folder['parentId'], $parents)) {
$folders[$folder_id] = $folder;
$parents[] = $folder_id;
}
}
return $folders;
}
/**
* Returns ActiveSync settings of specified folder
*
* @param string $folderid Folder identifier
*
* @return array Folder settings
*/
protected function getFolderConfig($folderid)
{
return $this->backend->getFolderConfig($folderid, $this->device->deviceid, $this->modelName);
}
/**
* Convert contact from xml to kolab format
*
* @param Syncroton_Model_IEntry $data Contact data
* @param string $folderId Folder identifier
* @param array $entry Old Contact data for merge
*
* @return array
*/
abstract function toKolab(Syncroton_Model_IEntry $data, $folderId, $entry = null);
/**
* Extracts data from kolab data array
*/
protected function getKolabDataItem($data, $name)
{
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!empty($data[$name]) && is_array($data[$name])) {
foreach ($data[$name] as $element) {
if ($element['type'] == $type) {
return $element[$key_name];
}
}
}
return null;
}
// custom properties
if ($count == 2 && $name_items[0] == 'x-custom') {
$value = null;
if (!empty($data['x-custom']) && is_array($data['x-custom'])) {
foreach ($data['x-custom'] as $val) {
if (is_array($val) && $val[0] == $name_items[1]) {
$value = $val[1];
break;
}
}
}
return $value;
}
$name_items = explode(':', $name);
$name = $name_items[0];
if (empty($data[$name])) {
return null;
}
// simple array (e.g. email)
if (count($name_items) == 2) {
return $data[$name][$name_items[1]];
}
return $data[$name];
}
/**
* Saves data in kolab data array
*/
protected function setKolabDataItem(&$data, $name, $value)
{
if (empty($value)) {
return $this->unsetKolabDataItem($data, $name);
}
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!isset($data[$name])) {
$data[$name] = array();
}
foreach ($data[$name] as $idx => $element) {
if ($element['type'] == $type) {
$found = $idx;
break;
}
}
if (!isset($found)) {
$data[$name] = array_values($data[$name]);
$found = count($data[$name]);
$data[$name][$found] = array('type' => $type);
}
$data[$name][$found][$key_name] = $value;
return;
}
// custom properties
if ($count == 2 && $name_items[0] == 'x-custom') {
$data['x-custom'] = isset($data['x-custom']) ? ((array) $data['x-custom']) : array();
foreach ($data['x-custom'] as $idx => $val) {
if (is_array($val) && $val[0] == $name_items[1]) {
$data['x-custom'][$idx][1] = $value;
return;
}
}
$data['x-custom'][] = array($name_items[1], $value);
return;
}
$name_items = explode(':', $name);
$name = $name_items[0];
// simple array (e.g. email)
if (count($name_items) == 2) {
$data[$name][$name_items[1]] = $value;
return;
}
$data[$name] = $value;
}
/**
* Unsets data item in kolab data array
*/
protected function unsetKolabDataItem(&$data, $name)
{
$name_items = explode('.', $name);
$count = count($name_items);
// multi-level array (e.g. address, phone)
if ($count == 3) {
$name = $name_items[0];
$type = $name_items[1];
$key_name = $name_items[2];
if (!isset($data[$name])) {
return;
}
foreach ($data[$name] as $idx => $element) {
if ($element['type'] == $type) {
$found = $idx;
break;
}
}
if (!isset($found)) {
return;
}
unset($data[$name][$found][$key_name]);
// if there's only one element and it's 'type', remove it
if (count($data[$name][$found]) == 1 && isset($data[$name][$found]['type'])) {
unset($data[$name][$found]['type']);
}
if (empty($data[$name][$found])) {
unset($data[$name][$found]);
}
if (empty($data[$name])) {
unset($data[$name]);
}
return;
}
// custom properties
if ($count == 2 && $name_items[0] == 'x-custom') {
foreach ((array) $data['x-custom'] as $idx => $val) {
if (is_array($val) && $val[0] == $name_items[1]) {
unset($data['x-custom'][$idx]);
}
}
}
$name_items = explode(':', $name);
$name = $name_items[0];
// simple array (e.g. email)
if (count($name_items) == 2) {
unset($data[$name][$name_items[1]]);
if (empty($data[$name])) {
unset($data[$name]);
}
return;
}
unset($data[$name]);
}
/**
* Setter for Body attribute according to client version
*
* @param string $value Body
* @param array $param Body parameters
*
* @reurn Syncroton_Model_EmailBody Body element
*/
protected function setBody($value, $params = array())
{
if (empty($value) && empty($params)) {
return;
}
// Old protocol version doesn't support AirSyncBase:Body, it's eg. WindowsCE
if ($this->asversion < 12) {
return;
}
if (!empty($value)) {
// cast to string to workaround issue described in Bug #1635
$params['data'] = (string) $value;
}
if (!isset($params['type'])) {
$params['type'] = Syncroton_Model_EmailBody::TYPE_PLAINTEXT;
}
return new Syncroton_Model_EmailBody($params);
}
/**
* Getter for Body attribute value according to client version
*
* @param mixed $body Body element
* @param int $type Result data type (to which the body will be converted, if specified).
* One or array of Syncroton_Model_EmailBody constants.
*
* @return string|null Body value
*/
protected function getBody($body, $type = null)
{
$data = null;
if ($body && $body->data) {
$data = $body->data;
}
if (!$data || empty($type)) {
return null;
}
$type = (array) $type;
// Convert to specified type
if (!in_array($body->type, $type)) {
$converter = new kolab_sync_body_converter($data, $body->type);
$data = $converter->convert($type[0]);
}
return $data;
}
/**
* Converts text (plain or html) into ActiveSync Body element.
* Takes bodyPreferences into account and detects if the text is plain or html.
*/
protected function body_from_kolab($body, $collection)
{
if (empty($body)) {
return;
}
$opts = $collection->options;
$prefs = $opts['bodyPreferences'];
$html_type = Syncroton_Command_Sync::BODY_TYPE_HTML;
$type = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
$params = array();
// HTML? check for opening and closing <html> or <body> tags
$is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, '</'.$m[1].'>') > 0;
// here we assume that all devices support plain text
if ($is_html) {
// device supports HTML...
if (!empty($prefs[$html_type])) {
$type = $html_type;
}
// ...else convert to plain text
else {
$txt = new rcube_html2text($body, false, true);
$body = $txt->get_text();
}
}
// strip out any non utf-8 characters
$body = rcube_charset::clean($body);
$real_length = $body_length = strlen($body);
// truncate the body if needed
if (isset($prefs[$type]['truncationSize']) && ($truncateAt = $prefs[$type]['truncationSize']) && $body_length > $truncateAt) {
$body = mb_strcut($body, 0, $truncateAt);
$body_length = strlen($body);
$params['truncated'] = 1;
$params['estimatedDataSize'] = $real_length;
}
$params['type'] = $type;
return $this->setBody($body, $params);
}
/**
* Converts PHP DateTime, date (YYYY-MM-DD) or unixtimestamp into PHP DateTime in UTC
*
* @param DateTime|int|string $date Unix timestamp, date (YYYY-MM-DD) or PHP DateTime object
*
* @return DateTime Datetime object
*/
protected static function date_from_kolab($date)
{
if (!empty($date)) {
if (is_numeric($date)) {
$date = new DateTime('@' . $date);
}
else if (is_string($date)) {
$date = new DateTime($date, new DateTimeZone('UTC'));
}
else if ($date instanceof DateTime) {
$date = clone $date;
$tz = $date->getTimezone();
$tz_name = $tz->getName();
// convert to UTC if needed
if ($tz_name != 'UTC') {
$utc = new DateTimeZone('UTC');
// safe dateonly object conversion to UTC
// note: _dateonly flag is set by libkolab e.g. for birthdays
if ($date->_dateonly) {
// avoid time change
$date = new DateTime($date->format('Y-m-d'), $utc);
// set time to noon to avoid timezone troubles
$date->setTime(12, 0, 0);
}
else {
$date->setTimezone($utc);
}
}
}
else {
return null; // invalid input
}
return $date;
}
}
/**
* Convert Kolab event/task recurrence into ActiveSync
*/
protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event')
{
if (empty($data['recurrence']) || !empty($data['recurrence_date']) || empty($data['recurrence']['FREQ'])) {
return;
}
$recurrence = array();
$r = $data['recurrence'];
// required fields
switch($r['FREQ']) {
case 'DAILY':
$recurrence['type'] = self::RECUR_TYPE_DAILY;
break;
case 'WEEKLY':
$day = $r['BYDAY'] ?? 0;
if (!$day && (!empty($data['_start']) || !empty($data['start']))) {
$days = ['', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA','SU'];
$start = $data['_start'] ?? $data['start'];
$day = $days[$start->format('N')];
}
$recurrence['type'] = self::RECUR_TYPE_WEEKLY;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
break;
case 'MONTHLY':
if (!empty($r['BYMONTHDAY'])) {
// @TODO: ActiveSync doesn't support multi-valued month days,
// should we replicate the recurrence element for each day of month?
[$month_day, ] = explode(',', $r['BYMONTHDAY']);
$recurrence['type'] = self::RECUR_TYPE_MONTHLY;
$recurrence['dayOfMonth'] = $month_day;
}
else if (!empty($r['BYDAY'])) {
$week = (int) substr($r['BYDAY'], 0, -2);
$week = ($week == -1) ? 5 : $week;
$day = substr($r['BYDAY'], -2);
$recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN;
$recurrence['weekOfMonth'] = $week;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
} else {
return;
}
break;
case 'YEARLY':
// @TODO: ActiveSync doesn't support multi-valued months,
// should we replicate the recurrence element for each month?
[$month, ] = explode(',', $r['BYMONTH']);
if (!empty($r['BYDAY'])) {
$week = (int) substr($r['BYDAY'], 0, -2);
$week = ($week == -1) ? 5 : $week;
$day = substr($r['BYDAY'], -2);
$recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN;
$recurrence['weekOfMonth'] = $week;
$recurrence['dayOfWeek'] = $this->day2bitmask($day);
$recurrence['monthOfYear'] = $month;
}
else if (!empty($r['BYMONTHDAY'])) {
// @TODO: ActiveSync doesn't support multi-valued month days,
// should we replicate the recurrence element for each day of month?
[$month_day, ] = explode(',', $r['BYMONTHDAY']);
$recurrence['type'] = self::RECUR_TYPE_YEARLY;
$recurrence['dayOfMonth'] = $month_day;
$recurrence['monthOfYear'] = $month;
}
else {
$recurrence['type'] = self::RECUR_TYPE_YEARLY;
$recurrence['monthOfYear'] = $month;
}
break;
}
// Skip all empty values (T2519)
if ($recurrence['type'] != self::RECUR_TYPE_DAILY) {
$recurrence = array_filter($recurrence);
}
// required field
$recurrence['interval'] = $r['INTERVAL'] ?: 1;
if (!empty($r['UNTIL'])) {
$recurrence['until'] = self::date_from_kolab($r['UNTIL']);
}
else if (!empty($r['COUNT'])) {
$recurrence['occurrences'] = $r['COUNT'];
}
$class = 'Syncroton_Model_' . $type . 'Recurrence';
$result['recurrence'] = new $class($recurrence);
// Tasks do not support exceptions
if ($type == 'Event') {
$result['exceptions'] = $this->exceptions_from_kolab($collection, $data);
}
}
/**
* Convert ActiveSync event/task recurrence into Kolab
*/
protected function recurrence_to_kolab($data, $folderid, $timezone = null)
{
if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence)
&& !($data->recurrence instanceof Syncroton_Model_TaskRecurrence)
) {
return;
}
if (!isset($data->recurrence->type)) {
return;
}
$recurrence = $data->recurrence;
$type = $recurrence->type;
switch ($type) {
case self::RECUR_TYPE_DAILY:
break;
case self::RECUR_TYPE_WEEKLY:
$rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek);
break;
case self::RECUR_TYPE_MONTHLY:
$rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
break;
case self::RECUR_TYPE_MONTHLY_DAYN:
$week = $recurrence->weekOfMonth;
$day = $recurrence->dayOfWeek;
$byDay = $week == 5 ? -1 : $week;
$byDay .= $this->bitmask2day($day);
$rrule['BYDAY'] = $byDay;
break;
case self::RECUR_TYPE_YEARLY:
$rrule['BYMONTH'] = $recurrence->monthOfYear;
$rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
break;
case self::RECUR_TYPE_YEARLY_DAYN:
$rrule['BYMONTH'] = $recurrence->monthOfYear;
$week = $recurrence->weekOfMonth;
$day = $recurrence->dayOfWeek;
$byDay = $week == 5 ? -1 : $week;
$byDay .= $this->bitmask2day($day);
$rrule['BYDAY'] = $byDay;
break;
}
$rrule['FREQ'] = $this->recurTypeMap[$type];
$rrule['INTERVAL'] = isset($recurrence->interval) ? $recurrence->interval : 1;
if (isset($recurrence->until)) {
if ($timezone) {
$recurrence->until->setTimezone($timezone);
}
$rrule['UNTIL'] = $recurrence->until;
}
else if (!empty($recurrence->occurrences)) {
$rrule['COUNT'] = $recurrence->occurrences;
}
// recurrence exceptions (not supported by Tasks)
if ($data instanceof Syncroton_Model_Event) {
$this->exceptions_to_kolab($data, $rrule, $folderid, $timezone);
}
return $rrule;
}
/**
* Convert Kolab event recurrence exceptions into ActiveSync
*/
protected function exceptions_from_kolab($collection, $data)
{
if (empty($data['recurrence']['EXCEPTIONS']) && empty($data['recurrence']['EXDATE'])) {
return null;
}
$ex_list = array();
// exceptions (modified occurences)
if (!empty($data['recurrence']['EXCEPTIONS'])) {
foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) {
$exception['_mailbox'] = $data['_mailbox'];
$ex = $this->getEntry($collection, $exception, true);
$date = clone ($exception['recurrence_date'] ?: $ex['startTime']);
$ex['exceptionStartTime'] = self::set_exception_time($date, $data['_start'] ?? null);
// remove fields not supported by Syncroton_Model_EventException
unset($ex['uID']);
// @TODO: 'thisandfuture=true' is not supported in Activesync
// we'd need to slit the event into two separate events
$ex_list[] = new Syncroton_Model_EventException($ex);
}
}
// exdate (deleted occurences)
if (!empty($data['recurrence']['EXDATE'])) {
foreach ((array)$data['recurrence']['EXDATE'] as $exception) {
if (!($exception instanceof DateTime)) {
continue;
}
$ex = array(
'deleted' => 1,
'exceptionStartTime' => self::set_exception_time($exception, $data['_start'] ?? null),
);
$ex_list[] = new Syncroton_Model_EventException($ex);
}
}
return $ex_list;
}
/**
* Convert ActiveSync event recurrence exceptions into Kolab
*/
protected function exceptions_to_kolab($data, &$rrule, $folderid, $timezone = null)
{
$rrule['EXDATE'] = array();
$rrule['EXCEPTIONS'] = array();
// handle exceptions from recurrence
if (!empty($data->exceptions)) {
foreach ($data->exceptions as $exception) {
$date = clone $exception->exceptionStartTime;
if ($timezone) {
$date->setTimezone($timezone);
}
if ($exception->deleted) {
$date->setTime(0, 0, 0);
$rrule['EXDATE'][] = $date;
}
else {
$ex = $this->toKolab($exception, $folderid, null, $timezone);
$ex['recurrence_date'] = $date;
if ($data->allDayEvent) {
$ex['allday'] = 1;
}
$rrule['EXCEPTIONS'][] = $ex;
}
}
}
if (empty($rrule['EXDATE'])) {
unset($rrule['EXDATE']);
}
if (empty($rrule['EXCEPTIONS'])) {
unset($rrule['EXCEPTIONS']);
}
}
/**
* Sets ExceptionStartTime according to occurrence date and event start time
*/
protected static function set_exception_time($exception_date, $event_start)
{
if ($exception_date && $event_start) {
$hour = $event_start->format('H');
$minute = $event_start->format('i');
$second = $event_start->format('s');
$exception_date->setTime($hour, $minute, $second);
$exception_date->_dateonly = false;
return self::date_from_kolab($exception_date);
}
}
/**
* Converts string of days (TU,TH) to bitmask used by ActiveSync
*
* @param string $days
*
* @return int
*/
protected function day2bitmask($days)
{
$days = explode(',', $days);
$result = 0;
foreach ($days as $day) {
if ($day) {
$result = $result + ($this->recurDayMap[$day] ?? 0);
}
}
return $result;
}
/**
* Convert bitmask used by ActiveSync to string of days (TU,TH)
*
* @param int $days
*
* @return string
*/
protected function bitmask2day($days)
{
$days_arr = array();
for ($bitmask = 1; $bitmask <= self::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) {
$dayMatch = $days & $bitmask;
if ($dayMatch === $bitmask) {
$days_arr[] = array_search($bitmask, $this->recurDayMap);
}
}
$result = implode(',', $days_arr);
return $result;
}
/**
* Check if current device type string matches any of options
*/
protected function deviceTypeFilter($options)
{
foreach ($options as $option) {
if ($option[0] == '/') {
if (preg_match($option, $this->device->devicetype)) {
return true;
}
}
else if (stripos($this->device->devicetype, $option) !== false) {
return true;
}
}
return false;
}
/**
* Returns all email addresses of the current user
*/
protected function user_emails()
{
$user_emails = kolab_sync::get_instance()->user->list_emails();
$user_emails = array_map(function($v) { return $v['email']; }, $user_emails);
return $user_emails;
}
/**
* Generate CRC-based ServerId from object UID
*/
protected function serverId($uid, $folder)
{
// When ActiveSync communicates with the client, it refers to objects with a ServerId
// We can't use object UID for ServerId because:
// - ServerId is limited to 64 chars,
// - there can be multiple calendars with a copy of the same event.
//
// The solution is to; Take the original UID, and regardless of its length, execute the following:
// - Hash the UID concatenated with the Folder ID using CRC32b,
// - Prefix the UID with 'CRC' and the hash string,
// - Tryncate the result to 64 characters.
//
// Searching for the server-side copy of the object now follows the logic;
// - If the ServerId is prefixed with 'CRC', strip off the first 11 characters
// and we search for the UID using the remainder;
// - if the UID is shorter than 53 characters, it'll be the complete UID,
// - if the UID is longer than 53 characters, it'll be the truncated UID,
// and we search for a wildcard match of <uid>*
// When multiple copies of the same event are found, the same CRC32b hash can be used
// on the events metadata (i.e. the copy's UID and Folder ID), and compared with the CRC from the ServerId.
// ServerId is max. 64 characters, below we generate a string of max. 64 chars
// Note: crc32b is always 8 characters
return 'CRC' . $this->objectCRC($uid, $folder) . substr($uid, 0, 53);
}
/**
* Calculate checksum on object UID and folder UID
*/
protected function objectCRC($uid, $folder)
{
if (!is_object($folder)) {
$folder = $this->backend->getFolder($folder, $this->device->deviceid, $this->modelName);
}
$folder_uid = $folder->get_uid();
return strtoupper(hash('crc32b', $folder_uid . $uid)); // always 8 chars
}
}
diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php
index 221b101..5ac9c17 100644
--- a/lib/kolab_sync_data_email.php
+++ b/lib/kolab_sync_data_email.php
@@ -1,1565 +1,1567 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Email data class for Syncroton
*/
class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_IDataSearch
{
const MAX_SEARCH_RESULT = 200;
/**
* Mapping from ActiveSync Email namespace fields
*/
protected $mapping = array(
'cc' => 'cc',
//'contentClass' => 'contentclass',
'dateReceived' => 'internaldate',
//'displayTo' => 'displayto', //?
//'flag' => 'flag',
'from' => 'from',
//'importance' => 'importance',
'internetCPID' => 'charset',
//'messageClass' => 'messageclass',
'replyTo' => 'replyto',
//'read' => 'read',
'subject' => 'subject',
//'threadTopic' => 'threadtopic',
'to' => 'to',
);
static $memory_accumulated = 0;
/**
* Special folder type/name map
*
* @var array
*/
protected $folder_types = array(
2 => 'Inbox',
3 => 'Drafts',
4 => 'Deleted Items',
5 => 'Sent Items',
6 => 'Outbox',
);
/**
* Kolab object type
*
* @var string
*/
protected $modelName = 'mail';
/**
* Type of the default folder
*
* @var int
*/
protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_INBOX;
/**
* Default container for new entries
*
* @var string
*/
protected $defaultFolder = 'INBOX';
/**
* Type of user created folders
*
* @var int
*/
protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED;
/**
* the constructor
*
* @param Syncroton_Model_IDevice $device
* @param DateTime $syncTimeStamp
*/
public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp)
{
parent::__construct($device, $syncTimeStamp);
$this->storage = rcube::get_instance()->get_storage();
// Outlook 2013 support multi-folder
$this->ext_devices[] = 'windowsoutlook15';
if ($this->asversion >= 14) {
$this->tag_categories = true;
}
}
/**
* Encode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3
*
* @param array $data An array with the data to encode
*
* @return string the encoded globalObjId
*/
public static function encodeGlobalObjId(array $data): string
{
$classid = "040000008200e00074c5b7101a82e008";
if (!empty($data['data'])) {
$payload = $data['data'];
} else {
$uid = $data['uid'];
$payload = "vCal-Uid\1\0\0\0{$uid}\0";
}
$packed = pack(
"H32nCCPx8Va*",
$classid,
$data['year'] ?? 0,
$data['month'] ?? 0,
$data['day'] ?? 0,
$data['now'] ?? 0,
strlen($payload),
$payload
);
return base64_encode($packed);
}
/**
* Decode a globalObjId according to https://interoperability.blob.core.windows.net/files/MS-ASEMAIL/%5bMS-ASEMAIL%5d-150526.pdf 2.2.2.3
*
* @param string the encoded globalObjId
*
* @return array An array with the decoded data
*/
public static function decodeGlobalObjId(string $globalObjId): array
{
$unpackString = 'H32classid/nyear/Cmonth/Cday/Pnow/x8/Vbytecount/a*data';
$decoded = unpack($unpackString, base64_decode($globalObjId));
$decoded['uid'] = substr($decoded['data'], strlen("vCal-Uid\1\0\0\0"), -1);
return $decoded;
}
/**
* Creates model object
*
* @param Syncroton_Model_SyncCollection $collection Collection data
* @param string $serverId Local entry identifier
*
* @return Syncroton_Model_Email Email object
*/
public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId)
{
$message = $this->getObject($serverId);
// error (message doesn't exist?)
if (empty($message)) {
throw new Syncroton_Exception_NotFound("Message $serverId not found");
}
$headers = $message->headers; // rcube_message_header
$this->storage->set_folder($message->folder);
$this->logger->debug(sprintf("Processing message %s (size: %.2f MB)", $serverId, $headers->size / 1024 / 1024));
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
$value = null;
switch ($name) {
case 'internaldate':
$value = self::date_from_kolab(rcube_utils::strtotime($headers->internaldate));
break;
case 'cc':
case 'to':
case 'replyto':
case 'from':
$addresses = rcube_mime::decode_address_list($headers->$name, null, true, $headers->charset);
foreach ($addresses as $idx => $part) {
// @FIXME: set name + address or address only?
$addresses[$idx] = format_email_recipient($part['mailto'], $part['name']);
}
$value = implode(',', $addresses);
break;
case 'subject':
$value = $headers->get('subject');
break;
case 'charset':
$value = self::charset_to_cp($headers->charset);
break;
}
if (empty($value) || is_array($value)) {
continue;
}
if (is_string($value)) {
$value = rcube_charset::clean($value);
}
$result[$key] = $value;
}
// $result['ConversationId'] = 'FF68022058BD485996BE15F6F6D99320';
// $result['ConversationIndex'] = 'CA2CFA8A23';
// Read flag
$result['read'] = intval(!empty($headers->flags['SEEN']));
// Flagged message
if (!empty($headers->flags['FLAGGED'])) {
// Use FollowUp flag which is used in Android when message is marked with a star
$result['flag'] = new Syncroton_Model_EmailFlag(array(
'flagType' => 'FollowUp',
'status' => Syncroton_Model_EmailFlag::STATUS_ACTIVE,
));
} else {
$result['flag'] = new Syncroton_Model_EmailFlag();
}
// Importance/Priority
if ($headers->priority) {
if ($headers->priority < 3) {
$result['importance'] = 2; // High
}
else if ($headers->priority > 3) {
$result['importance'] = 0; // Low
}
}
// get truncation and body type
$airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
$truncateAt = null;
$opts = $collection->options;
$prefs = $opts['bodyPreferences'];
if ($opts['mimeSupport'] == Syncroton_Command_Sync::MIMESUPPORT_SEND_MIME) {
$airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_MIME;
if (isset($prefs[Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize'])) {
$truncateAt = $prefs[Syncroton_Command_Sync::BODY_TYPE_MIME]['truncationSize'];
}
else if (isset($opts['mimeTruncation']) && $opts['mimeTruncation'] < Syncroton_Command_Sync::TRUNCATE_NOTHING) {
switch ($opts['mimeTruncation']) {
case Syncroton_Command_Sync::TRUNCATE_ALL:
$truncateAt = 0;
break;
case Syncroton_Command_Sync::TRUNCATE_4096:
$truncateAt = 4096;
break;
case Syncroton_Command_Sync::TRUNCATE_5120:
$truncateAt = 5120;
break;
case Syncroton_Command_Sync::TRUNCATE_7168:
$truncateAt = 7168;
break;
case Syncroton_Command_Sync::TRUNCATE_10240:
$truncateAt = 10240;
break;
case Syncroton_Command_Sync::TRUNCATE_20480:
$truncateAt = 20480;
break;
case Syncroton_Command_Sync::TRUNCATE_51200:
$truncateAt = 51200;
break;
case Syncroton_Command_Sync::TRUNCATE_102400:
$truncateAt = 102400;
break;
}
}
}
else {
// The spec is not very clear, but it looks that if MimeSupport is not set
// we can't add Syncroton_Command_Sync::BODY_TYPE_MIME to the supported types
// list below (Bug #1688)
$types = array(
Syncroton_Command_Sync::BODY_TYPE_HTML,
Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT,
);
// @TODO: if client can support both HTML and TEXT use one of
// them which is better according to the real message body type
foreach ($types as $type) {
if (!empty($prefs[$type])) {
if (!empty($prefs[$type]['truncationSize'])) {
$truncateAt = $prefs[$type]['truncationSize'];
}
$preview = (int) (isset($prefs[$type]['preview']) ? $prefs[$type]['preview'] : 0);
$airSyncBaseType = $type;
break;
}
}
}
$body_params = array('type' => $airSyncBaseType);
// Message body
// In Sync examples there's one in which bodyPreferences is not defined
// in such case Truncated=1 and there's no body sent to the client
// only it's estimated size
$isTruncated = 0;
if (empty($prefs)) {
$messageBody = '';
$real_length = $headers->size;
$truncateAt = 0;
$body_length = 0;
$isTruncated = 1;
}
else if ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) {
// Check if we have enough memory to handle the message
$messageBody = $this->message_mem_check($message, $headers->size);
static::$memory_accumulated += $headers->size;
if (empty($messageBody)) {
$messageBody = $this->storage->get_raw_body($message->uid);
}
// make the source safe (Bug #2715, #2757)
$messageBody = kolab_sync_message::recode_message($messageBody);
// strip out any non utf-8 characters
$messageBody = rcube_charset::clean($messageBody);
$real_length = $body_length = strlen($messageBody);
}
else {
$messageBody = $this->getMessageBody($message, $airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_HTML);
// strip out any non utf-8 characters
$messageBody = rcube_charset::clean($messageBody);
$real_length = $body_length = strlen($messageBody);
}
// add Preview element to the Body result
if (!empty($preview) && $body_length) {
$body_params['preview'] = $this->getPreview($messageBody, $airSyncBaseType, $preview);
}
// truncate the body if needed
if ($truncateAt && $body_length > $truncateAt) {
$messageBody = mb_strcut($messageBody, 0, $truncateAt);
$body_length = strlen($messageBody);
$isTruncated = 1;
}
if ($isTruncated) {
$body_params['truncated'] = 1;
$body_params['estimatedDataSize'] = $real_length;
}
// add Body element to the result
$result['body'] = $this->setBody($messageBody, $body_params);
// original body type
// @TODO: get this value from getMessageBody()
$result['nativeBodyType'] = $message->has_html_part() ? 2 : 1;
// Message class
$result['messageClass'] = 'IPM.Note';
$result['contentClass'] = 'urn:content-classes:message';
if ($headers->ctype == 'multipart/signed'
&& !empty($message->parts[1])
&& $message->parts[1]->mimetype == 'application/pkcs7-signature'
) {
$result['messageClass'] = 'IPM.Note.SMIME.MultipartSigned';
}
else if ($headers->ctype == 'application/pkcs7-mime' || $headers->ctype == 'application/x-pkcs7-mime') {
$result['messageClass'] = 'IPM.Note.SMIME';
}
else if ($event = $this->get_invitation_event_from_message($message)) {
$result['messageClass'] = 'IPM.Schedule.Meeting.Request';
$result['contentClass'] = 'urn:content-classes:calendarmessage';
$meeting = array();
$meeting['allDayEvent'] = $event['allday'] ?? null ? 1 : 0;
$meeting['startTime'] = self::date_from_kolab($event['start']);
$meeting['dtStamp'] = self::date_from_kolab($event['dtstamp'] ?? null);
$meeting['endTime'] = self::date_from_kolab($event['end'] ?? null);
$meeting['location'] = $event['location'] ?? null;
$meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_NORMAL;
if (!empty($event['recurrence_date'])) {
$meeting['recurrenceId'] = self::date_from_kolab($event['recurrence_date']);
if (!empty($event['status']) && $event['status'] == 'CANCELLED') {
$meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_EXCEPTION;
} else {
$meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_SINGLE;
}
} else if (!empty($event['recurrence'])) {
$meeting['instanceType'] = Syncroton_Model_EmailMeetingRequest::TYPE_RECURRING_MASTER;
// TODO: MeetingRequest recurrence is different that the one in Calendar
// $this->recurrence_from_kolab($collection, $event, $meeting);
}
// Organizer
if (!empty($event['attendees'])) {
foreach ($event['attendees'] as $idx => $attendee) {
if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER' && !empty($attendee['email'])) {
$meeting['organizer'] = $attendee['email'];
break;
}
}
}
// Current time as a number of 100-nanosecond units since 1601-01-01
$fileTime = ($event['start']->getTimestamp() + 11644473600) * 10000000;
// Kolab Format 3.0 and xCal does support timezone per-date, but ActiveSync allows
// only one timezone per-event. We'll use timezone of the start date
$meeting['timeZone'] = kolab_sync_timezone_converter::encodeTimezoneFromDate($event['start']);
$meeting['globalObjId'] = self::encodeGlobalObjId([
'uid' => $event['uid'],
'year' => intval($event['start']->format('Y')),
'month' => intval($event['start']->format('n')),
'day' => intval($event['start']->format('j')),
'now' => $fileTime,
]);
// TODO handle other methods
if ($event['_method'] == 'REQUEST') {
$meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_REQUEST;
// Some clients (iOS) without this flag do not send the invitation reply to the organizer.
// Note: Microsoft says "the value of the ResponseRequested element comes from the PARTSTAT
// parameter value of "NEEDS-ACTION" in the request". I think it is safe to do this for all requests.
// Note: This does not have impact on the existence of Accept/Decline buttons in the client.
$meeting['responseRequested'] = 1;
} else {
$meeting['meetingMessageType'] = Syncroton_Model_EmailMeetingRequest::MESSAGE_TYPE_NORMAL;
}
// New time proposals aren't supported by Kolab.
// This disables the UI elements related to this on the client side
$meeting['disallowNewTimeProposal'] = 1;
$result['meetingRequest'] = new Syncroton_Model_EmailMeetingRequest($meeting);
}
// Categories (Tags)
if (isset($this->tag_categories) && $this->tag_categories) {
// convert kolab tags into categories
$result['categories'] = $this->backend->getCategories($message);
}
$is_ios = preg_match('/(iphone|ipad)/i', $this->device->devicetype);
// attachments
$attachments = array_merge($message->attachments, $message->inline_parts);
if (!empty($attachments)) {
$result['attachments'] = array();
foreach ($attachments as $attachment) {
$att = array();
if ($is_ios && !empty($event) && $attachment->mime_id == $event['_mime_id']) {
continue;
}
$filename = rcube_charset::clean($attachment->filename);
if (empty($filename) && $attachment->mimetype == 'text/html') {
$filename = 'HTML Part';
}
$att['displayName'] = $filename;
$att['fileReference'] = $serverId . '::' . $attachment->mime_id;
$att['method'] = 1;
$att['estimatedDataSize'] = $attachment->size;
if (!empty($attachment->content_id)) {
$att['contentId'] = rcube_charset::clean($attachment->content_id);
}
if (!empty($attachment->content_location)) {
$att['contentLocation'] = rcube_charset::clean($attachment->content_location);
}
if (in_array($attachment, $message->inline_parts)) {
$att['isInline'] = 1;
}
$result['attachments'][] = new Syncroton_Model_EmailAttachment($att);
}
}
return new Syncroton_Model_Email($result);
}
/**
* Returns properties of a message for Search response
*
* @param string $longId Message identifier
* @param array $options Search options
*
* @return Syncroton_Model_Email Email object
*/
public function getSearchEntry($longId, $options)
{
$collection = new Syncroton_Model_SyncCollection(array(
'options' => $options,
));
return $this->getEntry($collection, $longId);
}
/**
* convert contact from xml to libkolab array
*
* @param Syncroton_Model_IEntry $data Contact to convert
* @param string $folderid Folder identifier
* @param array $entry Existing entry
*
* @return array
*/
public function toKolab(Syncroton_Model_IEntry $data, $folderid, $entry = null)
{
// does nothing => you can't add emails via ActiveSync
}
/**
* Returns filter query array according to specified ActiveSync FilterType
*
* @param int $filter_type Filter type
*
* @param array Filter query
*/
protected function filter($filter_type = 0)
{
$filter = array();
switch ($filter_type) {
case Syncroton_Command_Sync::FILTER_1_DAY_BACK:
$mod = '-1 day';
break;
case Syncroton_Command_Sync::FILTER_3_DAYS_BACK:
$mod = '-3 days';
break;
case Syncroton_Command_Sync::FILTER_1_WEEK_BACK:
$mod = '-1 week';
break;
case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
$mod = '-2 weeks';
break;
case Syncroton_Command_Sync::FILTER_1_MONTH_BACK:
$mod = '-1 month';
break;
}
if (!empty($mod)) {
$dt = new DateTime('now', new DateTimeZone('UTC'));
$dt->modify($mod);
// RFC3501: IMAP SEARCH
$filter[] = 'SINCE ' . $dt->format('d-M-Y');
}
return $filter;
}
/**
* Return list of supported folders for this backend
*
* @return array
*/
public function getAllFolders()
{
$list = $this->listFolders();
if (!is_array($list)) {
throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR);
}
// device doesn't support multiple folders
if (!$this->isMultiFolder()) {
// We'll return max. one folder of supported type
$result = array();
$types = $this->folder_types;
foreach ($list as $idx => $folder) {
$type = $folder['type'] == 12 ? 2 : $folder['type']; // unknown to Inbox
if ($folder_id = $types[$type]) {
$result[$folder_id] = array(
'displayName' => $folder_id,
'serverId' => $folder_id,
'parentId' => 0,
'type' => $type,
);
}
}
$list = $result;
}
foreach ($list as $idx => $folder) {
$list[$idx] = new Syncroton_Model_Folder($folder);
}
return $list;
}
/**
* Return list of folders for specified folder ID
*
+ * @param string $folder_id Folder identifier
+ *
* @return array Folder identifiers list
*/
protected function extractFolders($folder_id)
{
$list = $this->listFolders();
$result = array();
if (!is_array($list)) {
throw new Syncroton_Exception_NotFound('Folder not found');
}
// device supports multiple folders?
if ($this->isMultiFolder()) {
if ($list[$folder_id]) {
$result[] = $folder_id;
}
}
else if ($type = array_search($folder_id, $this->folder_types)) {
foreach ($list as $id => $folder) {
if ($folder['type'] == $type || ($folder_id == 'Inbox' && $folder['type'] == 12)) {
$result[] = $id;
}
}
}
if (empty($result)) {
throw new Syncroton_Exception_NotFound('Folder not found');
}
return $result;
}
/**
* Moves object into another location (folder)
*
* @param string $srcFolderId Source folder identifier
* @param string $serverId Object identifier
* @param string $dstFolderId Destination folder identifier
*
* @throws Syncroton_Exception_Status
* @return string New object identifier
*/
public function moveItem($srcFolderId, $serverId, $dstFolderId)
{
$msg = $this->parseMessageId($serverId);
$dest = $this->extractFolders($dstFolderId);
$dest_id = array_shift($dest);
$dest_name = $this->backend->folder_id2name($dest_id, $this->device->deviceid);
if (empty($msg)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
if ($dest_name === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if (!$this->storage->move_message($msg['uid'], $dest_name, $msg['foldername'])) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
// Use COPYUID feature (RFC2359) to get the new UID of the copied message
$copyuid = isset($this->storage->conn->data['COPYUID']) ? $this->storage->conn->data['COPYUID'] : null;
if (is_array($copyuid) && ($uid = $copyuid[1])) {
return $this->serverId($uid, $dest_id);
}
}
/**
* add entry from xml data
*
* @param string $folderId Folder identifier
* @param Syncroton_Model_IEntry $entry Entry
*
* @return string ID of the created entry
*/
public function createEntry($folderId, Syncroton_Model_IEntry $entry)
{
// Creating emails is not normally supported like this, but is implemented for testing purposes
$foldername = $this->backend->folder_id2name($folderId, $this->device->deviceid);
$flag = !empty($entry->read) ? 'SEEN' : 'UNSEEN';
$uid = $this->storage->save_message($foldername, $entry->body->data, '', false, [$flag]);
if (!$uid) {
$this->logger->error("Error while storing the message " . $this->storage->get_error_str());
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
return $this->serverId($uid, $folderId);
}
/**
* Update existing message
*
* @param string $folderId Folder identifier
* @param string $serverId Entry identifier
* @param Syncroton_Model_IEntry $entry Entry
*/
public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
{
$msg = $this->parseMessageId($serverId);
if (empty($msg)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
if (isset($entry->categories)) {
// Read the message headers only when they are needed
$message = $this->getObject($serverId);
if (empty($message)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
}
// Read status change
if (isset($entry->read)) {
// here we update only Read flag
$flag = !empty($entry->read) ? 'SEEN' : 'UNSEEN';
$this->storage->set_flag($msg['uid'], $flag, $msg['foldername']);
}
// Flag change
if (isset($entry->flag)) {
if (empty($entry->flag) || empty($entry->flag->flagType)) {
$this->storage->set_flag($msg['uid'], 'UNFLAGGED', $msg['foldername']);
}
else if (preg_match('/follow\s*up/i', $entry->flag->flagType)) {
$this->storage->set_flag($msg['uid'], 'FLAGGED', $msg['foldername']);
}
}
// Categories (Tags) change
if (isset($entry->categories)) {
$this->backend->setCategories($message, $entry->categories);
}
}
/**
* Delete an email (or move to Trash)
*
* @param string $folderId
* @param string $serverId
* @param Syncroton_Model_SyncCollection $collection
*/
public function deleteEntry($folderId, $serverId, $collection)
{
$trash = kolab_sync::get_instance()->config->get('trash_mbox');
$msg = $this->parseMessageId($serverId);
if (empty($msg)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
// Note: If DeletesAsMoves is not specified in the request, its default is 1 (true).
// move message to trash folder
if ((!isset($collection->deletesAsMoves) || !empty($collection->deletesAsMoves))
&& strlen($trash)
&& $trash != $msg['foldername']
&& $this->storage->folder_exists($trash)
) {
$this->storage->move_message($msg['uid'], $trash, $msg['foldername']);
}
// delete the message
else {
// According to the ActiveSync spec. "If the DeletesAsMoves element is set to false,
// the deletion is PERMANENT.", therefore we delete the message, and not flag as deleted.
$this->storage->delete_message($msg['uid'], $msg['foldername']);
// FIXME: We could consider acting according to the 'flag_for_deletion' setting.
// Don't forget about 'read_when_deleted' setting then.
// $this->storage->set_flag($msg['uid'], 'DELETED', $msg['foldername']);
// $this->storage->set_flag($msg['uid'], 'SEEN', $msg['foldername']);
}
}
/**
* Send an email
*
* @param mixed $message MIME message
* @param boolean $saveInSent Enables saving the sent message in Sent folder
*
* @throws Syncroton_Exception_Status
*/
public function sendEmail($message, $saveInSent)
{
if (!($message instanceof kolab_sync_message)) {
$message = new kolab_sync_message($message);
}
$sent = $message->send($smtp_error);
if (!$sent) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::MAIL_SUBMISSION_FAILED);
}
// Save sent message in Sent folder
if ($saveInSent) {
$sent_folder = kolab_sync::get_instance()->config->get('sent_mbox');
if (strlen($sent_folder) && $this->storage->folder_exists($sent_folder)) {
return $this->storage->save_message($sent_folder, $message->source(), '', false, array('SEEN'));
}
}
}
/**
* Forward an email
*
* @param array|string $itemId A string LongId or an array with following properties:
* collectionId, itemId and instanceId
* @param resource|string $body MIME message
* @param boolean $saveInSent Enables saving the sent message in Sent folder
* @param boolean $replaceMime If enabled, original message would be appended
*
* @throws Syncroton_Exception_Status
*/
public function forwardEmail($itemId, $body, $saveInSent, $replaceMime)
{
/*
@TODO:
The SmartForward command can be applied to a meeting. When SmartForward is applied to a recurring meeting,
the InstanceId element (section 2.2.3.83.2) specifies the ID of a particular occurrence in the recurring meeting.
If SmartForward is applied to a recurring meeting and the InstanceId element is absent, the server SHOULD
forward the entire recurring meeting. If the value of the InstanceId element is invalid, the server responds
with Status element (section 2.2.3.162.15) value 104, as specified in section 2.2.4.
When the SmartForward command is used for an appointment, the original message is included by the server
as an attachment to the outgoing message. When the SmartForward command is used for a normal message
or a meeting, the behavior of the SmartForward command is the same as that of the SmartReply command (section 2.2.2.18).
*/
$msg = $this->parseMessageId($itemId);
$message = $this->getObject($itemId);
if (empty($message)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
}
// Parse message
$sync_msg = new kolab_sync_message($body);
// forward original message as attachment
if (!$replaceMime) {
$this->storage->set_folder($msg['foldername']);
$attachment = $this->storage->get_raw_body($msg['uid']);
if (empty($attachment)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
}
$sync_msg->add_attachment($attachment, array(
'encoding' => '8bit',
'content_type' => 'message/rfc822',
'disposition' => 'inline',
//'name' => 'message.eml',
));
}
// Send message
$this->sendEmail($sync_msg, $saveInSent);
// Set FORWARDED flag on the replied message
if (empty($message->headers->flags['FORWARDED'])) {
$this->storage->set_flag($msg['uid'], 'FORWARDED', $msg['foldername']);
}
}
/**
* Reply to an email
*
* @param array|string $itemId A string LongId or an array with following properties:
* collectionId, itemId and instanceId
* @param resource|string $body MIME message
* @param boolean $saveInSent Enables saving the sent message in Sent folder
* @param boolean $replaceMime If enabled, original message would be appended
*
* @throws Syncroton_Exception_Status
*/
public function replyEmail($itemId, $body, $saveInSent, $replaceMime)
{
$msg = $this->parseMessageId($itemId);
$message = $this->getObject($itemId);
if (empty($message)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
}
$sync_msg = new kolab_sync_message($body);
$headers = $sync_msg->headers();
// Add References header
if (empty($headers['References'])) {
$sync_msg->set_header('References', trim($message->headers->references . ' ' . $message->headers->messageID));
}
// Get original message body
if (!$replaceMime) {
// @TODO: here we're assuming that reply message is in text/plain format
// So, original message will be converted to plain text if needed
$message_body = $this->getMessageBody($message, false);
// Quote original message body
$message_body = self::wrap_and_quote(trim($message_body), 72);
// Join bodies
$sync_msg->append("\n" . ltrim($message_body));
}
// Send message
$this->sendEmail($sync_msg, $saveInSent);
// Set ANSWERED flag on the replied message
if (empty($message->headers->flags['ANSWERED'])) {
$this->storage->set_flag($msg['uid'], 'ANSWERED', $msg['foldername']);
}
}
/**
* ActiveSync Search handler
*
* @param Syncroton_Model_StoreRequest $store Search query
*
* @return Syncroton_Model_StoreResponse Complete Search response
*/
public function search(Syncroton_Model_StoreRequest $store)
{
list($folders, $search_str) = $this->parse_search_query($store);
if (empty($search_str)) {
throw new Exception('Empty/invalid search request');
}
if (!is_array($folders)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = array();
// @TODO: caching with Options->RebuildResults support
foreach ($folders as $folderid) {
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
if ($foldername === null) {
continue;
}
// $this->storage->set_folder($foldername);
// $this->storage->folder_sync($foldername);
$search = $this->storage->search_once($foldername, $search_str);
if (!($search instanceof rcube_result_index)) {
continue;
}
$uids = $search->get();
foreach ($uids as $idx => $uid) {
$uids[$idx] = new Syncroton_Model_StoreResponseResult(array(
'longId' => $this->serverId($uid, $folderid),
'collectionId' => $folderid,
'class' => 'Email',
));
}
$result = array_merge($result, $uids);
// We don't want to search all folders if we've got already a lot messages
if (count($result) >= self::MAX_SEARCH_RESULT) {
break;
}
}
$result = array_values($result);
$response = new Syncroton_Model_StoreResponse();
// Calculate requested range
$start = (int) $store->options['range'][0];
$limit = (int) $store->options['range'][1] + 1;
$total = count($result);
$response->total = $total;
// Get requested chunk of data set
if ($total) {
if ($start > $total) {
$start = $total;
}
if ($limit > $total) {
$limit = max($start+1, $total);
}
if ($start > 0 || $limit < $total) {
$result = array_slice($result, $start, $limit-$start);
}
$response->range = array($start, $start + count($result) - 1);
}
// Build result array, convert to ActiveSync format
foreach ($result as $idx => $rec) {
$rec->properties = $this->getSearchEntry($rec->longId, $store->options);
$response->result[] = $rec;
unset($result[$idx]);
}
return $response;
}
/**
* Converts ActiveSync search parameters into IMAP search string
*/
protected function parse_search_query($store)
{
$options = $store->options;
$query = $store->query;
$search_str = '';
$folders = array();
if (empty($query) || !is_array($query)) {
return array();
}
if (isset($query['and']['freeText']) && strlen($query['and']['freeText'])) {
$search = $query['and']['freeText'];
}
if (!empty($query['and']['collections'])) {
foreach ($query['and']['collections'] as $collection) {
$folders = array_merge($folders, $this->extractFolders($collection));
}
}
if (!empty($query['and']['greaterThan'])
&& !empty($query['and']['greaterThan']['dateReceived'])
&& !empty($query['and']['greaterThan']['value'])
) {
$search_str .= ' SINCE ' . $query['and']['greaterThan']['value']->format('d-M-Y');
}
if (!empty($query['and']['lessThan'])
&& !empty($query['and']['lessThan']['dateReceived'])
&& !empty($query['and']['lessThan']['value'])
) {
$search_str .= ' BEFORE ' . $query['and']['lessThan']['value']->format('d-M-Y');
}
if ($search !== null) {
// @FIXME: should we use TEXT/BODY search?
// ActiveSync protocol specification says "indexed fields"
$search_keys = array('SUBJECT', 'TO', 'FROM', 'CC');
$search_str .= str_repeat(' OR', count($search_keys)-1);
foreach ($search_keys as $key) {
$search_str .= sprintf(" %s {%d}\r\n%s", $key, strlen($search), $search);
}
}
if (empty($search_str)) {
return array();
}
$search_str = 'ALL UNDELETED ' . trim($search_str);
// @TODO: DeepTraversal
if (empty($folders)) {
$folders = $this->listFolders();
if (!is_array($folders)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$folders = array_keys($folders);
}
return array($folders, $search_str);
}
/**
* Fetches the entry from the backend
*/
protected function getObject($entryid, $dummy = null)
{
$message = $this->parseMessageId($entryid);
if (empty($message)) {
// @TODO: exception?
return null;
}
// get message
$message = new rcube_message($message['uid'], $message['foldername']);
return $message && !empty($message->headers) ? $message : null;
}
/**
* @return Syncroton_Model_FileReference
*/
public function getFileReference($fileReference)
{
list($folderid, $uid, $part_id) = explode('::', $fileReference);
$message = $this->getObject($fileReference);
if (!$message) {
throw new Syncroton_Exception_NotFound('Message not found');
}
$part = $message->mime_parts[$part_id];
$body = $message->get_part_body($part_id);
return new Syncroton_Model_FileReference(array(
'contentType' => $part->mimetype,
'data' => $body,
));
}
/**
* Parses entry ID to get folder name and UID of the message
*/
protected function parseMessageId($entryid)
{
// replyEmail/forwardEmail
if (is_array($entryid)) {
$entryid = $entryid['itemId'];
}
if (!is_string($entryid) || !strpos($entryid, '::')) {
return;
}
// Note: the id might be in a form of <folder>::<uid>[::<part_id>]
list($folderid, $uid) = explode('::', $entryid);
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
if ($foldername === null || $foldername === false) {
return;
}
return array(
'uid' => $uid,
'folderid' => $folderid,
'foldername' => $foldername,
);
}
/**
* Creates entry ID of the message
*/
protected function serverId($uid, $folderid)
{
return $folderid . '::' . $uid;
}
/**
* Returns body of the message in specified format
*/
protected function getMessageBody($message, $html = false)
{
if (!is_array($message->parts) && empty($message->body)) {
return '';
}
if (!empty($message->parts)) {
foreach ($message->parts as $part) {
// skip no-content and attachment parts (#1488557)
if ($part->type != 'content' || !$part->size || $message->is_attachment($part)) {
continue;
}
return $this->getMessagePartBody($message, $part, $html);
}
}
return $this->getMessagePartBody($message, $message, $html);
}
/**
* Returns body of the message part in specified format
*/
protected function getMessagePartBody($message, $part, $html = false)
{
if (empty($part->size) || !isset($part->mime_id)) {
// TODO: Throw an exception?
return '';
}
// Check if we have enough memory to handle the message in it
$body = $this->message_mem_check($message, $part->size, false);
if ($body !== false) {
$body = $message->get_part_body($part->mime_id, true);
}
// message is cached but not exists, or other error
if ($body === false) {
return '';
}
$ctype_secondary = !empty($part->ctype_secondary) ? $part->ctype_secondary : null;
if ($html) {
if ($ctype_secondary == 'html') {
// charset was converted to UTF-8 in rcube_storage::get_message_part(),
// change/add charset specification in HTML accordingly
$meta = '<meta http-equiv="Content-Type" content="text/html; charset='.RCUBE_CHARSET.'" />';
// remove old meta tag and add the new one, making sure
// that it is placed in the head
$body = preg_replace('/<meta[^>]+charset=[a-z0-9-_]+[^>]*>/Ui', '', $body);
$body = preg_replace('/(<head[^>]*>)/Ui', '\\1'.$meta, $body, -1, $rcount);
if (!$rcount) {
$body = '<head>' . $meta . '</head>' . $body;
}
}
else if ($ctype_secondary == 'enriched') {
$body = rcube_enriched::to_html($body);
}
else {
// Roundcube >= 1.2
if (class_exists('rcube_text2html')) {
$flowed = isset($part->ctype_parameters['format']) && $part->ctype_parameters['format'] == 'flowed';
$delsp = isset($part->ctype_parameters['delsp']) && $part->ctype_parameters['delsp'] == 'yes';
$options = array('flowed' => $flowed, 'wrap' => false, 'delsp' => $delsp);
$text2html = new rcube_text2html($body, false, $options);
$body = '<html><body>' . $text2html->get_html() . '</body></html>';
}
else {
$body = '<html><body><pre>' . $body . '</pre></body></html>';
}
}
}
else {
if ($ctype_secondary == 'enriched') {
$body = rcube_enriched::to_html($body);
$part->ctype_secondary = 'html';
}
if ($ctype_secondary == 'html') {
$txt = new rcube_html2text($body, false, true);
$body = $txt->get_text();
}
else {
if ($ctype_secondary == 'plain'
&& !empty($part->ctype_parameters['format'])
&& $part->ctype_parameters['format'] == 'flowed'
) {
$body = rcube_mime::unfold_flowed($body);
}
}
}
return $body;
}
/**
* Converts and truncates message body for use in <Preview>
*
* @return string Truncated plain text message
*/
protected function getPreview($body, $type, $size)
{
if ($type == Syncroton_Command_Sync::BODY_TYPE_HTML) {
$txt = new rcube_html2text($body, false, true);
$body = $txt->get_text();
}
// size limit defined in ActiveSync protocol
if ($size > 255) {
$size = 255;
}
return mb_strcut(trim($body), 0, $size);
}
public static function charset_to_cp($charset)
{
// @TODO: ?????
// The body is converted to utf-8 in get_part_body(), what about headers?
return 65001; // UTF-8
$aliases = array(
'asmo708' => 708,
'shiftjis' => 932,
'gb2312' => 936,
'ksc56011987' => 949,
'big5' => 950,
'utf16' => 1200,
'utf16le' => 1200,
'unicodefffe' => 1201,
'utf16be' => 1201,
'johab' => 1361,
'macintosh' => 10000,
'macjapanese' => 10001,
'macchinesetrad' => 10002,
'mackorean' => 10003,
'macarabic' => 10004,
'machebrew' => 10005,
'macgreek' => 10006,
'maccyrillic' => 10007,
'macchinesesimp' => 10008,
'macromanian' => 10010,
'macukrainian' => 10017,
'macthai' => 10021,
'macce' => 10029,
'macicelandic' => 10079,
'macturkish' => 10081,
'maccroatian' => 10082,
'utf32' => 12000,
'utf32be' => 12001,
'chinesecns' => 20000,
'chineseeten' => 20002,
'ia5' => 20105,
'ia5german' => 20106,
'ia5swedish' => 20107,
'ia5norwegian' => 20108,
'usascii' => 20127,
'ibm273' => 20273,
'ibm277' => 20277,
'ibm278' => 20278,
'ibm280' => 20280,
'ibm284' => 20284,
'ibm285' => 20285,
'ibm290' => 20290,
'ibm297' => 20297,
'ibm420' => 20420,
'ibm423' => 20423,
'ibm424' => 20424,
'ebcdickoreanextended' => 20833,
'ibmthai' => 20838,
'koi8r' => 20866,
'ibm871' => 20871,
'ibm880' => 20880,
'ibm905' => 20905,
'ibm00924' => 20924,
'cp1025' => 21025,
'koi8u' => 21866,
'iso88591' => 28591,
'iso88592' => 28592,
'iso88593' => 28593,
'iso88594' => 28594,
'iso88595' => 28595,
'iso88596' => 28596,
'iso88597' => 28597,
'iso88598' => 28598,
'iso88599' => 28599,
'iso885913' => 28603,
'iso885915' => 28605,
'xeuropa' => 29001,
'iso88598i' => 38598,
'iso2022jp' => 50220,
'csiso2022jp' => 50221,
'iso2022jp' => 50222,
'iso2022kr' => 50225,
'eucjp' => 51932,
'euccn' => 51936,
'euckr' => 51949,
'hzgb2312' => 52936,
'gb18030' => 54936,
'isciide' => 57002,
'isciibe' => 57003,
'isciita' => 57004,
'isciite' => 57005,
'isciias' => 57006,
'isciior' => 57007,
'isciika' => 57008,
'isciima' => 57009,
'isciigu' => 57010,
'isciipa' => 57011,
'utf7' => 65000,
'utf8' => 65001,
);
$charset = strtolower($charset);
$charset = preg_replace(array('/^x-/', '/[^a-z0-9]/'), '', $charset);
if (isset($aliases[$charset])) {
return $aliases[$charset];
}
if (preg_match('/^(ibm|dos|cp|windows|win)[0-9]+/', $charset, $m)) {
return substr($charset, strlen($m[1]) + 1);
}
}
/**
* Wrap text to a given number of characters per line
* but respect the mail quotation of replies messages (>).
* Finally add another quotation level by prepending the lines
* with >
*
* @param string $text Text to wrap
* @param int $length The line width
*
* @return string The wrapped text
*/
protected static function wrap_and_quote($text, $length = 72)
{
// Function stolen from Roundcube ;)
// Rebuild the message body with a maximum of $max chars, while keeping quoted message.
$max = min(77, $length + 8);
$lines = preg_split('/\r?\n/', trim($text));
$out = '';
foreach ($lines as $line) {
// don't wrap already quoted lines
if (isset($line[0]) && $line[0] == '>') {
$line = '>' . rtrim($line);
}
else if (mb_strlen($line) > $max) {
$newline = '';
foreach (explode("\n", rcube_mime::wordwrap($line, $length - 2)) as $l) {
if (strlen($l)) {
$newline .= '> ' . $l . "\n";
}
else {
$newline .= ">\n";
}
}
$line = rtrim($newline);
}
else {
$line = '> ' . $line;
}
// Append the line
$out .= $line . "\n";
}
return $out;
}
/**
* Returns calendar event data from the iTip invitation attached to a mail message
*/
public function get_invitation_event_from_message($message)
{
// Parse the message and find iTip attachments
$libcal = libcalendaring::get_instance();
$libcal->mail_message_load(array('object' => $message));
$ical_objects = $libcal->get_mail_ical_objects();
// We support only one event in the iTip
foreach ($ical_objects as $mime_id => $event) {
if ($event['_type'] == 'event') {
$event['_method'] = $ical_objects->method;
$event['_mime_id'] = $ical_objects->mime_id;
return $event;
}
}
return null;
}
/**
* Returns calendar event data from the iTip invitation attached to a mail message
*/
public function get_invitation_event($messageId)
{
// Get the mail message object
if ($message = $this->getObject($messageId)) {
return $this->get_invitation_event_from_message($message);
}
return null;
}
private function mem_check($need)
{
$mem_limit = parse_bytes(ini_get('memory_limit'));
$memory = static::$memory_accumulated;
return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true;
}
/**
* Checks if the message can be processed, depending on its size and
* memory_limit, otherwise throws an exception or returns fake body.
*/
protected function message_mem_check($message, $size, $result = null)
{
static $memory_rised;
// @FIXME: we need up to 5x more memory than the body
// Note: Biggest memory multiplication happens in recode_message()
// and the Syncroton engine (which also does not support passing bodies
// as streams). It also happens when parsing the plain/html text body
// in getMessagePartBody() though the footprint there is probably lower.
if (!$this->mem_check($size * 5)) {
// If we already rised the memory we throw an exception, so the message
// will be synchronized in the next run (then we might have enough memory)
if ($memory_rised) {
throw new Syncroton_Exception_MemoryExhausted;
}
$memory_rised = true;
$memory_max = 512; // maximum in MB
$memory_limit = round(parse_bytes(ini_get('memory_limit')) / 1024 / 1024); // current limit (in MB)
$memory_add = round($size * 5 / 1024 / 1024); // how much we need (in MB)
$memory_needed = min($memory_limit + $memory_add, $memory_max) . "M";
if ($memory_limit < $memory_max) {
$this->logger->debug("Setting memory_limit=$memory_needed");
if (ini_set('memory_limit', $memory_needed) !== false) {
// Memory has been rised, check again
if ($this->mem_check($size * 5)) {
return;
}
}
}
$this->logger->warn("Not enough memory. Using fake email body.");
if ($result !== null) {
return $result;
}
// Let's return a fake message. If we return an empty body Outlook
// will not list the message at all. This way user can do something
// with the message (flag, delete, move) and see the reason why it's fake
// and importantly see its subject, sender, etc.
// TODO: Localization?
$msg = "This message is too large for ActiveSync.";
// $msg .= "See https://kb.kolabenterprise.com/documentation/some-place for more information.";
// Get original message headers
$headers = $this->storage->get_raw_headers($message->uid);
// Build a fake message with original headers, but changed body
return kolab_sync_message::fake_message($headers, $msg);
}
}
}
diff --git a/lib/kolab_sync_storage.php b/lib/kolab_sync_storage.php
index 7a1c5c9..ad069f2 100644
--- a/lib/kolab_sync_storage.php
+++ b/lib/kolab_sync_storage.php
@@ -1,1827 +1,1807 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Storage handling class with basic Kolab support (everything stored in IMAP)
*/
class kolab_sync_storage
{
const INIT_SUB_PERSONAL = 1; // all subscribed folders in personal namespace
const INIT_ALL_PERSONAL = 2; // all folders in personal namespace
const INIT_SUB_OTHER = 4; // all subscribed folders in other users namespace
const INIT_ALL_OTHER = 8; // all folders in other users namespace
const INIT_SUB_SHARED = 16; // all subscribed folders in shared namespace
const INIT_ALL_SHARED = 32; // all folders in shared namespace
const MODEL_CALENDAR = 'event';
const MODEL_CONTACTS = 'contact';
const MODEL_EMAIL = 'mail';
const MODEL_NOTES = 'note';
const MODEL_TASKS = 'task';
const ROOT_MAILBOX = 'INBOX';
const ASYNC_KEY = '/private/vendor/kolab/activesync';
const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
public $syncTimeStamp;
protected $storage;
protected $folder_meta;
protected $folder_uids;
protected $folders = [];
protected $root_meta;
protected $relationSupport = true;
protected $tag_rts = [];
protected static $instance;
protected static $types = [
1 => '',
2 => 'mail.inbox',
3 => 'mail.drafts',
4 => 'mail.wastebasket',
5 => 'mail.sentitems',
6 => 'mail.outbox',
7 => 'task.default',
8 => 'event.default',
9 => 'contact.default',
10 => 'note.default',
11 => 'journal.default',
12 => 'mail',
13 => 'event',
14 => 'contact',
15 => 'task',
16 => 'journal',
17 => 'note',
];
- protected static $classes = [
- Syncroton_Data_Factory::CLASS_CALENDAR => self::MODEL_CALENDAR,
- Syncroton_Data_Factory::CLASS_CONTACTS => self::MODEL_CONTACTS,
- Syncroton_Data_Factory::CLASS_EMAIL => self::MODEL_EMAIL,
- Syncroton_Data_Factory::CLASS_NOTES => self::MODEL_NOTES,
- Syncroton_Data_Factory::CLASS_TASKS => self::MODEL_TASKS,
- ];
-
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync_storage The one and only instance
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_sync_storage;
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
-
/**
* Class initialization
*/
public function startup()
{
$this->storage = kolab_sync::get_instance()->get_storage();
// @TODO: reset cache? if we do this for every request the cache would be useless
// There's no session here
//$this->storage->clear_cache('mailboxes.', true);
// set additional header used by libkolab
$this->storage->set_options([
// @TODO: there can be Roundcube plugins defining additional headers,
// we maybe would need to add them here
'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION',
'skip_deleted' => true,
'threading' => false,
]);
// Disable paging
$this->storage->set_pagesize(999999);
}
/**
* Clear internal cache state
*/
public function reset()
{
$this->folders = [];
}
/**
* List known devices
*
* @return array Device list as hash array
*/
public function devices_list()
{
if ($this->root_meta === null) {
// @TODO: consider server annotation instead of INBOX
if ($meta = $this->storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) {
$this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]);
}
else {
$this->root_meta = [];
}
}
if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) {
return $this->root_meta['DEVICE'];
}
return [];
}
-
/**
* Get list of folders available for sync
*
* @param string $deviceid Device identifier
* @param string $type Folder type
* @param bool $flat_mode Enables flat-list mode
*
* @return array|bool List of mailbox folders, False on backend failure
*/
public function folders_list($deviceid, $type, $flat_mode = false)
{
// get all folders of specified type
$folders = kolab_storage::list_folders('', '*', $type, false, $typedata);
// get folders activesync config
$folderdata = $this->folder_meta();
if (!is_array($folders) || !is_array($folderdata)) {
return false;
}
$folders_list = [];
// check if folders are "subscribed" for activesync
foreach ($folderdata as $folder => $meta) {
if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
|| empty($meta['FOLDER'][$deviceid]['S'])
) {
continue;
}
// force numeric folder name to be a string (T1283)
$folder = (string) $folder;
if (!empty($type) && !in_array($folder, $folders)) {
continue;
}
// Activesync folder identifier (serverId)
$folder_type = !empty($typedata[$folder]) ? $typedata[$folder] : 'mail';
$folder_id = $this->folder_id($folder, $folder_type);
$folders_list[$folder_id] = $this->folder_data($folder, $folder_type);
}
if ($flat_mode) {
$folders_list = $this->folders_list_flat($folders_list, $type, $typedata);
}
return $folders_list;
}
-
/**
* Converts list of folders to a "flat" list
*/
protected function folders_list_flat($folders, $type, $typedata)
{
$delim = $this->storage->get_hierarchy_delimiter();
foreach ($folders as $idx => $folder) {
if ($folder['parentId']) {
// for non-mail folders we make the list completely flat
if ($type != self::MODEL_EMAIL) {
$display_name = kolab_storage::object_name($folder['imap_name']);
$display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
$folders[$idx]['parentId'] = 0;
$folders[$idx]['displayName'] = $display_name;
}
// for mail folders we modify only folders with non-existing parents
else if (!isset($folders[$folder['parentId']])) {
$items = explode($delim, $folder['imap_name']);
$parent = 0;
// find existing parent
while (count($items) > 0) {
array_pop($items);
$parent_name = implode($delim, $items);
$parent_type = !empty($typedata[$parent_name]) ? $typedata[$parent_name] : 'mail';
$parent_id = $this->folder_id($parent_name, $parent_type);
if (isset($folders[$parent_id])) {
$parent = $parent_id;
break;
}
}
if (!$parent) {
$display_name = kolab_storage::object_name($folder['imap_name']);
$display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
}
else {
$parent_name = $folders[$parent_id]['imap_name'];
$display_name = substr($folder['imap_name'], strlen($parent_name)+1);
$display_name = rcube_charset::convert($display_name, 'UTF7-IMAP');
$display_name = str_replace($delim, ' » ', $display_name);
}
$folders[$idx]['parentId'] = $parent;
$folders[$idx]['displayName'] = $display_name;
}
}
}
return $folders;
}
-
/**
* Getter for folder metadata
*
* @return array|bool Hash array with meta data for each folder, False on backend failure
*/
- public function folder_meta()
+ protected function folder_meta()
{
if (!isset($this->folder_meta)) {
// get folders activesync config
$folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY);
if (!is_array($folderdata)) {
return $this->folder_meta = false;
}
$this->folder_meta = [];
foreach ($folderdata as $folder => $meta) {
if ($asyncdata = $meta[self::ASYNC_KEY]) {
if ($metadata = $this->unserialize_metadata($asyncdata)) {
$this->folder_meta[$folder] = $metadata;
}
}
}
}
return $this->folder_meta;
}
-
/**
* Creates folder and subscribes to the device
*
* @param string $name Folder name (UTF8)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
* @param ?string $parentid Parent folder id identifier
*
* @return string|false New folder identifier on success, False on failure
*/
public function folder_create($name, $type, $deviceid, $parentid = null)
{
$parent = null;
$name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parentid) {
$parent = $this->folder_id2name($parentid, $deviceid);
if ($parent === null) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND);
}
}
if ($parent !== null) {
$delim = $this->storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
if ($this->storage->folder_exists($name)) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS);
}
$type = self::type_activesync2kolab($type);
$created = kolab_storage::folder_create($name, $type, true);
if ($created) {
// Set ActiveSync subscription flag
$this->folder_set($name, $deviceid, 1);
return $this->folder_id($name, $type);
}
// Special case when client tries to create a subfolder of INBOX
// which is not possible on Cyrus-IMAP (T2223)
if ($parent === 'INBOX' && stripos($this->last_error(), 'invalid') !== false) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER);
}
return false;
}
-
/**
* Renames a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $new_name New folder name (UTF8)
* @param ?string $parentid Folder parent identifier
*
* @return bool True on success, False on failure
*/
public function folder_rename($folderid, $deviceid, $new_name, $parentid)
{
$old_name = $this->folder_id2name($folderid, $deviceid);
if ($parentid) {
$parent = $this->folder_id2name($parentid, $deviceid);
}
$name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if (isset($parent)) {
$delim = $this->storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
// Rename/move IMAP folder
if ($name === $old_name) {
return true;
}
$this->folder_meta = null;
// TODO: folder type change?
$type = kolab_storage::folder_type($old_name);
// don't use kolab_storage for moving mail folders
if (preg_match('/^mail/', $type)) {
return $this->storage->rename_folder($old_name, $name);
}
else {
return kolab_storage::folder_rename($old_name, $name);
}
}
/**
* Deletes folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
*
* @param bool True on success, False otherwise
*/
public function folder_delete($folderid, $deviceid)
{
$name = $this->folder_id2name($folderid, $deviceid);
$type = kolab_storage::folder_type($name);
unset($this->folder_meta[$name]);
// don't use kolab_storage for deleting mail folders
if (preg_match('/^mail/', $type)) {
return $this->storage->delete_folder($name);
}
return kolab_storage::folder_delete($name);
}
/**
* Deletes contents of a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param bool $recursive Apply to the folder and its subfolders
*
* @param bool True on success, False otherwise
*/
public function folder_empty($folderid, $deviceid, $recursive = false)
{
$foldername = $this->folder_id2name($folderid, $deviceid);
// Remove all entries
if (!$this->storage->clear_folder($foldername)) {
return false;
}
// Remove subfolders
if ($recursive) {
$delim = $this->storage->get_hierarchy_delimiter();
$folderdata = $this->folder_meta();
if (!is_array($folderdata)) {
return false;
}
foreach ($folderdata as $subfolder => $meta) {
if (!empty($meta['FOLDER'][$deviceid]['S']) && strpos((string) $subfolder, $foldername . $delim)) {
if (!$this->storage->clear_folder((string) $subfolder)) {
return false;
}
}
}
}
return true;
}
/**
* Sets ActiveSync subscription flag on a folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $deviceid Device identifier
* @param int $flag Flag value (0|1|2)
+ *
+ * @return bool True on success, False on failure
*/
- public function folder_set($name, $deviceid, $flag)
+ protected function folder_set($name, $deviceid, $flag)
{
if (empty($deviceid)) {
return false;
}
// get folders activesync config
$metadata = $this->folder_meta();
if (!is_array($metadata)) {
return false;
}
$metadata = isset($metadata[$name]) ? $metadata[$name] : [];
if ($flag) {
if (empty($metadata)) {
$metadata = [];
}
if (empty($metadata['FOLDER'])) {
$metadata['FOLDER'] = [];
}
if (empty($metadata['FOLDER'][$deviceid])) {
$metadata['FOLDER'][$deviceid] = [];
}
// Z-Push uses:
// 1 - synchronize, no alarms
// 2 - synchronize with alarms
$metadata['FOLDER'][$deviceid]['S'] = $flag;
}
else {
unset($metadata['FOLDER'][$deviceid]['S']);
if (empty($metadata['FOLDER'][$deviceid])) {
unset($metadata['FOLDER'][$deviceid]);
}
if (empty($metadata['FOLDER'])) {
unset($metadata['FOLDER']);
}
if (empty($metadata)) {
$metadata = null;
}
}
// Return if nothing's been changed
if (!self::data_array_diff(isset($this->folder_meta[$name]) ? $this->folder_meta[$name] : null, $metadata)) {
return true;
}
$this->folder_meta[$name] = $metadata;
return $this->storage->set_metadata($name, [self::ASYNC_KEY => $this->serialize_metadata($metadata)]);
}
-
+ /**
+ * Returns device metadata
+ *
+ * @param string $id Device ID
+ *
+ * @return array|null Device metadata
+ */
public function device_get($id)
{
$devices_list = $this->devices_list();
return $devices_list[$id] ?? null;
}
/**
* Registers new device on server
*
* @param array $device Device data
* @param string $id Device ID
*
* @return bool True on success, False on failure
*/
public function device_create($device, $id)
{
// Fill local cache
$this->devices_list();
// Some devices create dummy devices with name "validate" (#1109)
// This device entry is used in two initial requests, but later
// the device registers a real name. We can remove this dummy entry
// on new device creation
$this->device_delete('validate');
// Old Kolab_ZPush device parameters
// MODE: -1 | 0 | 1 (not set | flatmode | foldermode)
// TYPE: device type string
// ALIAS: user-friendly device name
// Syncroton (kolab_sync_backend_device) uses
// ID: internal identifier in syncroton database
// TYPE: device type string
// ALIAS: user-friendly device name
$metadata = $this->root_meta;
$metadata['DEVICE'][$id] = $device;
$metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)];
$result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
if ($result) {
// Update local cache
$this->root_meta['DEVICE'][$id] = $device;
// subscribe default set of folders
$this->device_init_subscriptions($id);
}
return $result;
}
/**
* Device update.
*
* @param array $device Device data
* @param string $id Device ID
*
* @return bool True on success, False on failure
*/
public function device_update($device, $id)
{
$devices_list = $this->devices_list();
$old_device = $devices_list[$id];
if (!$old_device) {
return false;
}
// Do nothing if nothing is changed
if (!self::data_array_diff($old_device, $device)) {
return true;
}
$device = array_merge($old_device, $device);
$metadata = $this->root_meta;
$metadata['DEVICE'][$id] = $device;
$metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)];
$result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
if ($result) {
// Update local cache
$this->root_meta['DEVICE'][$id] = $device;
}
return $result;
}
-
/**
* Device delete.
*
- * @param string $id Device ID
+ * @param string $id Device ID
*
* @return bool True on success, False on failure
*/
public function device_delete($id)
{
$device = $this->device_get($id);
if (!$device) {
return false;
}
unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]);
if (empty($this->root_meta['DEVICE'])) {
unset($this->root_meta['DEVICE']);
}
if (empty($this->root_meta['FOLDER'])) {
unset($this->root_meta['FOLDER']);
}
$metadata = $this->serialize_metadata($this->root_meta);
$metadata = [self::ASYNC_KEY => $metadata];
// update meta data
$result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
if ($result) {
// remove device annotation for every folder
foreach ($this->folder_meta() as $folder => $meta) {
// skip root folder (already handled above)
if ($folder == self::ROOT_MAILBOX)
continue;
if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) {
unset($meta['FOLDER'][$id]);
if (empty($meta['FOLDER'])) {
unset($this->folder_meta[$folder]['FOLDER']);
unset($meta['FOLDER']);
}
if (empty($meta)) {
unset($this->folder_meta[$folder]);
$meta = null;
}
$metadata = [self::ASYNC_KEY => $this->serialize_metadata($meta)];
$res = $this->storage->set_metadata($folder, $metadata);
if ($res && $meta) {
$this->folder_meta[$folder] = $meta;
}
}
}
}
return $result;
}
/**
* Returns list of categories assigned to an object
*
* @param object|string $object UID or rcube_message object
* @param array $categories Addition tag names to merge with
*
* @return array List of categories
*/
public function getCategories($object, $categories = [])
{
if (is_object($object)) {
// support only messages with message-id
if (!($msg_id = $object->headers->get('message-id', false))) {
return [];
}
$config = kolab_storage_config::get_instance();
$delta = Syncroton_Registry::getPingTimeout();
$folder = $object->folder;
$uid = $object->uid;
// get tag objects raleted to specified message-id
$tags = $config->get_tags($msg_id);
foreach ($tags as $idx => $tag) {
// resolve members if it wasn't done recently
$force = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta;
$members = $config->resolve_members($tag, $force);
if (empty($members[$folder]) || !in_array($uid, $members[$folder])) {
unset($tags[$idx]);
}
if ($force) {
$this->tag_rts[$tag['uid']] = time();
}
}
// make sure current folder is set correctly again
$this->storage->set_folder($folder);
} else {
$config = kolab_storage_config::get_instance();
$tags = $config->get_tags($object);
}
$tags = array_filter(array_map(function($v) { return $v['name']; }, $tags));
// merge result with old categories
if (!empty($categories)) {
$tags = array_unique(array_merge($tags, (array) $categories));
}
return $tags;
}
/**
* Gets kolab_storage_folder object from Activesync folder ID.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
*
* @param ?kolab_storage_folder
*/
public function getFolder($folderid, $deviceid, $type)
{
$unique_key = "$folderid:$deviceid:$type";
if (array_key_exists($unique_key, $this->folders)) {
return $this->folders[$unique_key];
}
$foldername = $this->folder_id2name($folderid, $deviceid);
return $this->folders[$unique_key] = kolab_storage::get_folder($foldername, $type);
}
/**
* Gets Activesync preferences for a folder.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
*
- * @param array
+ * @param array Folder preferences
*/
public function getFolderConfig($folderid, $deviceid, $type)
{
$foldername = $this->folder_id2name($folderid, $deviceid);
- $metadata = $this->backend->folder_meta();
+ $metadata = $this->folder_meta();
$config = [];
if (!empty($metadata[$foldername]['FOLDER'][$deviceid])) {
$config = $metadata[$foldername]['FOLDER'][$deviceid];
}
return [
'ALARMS' => ($config['S'] ?? 0) == 2,
];
}
/**
* Gets an object from a folder by UID.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Requested object UID
*
- * @param array|null
+ * @param array|null Object properties
*/
public function getObject($folderid, $deviceid, $type, $uid)
{
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = $folder->get_object($uid);
if ($result === false) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
return $result;
}
/**
* Gets objects matching UID by prefix.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param string $uid Requested object UID prefix
*
- * @param array|iterable
+ * @param array|iterable List of objects
*/
public function getObjectsByUidPrefix($folderid, $deviceid, $type, $uid)
{
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$result = $folder->select([['uid', '~*', $uid]]);
if ($result === false) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
return $result;
}
/**
* Set categories to an object
*
* @param object|string $object UID or rcube_message object
* @param array $categories List of Category names
*/
public function setCategories($object, $categories)
{
if (!is_object($object)) {
$config = kolab_storage_config::get_instance();
$config->save_tags($object, $categories);
return;
}
$config = kolab_storage_config::get_instance();
$delta = Syncroton_Registry::getPingTimeout();
$uri = kolab_storage_config::get_message_uri($object->headers, $object->folder);
// for all tag objects...
foreach ($config->get_tags() as $relation) {
// resolve members if it wasn't done recently
$uid = $relation['uid'];
$force = empty($this->tag_rts[$uid]) || $this->tag_rts[$uid] <= time() - $delta;
if ($force) {
$config->resolve_members($relation, $force);
$this->tag_rts[$relation['uid']] = time();
}
$selected = !empty($categories) && in_array($relation['name'], $categories);
$found = !empty($relation['members']) && in_array($uri, $relation['members']);
$update = false;
// remove member from the relation
if ($found && !$selected) {
$relation['members'] = array_diff($relation['members'], (array) $uri);
$update = true;
}
// add member to the relation
else if (!$found && $selected) {
$relation['members'][] = $uri;
$update = true;
}
if ($update) {
$config->save($relation, 'relation');
}
$categories = array_diff($categories, (array) $relation['name']);
}
// create new relations
if (!empty($categories)) {
foreach ($categories as $tag) {
$relation = [
'name' => $tag,
'members' => (array) $uri,
'category' => 'tag',
];
$config->save($relation, 'relation');
}
}
// make sure current folder is set correctly again
$this->storage->set_folder($object->folder);
}
/**
* Search for existing objects in a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param array $filter Filter
* @param int $result_type Type of the result (see kolab_sync_data::RESULT_* constants)
* @param bool $force Force IMAP folder cache synchronization
* @param bool $tags Enable tags/relations check (if supported)
*
* @return array|int Search result as count or array of uids
*/
public function searchEntries($folderid, $deviceid, $type, $filter, $result_type, $force, $tags)
{
if ($type != self::MODEL_EMAIL) {
return $this->searchKolabEntries($folderid, $deviceid, $type, $filter, $result_type, $force, $tags);
}
$filter_str = 'ALL UNDELETED';
// convert filter into one IMAP search string
foreach ($filter as $idx => $filter_item) {
if (is_array($filter_item)) {
// This is a request for changes since last time
// we'll use HIGHESTMODSEQ value from the last Sync
if ($filter_item[0] == 'changed' && $filter_item[1] == '>') {
$modseq_lasttime = $filter_item[2];
$modseq_data = [];
$modseq = (array) $this->modseq_get($deviceid, $folderid, $modseq_lasttime);
}
}
else {
$filter_str .= ' ' . $filter_item;
}
}
// get members of modified relations
if ($tags && $this->relationSupport) {
$changed_msgs = $this->getChangesByRelations($folderid, $deviceid, $type, $filter);
}
$result = $result_type == kolab_sync_data::RESULT_COUNT ? 0 : [];
$foldername = $this->folder_id2name($folderid, $deviceid);
if ($foldername === null) {
return $result;
}
$this->storage->set_folder($foldername);
// Synchronize folder (if it wasn't synced in this request already)
if ($force) {
$this->storage->folder_sync($foldername);
}
// We're in "get changes" mode
if (isset($modseq_data)) {
$folder_data = $this->storage->folder_data($foldername);
$modified = false;
// If previous HIGHESTMODSEQ doesn't exist we can't get changes
// We can only get folder's HIGHESTMODSEQ value and store it for the next try
// Skip search if HIGHESTMODSEQ didn't change
if (!empty($folder_data['HIGHESTMODSEQ'])) {
$modseq_data[$foldername] = $folder_data['HIGHESTMODSEQ'];
$modseq_old = isset($modseq[$foldername]) ? $modseq[$foldername] : null;
if ($modseq_data[$foldername] != $modseq_old) {
$modseq_update = true;
if ($modseq && $modseq_old) {
$modified = true;
$filter_str .= " MODSEQ " . ($modseq_old + 1);
}
}
}
}
else {
$modified = true;
}
// We could use messages cache by replacing search() with index()
// in some cases. This however is possible only if user has skip_deleted=true,
// in his Roundcube preferences, otherwise we'd make often cache re-initialization,
// because Roundcube message cache can work only with one skip_deleted
// setting at a time. We'd also need to make sure folder_sync() was called
// before (see above).
//
// if ($filter_str == 'ALL UNDELETED')
// $search = $this->storage->index($foldername, null, null, true, true);
// else
if ($modified) {
$search = $this->storage->search_once($foldername, $filter_str);
if (!($search instanceof rcube_result_index) || $search->is_error()) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
$result = $search->count();
break;
case kolab_sync_data::RESULT_UID:
$result = $search->get();
break;
}
}
// handle relation changes
if (!empty($changed_msgs)) {
$members = $this->findRelationMembersInFolder($foldername, $changed_msgs, $filter);
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
$result += count($members);
break;
case kolab_sync_data::RESULT_UID:
$result = array_values(array_unique(array_merge($result, $members)));
break;
}
}
if (!empty($modseq_update)) {
$this->modseq_set($deviceid, $folderid, $this->syncTimeStamp, $modseq_data);
// if previous modseq information does not exist save current set as it,
// we would at least be able to detect changes since now
if (empty($result) && empty($modseq)) {
$this->modseq_set($deviceid, $folderid, $modseq_lasttime, $modseq_data);
}
}
return $result;
}
/**
* Search for existing objects in a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
* @param array $filter Filter
* @param int $result_type Type of the result (see kolab_sync_data::RESULT_* constants)
* @param bool $force Force IMAP folder cache synchronization
* @param bool $tags Enable tags/relations check (if supported)
*
* @return array|int Search result as count or array of uids
*/
protected function searchKolabEntries($folderid, $deviceid, $type, $filter, $result_type, $force, $tags)
{
// there's a PHP Warning from kolab_storage if $filter isn't an array
if (empty($filter)) {
$filter = [];
} elseif ($tags && $this->relationSupport) {
$changed_objects = $this->getChangesByRelations($folderid, $deviceid, $type, $filter);
}
$folder = $this->getFolder($folderid, $deviceid, $type);
if (!$folder || !$folder->valid) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
}
$error = false;
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
$count = $folder->count($filter);
if ($count === null || $count === false) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
} else {
$result = (int) $count;
}
break;
case kolab_sync_data::RESULT_UID:
$uids = $folder->get_uids($filter);
if (!is_array($uids)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
} else {
$result = $uids;
}
break;
}
// handle tag modifications
if (!empty($changed_objects)) {
// build new filter
// search objects mathing current filter,
// relations may contain members of many types, we need to
// search them by UID in all requested folders to get
// only these with requested type (and that really exist
// in specified folders)
$tag_filter = [['uid', '=', $changed_objects]];
foreach ($filter as $f) {
if ($f[0] != 'changed') {
$tag_filter[] = $f;
}
}
switch ($result_type) {
case kolab_sync_data::RESULT_COUNT:
// Note: this way we're potentally counting the same objects twice
// I'm not sure if this is a problem, we most likely do not
// need a precise result here
$count = $folder->count($tag_filter);
if ($count !== null && $count !== false) {
$result += (int) $count;
}
break;
case kolab_sync_data::RESULT_UID:
$uids = $folder->get_uids($tag_filter);
if (is_array($uids) && !empty($uids)) {
$result = array_unique(array_merge($result, $uids));
}
break;
}
}
return $result;
}
/**
* Find members (messages) in specified folder
*/
protected function findRelationMembersInFolder($foldername, $members, $filter)
{
foreach ($members as $member) {
// IMAP URI members
if ($url = kolab_storage_config::parse_member_url($member)) {
$result[$url['folder']][$url['uid']] = $url['params'];
}
}
// convert filter into one IMAP search string
$filter_str = 'ALL UNDELETED';
foreach ($filter as $filter_item) {
if (is_string($filter_item)) {
$filter_str .= ' ' . $filter_item;
}
}
$found = [];
// first find messages by UID
if (!empty($result[$foldername])) {
$index = $this->storage->search_once($foldername, 'UID '
. rcube_imap_generic::compressMessageSet(array_keys($result[$foldername])));
$found = $index->get();
// remove found messages from the $result
if (!empty($found)) {
$result[$foldername] = array_diff_key($result[$foldername], array_flip($found));
if (empty($result[$foldername])) {
unset($result[$foldername]);
}
// now apply the current filter to the found messages
$index = $this->storage->search_once($foldername, $filter_str . ' UID '
. rcube_imap_generic::compressMessageSet($found));
$found = $index->get();
}
}
// search by message parameters
if (!empty($result)) {
// @TODO: do this search in chunks (for e.g. 25 messages)?
$search = '';
$search_count = 0;
foreach ($result as $data) {
foreach ($data as $p) {
$search_params = [];
$search_count++;
foreach ($p as $key => $val) {
$key = strtoupper($key);
// don't search by subject, we don't want false-positives
if ($key != 'SUBJECT') {
$search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
}
}
$search .= ' (' . implode(' ', $search_params) . ')';
}
}
$search_str = str_repeat(' OR', $search_count-1) . $search;
// search messages in current folder
$search = $this->storage->search_once($foldername, $search_str);
$uids = $search->get();
if (!empty($uids)) {
// add UIDs into the result
$found = array_unique(array_merge($found, $uids));
}
}
return $found;
}
/**
* Detect changes of relation (tag) objects data and assigned objects
* Returns relation member identifiers
*/
protected function getChangesByRelations($folderid, $deviceid, $type, $filter)
{
// get period filter, create new objects filter
foreach ($filter as $f) {
if ($f[0] == 'changed' && $f[1] == '>') {
$since = $f[2];
}
}
// this is not search for changes, do nothing
if (empty($since)) {
return;
}
// get relations state from the last sync
$last_state = (array) $this->relations_state_get($deviceid, $folderid, $since);
// get current relations state
$config = kolab_storage_config::get_instance();
$default = true;
$filter = [
['type', '=', 'relation'],
['category', '=', 'tag']
];
$relations = $config->get_objects($filter, $default, 100);
$result = [];
$changed = false;
// compare states, get members of changed relations
foreach ($relations as $relation) {
$rel_id = $relation['uid'];
if ($relation['changed']) {
$relation['changed']->setTimezone(new DateTimeZone('UTC'));
}
// last state unknown...
if (empty($last_state[$rel_id])) {
// ...get all members
if (!empty($relation['members'])) {
$changed = true;
$result = array_merge($result, $relation['members']);
}
}
// last state known, changed tag name...
else if ($last_state[$rel_id]['name'] != $relation['name']) {
// ...get all (old and new) members
$members_old = explode("\n", $last_state[$rel_id]['members']);
$changed = true;
$members = array_unique(array_merge($relation['members'], $members_old));
$result = array_merge($result, $members);
}
// last state known, any other change change...
else if ($last_state[$rel_id]['changed'] < $relation['changed']->format('U')) {
// ...find new and removed members
$members_old = explode("\n", $last_state[$rel_id]['members']);
$new = array_diff($relation['members'], $members_old);
$removed = array_diff($members_old, $relation['members']);
if (!empty($new) || !empty($removed)) {
$changed = true;
$result = array_merge($result, $new, $removed);
}
}
unset($last_state[$rel_id]);
}
// get members of deleted relations
if (!empty($last_state)) {
$changed = true;
foreach ($last_state as $relation) {
$members = explode("\n", $relation['members']);
$result = array_merge($result, $members);
}
}
// save current state
if ($changed) {
$data = [];
foreach ($relations as $relation) {
$data[$relation['uid']] = [
'name' => $relation['name'],
'changed' => $relation['changed']->format('U'),
'members' => implode("\n", (array)$relation['members']),
];
}
$now = new DateTime('now', new DateTimeZone('UTC'));
$this->relations_state_set($deviceid, $folderid, $now, $data);
}
// in mail mode return only message URIs
if ($this->modelName == self::MODEL_EMAIL) {
// lambda function to skip email members
$filter_func = function($value) {
return strpos($value, 'imap://') === 0;
};
$result = array_filter(array_unique($result), $filter_func);
}
// otherwise return only object UIDs
else {
// lambda function to skip email members
$filter_func = function($value) {
return strpos($value, 'urn:uuid:') === 0;
};
// lambda function to parse member URI
$member_func = function($value) {
if (strpos($value, 'urn:uuid:') === 0) {
$value = substr($value, 9);
}
return $value;
};
$result = array_map($member_func, array_filter(array_unique($result), $filter_func));
}
return $result;
}
/**
* Subscribe default set of folders on device registration
*/
protected function device_init_subscriptions($deviceid)
{
// INBOX always exists
$this->folder_set('INBOX', $deviceid, 1);
$supported_types = [
'mail.drafts',
'mail.wastebasket',
'mail.sentitems',
'mail.outbox',
'event.default',
'contact.default',
'note.default',
'task.default',
'event',
'contact',
'note',
'task',
'event.confidential',
'event.private',
'task.confidential',
'task.private',
];
$rcube = rcube::get_instance();
$config = $rcube->config;
$mode = (int) $config->get('activesync_init_subscriptions');
$folders = [];
// Subscribe to default folders
$foldertypes = kolab_storage::folders_typedata();
if (!empty($foldertypes)) {
$_foldertypes = array_intersect($foldertypes, $supported_types);
// get default folders
foreach ($_foldertypes as $folder => $type) {
// only personal folders
if ($this->storage->folder_namespace($folder) == 'personal') {
$flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
$this->folder_set($folder, $deviceid, $flag);
$folders[] = $folder;
}
}
}
// we're in default mode, exit
if (!$mode) {
return;
}
// below we support additionally all mail folders
$supported_types[] = 'mail';
$supported_types[] = 'mail.junkemail';
// get configured special folders
$special_folders = [];
$map = [
'drafts' => 'mail.drafts',
'junk' => 'mail.junkemail',
'sent' => 'mail.sentitems',
'trash' => 'mail.wastebasket',
];
foreach ($map as $folder => $type) {
if ($folder = $config->get($folder . '_mbox')) {
$special_folders[$folder] = $type;
}
}
// get folders list(s)
if (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) {
$all_folders = $this->storage->list_folders();
if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) {
$subscribed_folders = $this->storage->list_folders_subscribed();
}
}
else {
$all_folders = $this->storage->list_folders_subscribed();
}
foreach ($all_folders as $folder) {
// folder already subscribed
if (in_array($folder, $folders)) {
continue;
}
$type = ($foldertypes[$folder] ?? null) ?: 'mail';
if ($type == 'mail' && isset($special_folders[$folder])) {
$type = $special_folders[$folder];
}
if (!in_array($type, $supported_types)) {
continue;
}
$ns = strtoupper($this->storage->folder_namespace($folder));
// subscribe the folder according to configured mode
// and folder namespace/subscription status
if (($mode & constant("self::INIT_ALL_{$ns}"))
|| (($mode & constant("self::INIT_SUB_{$ns}"))
&& (!isset($subscribed_folders) || in_array($folder, $subscribed_folders)))
) {
$flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
$this->folder_set($folder, $deviceid, $flag);
}
}
}
/**
* Helper method to decode saved IMAP metadata
*/
protected function unserialize_metadata($str)
{
if (!empty($str)) {
- // Support old Z-Push annotation format
- if ($str[0] != '{') {
- $str = base64_decode($str);
- }
$data = json_decode($str, true);
return $data;
}
return null;
}
/**
* Helper method to encode IMAP metadata for saving
*/
protected function serialize_metadata($data)
{
if (!empty($data) && is_array($data)) {
$data = json_encode($data);
-// $data = base64_encode($data);
return $data;
}
return null;
}
/**
* Returns Kolab folder type for specified ActiveSync type ID
*/
- public static function type_activesync2kolab($type)
+ protected static function type_activesync2kolab($type)
{
if (!empty(self::$types[$type])) {
return self::$types[$type];
}
return '';
}
/**
* Returns ActiveSync folder type for specified Kolab type
*/
- public static function type_kolab2activesync($type)
+ protected static function type_kolab2activesync($type)
{
$type = preg_replace('/\.(confidential|private)$/i', '', $type);
if ($key = array_search($type, self::$types)) {
return $key;
}
return key(self::$types);
}
- /**
- * Returns Kolab folder type for specified ActiveSync class name
- */
- public static function class_activesync2kolab($class)
- {
- if (!empty(self::$classes[$class])) {
- return self::$classes[$class];
- }
-
- return '';
- }
-
/**
* Returns folder data in Syncroton format
*/
protected function folder_data($folder, $type)
{
// Folder name parameters
$delim = $this->storage->get_hierarchy_delimiter();
$items = explode($delim, $folder);
$name = array_pop($items);
// Folder UID
$folder_id = $this->folder_id($folder, $type);
// Folder type
if (strcasecmp($folder, 'INBOX') === 0) {
// INBOX is always inbox, prevent from issues related with a change of
// folder type annotation (it can be initially unset).
$type = 2;
}
else {
$type = self::type_kolab2activesync($type);
// fix type, if there's no type annotation it's detected as UNKNOWN we'll use 'mail' (12)
if ($type == 1) {
$type = 12;
}
}
// Syncroton folder data array
return [
'serverId' => $folder_id,
'parentId' => count($items) ? $this->folder_id(implode($delim, $items), $type) : 0,
'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET),
'type' => $type,
// for internal use
'imap_name' => $folder,
];
}
/**
* Builds folder ID based on folder name
*/
- public function folder_id($name, $type = null)
+ protected function folder_id($name, $type = null)
{
// ActiveSync expects folder identifiers to be max.64 characters
// So we can't use just folder name
$name = (string) $name;
if ($name === '') {
return null;
}
if (isset($this->folder_uids[$name])) {
return $this->folder_uids[$name];
}
/*
@TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves.
There's one inconvenience of this solution: folder name/type change
would be handled in ActiveSync as delete + create.
// get folders unique identifier
$folderdata = $this->storage->get_metadata($name, self::UID_KEY);
if ($folderdata && !empty($folderdata[$name])) {
$uid = $folderdata[$name][self::UID_KEY];
return $this->folder_uids[$name] = $uid;
}
*/
if (strcasecmp($name, 'INBOX') === 0) {
// INBOX is always inbox, prevent from issues related with a change of
// folder type annotation (it can be initially unset).
$type = 'mail.inbox';
}
else {
if ($type === null) {
$type = kolab_storage::folder_type($name);
}
if ($type != null) {
$type = preg_replace('/\.(confidential|private)$/i', '', $type);
}
}
// Add type to folder UID hash, so type change can be detected by Syncroton
$uid = $name . '!!' . $type;
$uid = md5($uid);
return $this->folder_uids[$name] = $uid;
}
/**
* Returns IMAP folder name
*
* @param string $id Folder identifier
* @param string $deviceid Device dentifier
*
* @return string Folder name (UTF7-IMAP)
*/
public function folder_id2name($id, $deviceid)
{
// check in cache first
if (!empty($this->folder_uids)) {
if (($name = array_search($id, $this->folder_uids)) !== false) {
return $name;
}
}
/*
@TODO: see folder_id()
// get folders unique identifier
$folderdata = $this->storage->get_metadata('*', self::UID_KEY);
foreach ((array)$folderdata as $folder => $data) {
if (!empty($data[self::UID_KEY])) {
$uid = $data[self::UID_KEY];
$this->folder_uids[$folder] = $uid;
if ($uid == $id) {
$name = $folder;
}
}
}
*/
// get all folders of specified type
$folderdata = $this->folder_meta();
if (!is_array($folderdata) || $id === null) {
return null;
}
$name = null;
// check if folders are "subscribed" for activesync
foreach ($folderdata as $folder => $meta) {
if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
|| empty($meta['FOLDER'][$deviceid]['S'])
) {
continue;
}
if ($uid = $this->folder_id($folder)) {
$this->folder_uids[$folder] = $uid;
}
if ($uid === $id) {
$name = $folder;
}
}
return $name;
}
/**
+ * Save MODSEQ value for a folder
*/
- public function modseq_set($deviceid, $folderid, $synctime, $data)
+ protected function modseq_set($deviceid, $folderid, $synctime, $data)
{
$synctime = $synctime->format('Y-m-d H:i:s');
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$old_data = $this->modseq[$folderid][$synctime] ?? null;
if (empty($old_data)) {
$this->modseq[$folderid][$synctime] = $data;
$data = json_encode($data);
$db->set_option('ignore_key_errors', true);
$db->query("INSERT INTO `syncroton_modseq` (`device_id`, `folder_id`, `synctime`, `data`)"
." VALUES (?, ?, ?, ?)",
$deviceid, $folderid, $synctime, $data);
$db->set_option('ignore_key_errors', false);
}
}
- public function modseq_get($deviceid, $folderid, $synctime)
+ /**
+ * Get stored MODSEQ value for a folder
+ */
+ protected function modseq_get($deviceid, $folderid, $synctime)
{
$synctime = $synctime->format('Y-m-d H:i:s');
if (empty($this->modseq[$folderid][$synctime])) {
$this->modseq[$folderid] = [];
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$db->limitquery("SELECT `data`, `synctime` FROM `syncroton_modseq`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?"
." ORDER BY `synctime` DESC",
0, 1, $deviceid, $folderid, $synctime);
if ($row = $db->fetch_assoc()) {
$synctime = $row['synctime'];
// @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format
$this->modseq[$folderid][$synctime] = json_decode($row['data'], true);
}
// Cleanup: remove all records except the current one
$db->query("DELETE FROM `syncroton_modseq`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
$deviceid, $folderid, $synctime);
}
- return @$this->modseq[$folderid][$synctime];
+ return $this->modseq[$folderid][$synctime] ?? null;
}
/**
* Set state of relation objects at specified point in time
*/
public function relations_state_set($deviceid, $folderid, $synctime, $relations)
{
$synctime = $synctime->format('Y-m-d H:i:s');
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$old_data = $this->relations[$folderid][$synctime] ?? null;
if (empty($old_data)) {
$this->relations[$folderid][$synctime] = $relations;
$data = rcube_charset::clean(json_encode($relations));
$db->set_option('ignore_key_errors', true);
$db->query("INSERT INTO `syncroton_relations_state`"
." (`device_id`, `folder_id`, `synctime`, `data`)"
." VALUES (?, ?, ?, ?)",
$deviceid, $folderid, $synctime, $data);
$db->set_option('ignore_key_errors', false);
}
}
/**
* Get state of relation objects at specified point in time
*/
- public function relations_state_get($deviceid, $folderid, $synctime)
+ protected function relations_state_get($deviceid, $folderid, $synctime)
{
$synctime = $synctime->format('Y-m-d H:i:s');
if (empty($this->relations[$folderid][$synctime])) {
$this->relations[$folderid] = [];
$rcube = rcube::get_instance();
$db = $rcube->get_dbh();
$db->limitquery("SELECT `data`, `synctime` FROM `syncroton_relations_state`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?"
." ORDER BY `synctime` DESC",
0, 1, $deviceid, $folderid, $synctime);
if ($row = $db->fetch_assoc()) {
$synctime = $row['synctime'];
// @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format
$this->relations[$folderid][$synctime] = json_decode($row['data'], true);
}
// Cleanup: remove all records except the current one
$db->query("DELETE FROM `syncroton_relations_state`"
." WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
$deviceid, $folderid, $synctime);
}
- return @$this->relations[$folderid][$synctime];
+ return $this->relations[$folderid][$synctime] ?? null;
}
/**
* Return last storage error
*/
public static function last_error()
{
return kolab_storage::$last_error;
}
/**
* Compares two arrays
*
* @param array $array1
* @param array $array2
*
* @return bool True if arrays differs, False otherwise
*/
protected static function data_array_diff($array1, $array2)
{
if (!is_array($array1) || !is_array($array2)) {
return $array1 != $array2;
}
if (count($array1) != count($array2)) {
return true;
}
foreach ($array1 as $key => $val) {
if (!array_key_exists($key, $array2)) {
return true;
}
if ($val !== $array2[$key]) {
return true;
}
}
return false;
}
}
diff --git a/lib/kolab_sync_storage_kolab4.php b/lib/kolab_sync_storage_kolab4.php
index 61d288b..8f95024 100644
--- a/lib/kolab_sync_storage_kolab4.php
+++ b/lib/kolab_sync_storage_kolab4.php
@@ -1,592 +1,592 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab Sync (ActiveSync for Kolab) |
| |
| Copyright (C) 2011-2023, Apheleia IT AG <contact@apheleia-it.ch> |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Storage handling class with Kolab 4 support (IMAP + CalDAV + CardDAV)
*/
class kolab_sync_storage_kolab4 extends kolab_sync_storage
{
protected $davStorage = null;
protected $relationSupport = false;
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync_storage_kolab4 The one and only instance
*/
public static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_sync_storage_kolab4();
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Class initialization
*/
public function startup()
{
$sync = kolab_sync::get_instance();
if ($sync->username === null || $sync->password === null) {
throw new Exception("Unsupported storage handler use!");
}
$url = $sync->config->get('activesync_dav_server', 'http://localhost');
if (strpos($url, '://') === false) {
$url = 'http://' . $url;
}
// Inject user+password to the URL, there's no other way to pass it to the DAV client
$url = str_replace('://', '://' . rawurlencode($sync->username) . ':' . rawurlencode($sync->password) . '@', $url);
$this->davStorage = new kolab_storage_dav($url); // DAV
$this->storage = $sync->get_storage(); // IMAP
// set additional header used by libkolab
$this->storage->set_options([
'skip_deleted' => true,
'threading' => false,
]);
// Disable paging
$this->storage->set_pagesize(999999);
}
/**
* Get list of folders available for sync
*
* @param string $deviceid Device identifier
* @param string $type Folder (class) type
* @param bool $flat_mode Enables flat-list mode
*
* @return array|bool List of mailbox folders, False on backend failure
*/
public function folders_list($deviceid, $type, $flat_mode = false)
{
$list = [];
// get mail folders subscribed for sync
if ($type === self::MODEL_EMAIL) {
$folderdata = $this->folder_meta();
if (!is_array($folderdata)) {
return false;
}
$special_folders = $this->storage->get_special_folders(true);
$type_map = [
'drafts' => 3,
'trash' => 4,
'sent' => 5,
];
// Get the folders "subscribed" for activesync
foreach ($folderdata as $folder => $meta) {
if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
|| empty($meta['FOLDER'][$deviceid]['S'])
) {
continue;
}
// Force numeric folder name to be a string (T1283)
$folder = (string) $folder;
// Activesync folder properties
$folder_data = $this->folder_data($folder, 'mail');
// Set proper type for special folders
if (($type = array_search($folder, $special_folders)) && isset($type_map[$type])) {
$folder_data['type'] = $type_map[$type];
}
$list[$folder_data['serverId']] = $folder_data;
}
}
else if (in_array($type, [self::MODEL_CONTACTS, self::MODEL_CALENDAR, self::MODEL_TASKS])) {
$list = [];
// TODO: For now all DAV folders are subscribed
foreach ($this->davStorage->get_folders($type) as $folder) {
$folder_data = $this->folder_data($folder, $type);
$list[$folder_data['serverId']] = $folder_data;
}
return $list;
}
/*
// TODO
if ($flat_mode) {
$list = $this->folders_list_flat($list, $type, $typedata);
}
*/
return $list;
}
/**
* Creates folder and subscribes to the device
*
* @param string $name Folder name (UTF8)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
* @param ?string $parentid Parent folder identifier
*
* @return string|false New folder identifier on success, False on failure
*/
public function folder_create($name, $type, $deviceid, $parentid = null)
{
// Mail folder
if ($type <= 6 || $type == 12) {
$parent = null;
$name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP');
if ($parentid) {
$parent = $this->folder_id2name($parentid, $deviceid);
if ($parent === null) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND);
}
}
if ($parent !== null) {
$delim = $this->storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
if ($this->storage->folder_exists($name)) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS);
}
// TODO: Support setting folder types?
$created = $this->storage->create_folder($name, true);
if ($created) {
// Set ActiveSync subscription flag
$this->folder_set($name, $deviceid, 1);
return $this->folder_id($name, 'mail');
}
// Special case when client tries to create a subfolder of INBOX
// which is not possible on Cyrus-IMAP (T2223)
if ($parent == 'INBOX' && stripos($this->last_error(), 'invalid') !== false) {
throw new Syncroton_Exception('', Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER);
}
return false;
}
else if ($type == 8 || $type == 13 || $type == 7 || $type == 15 || $type == 9 || $type == 14) {
// DAV folder
$type = preg_replace('|\..*|', '', self::type_activesync2kolab($type));
// TODO: Folder hierarchy support
// Check if folder exists
foreach ($this->davStorage->get_folders($type) as $folder) {
if ($folder->get_name() == $name) {
throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS);
}
}
$props = ['name' => $name, 'type' => $type];
if ($id = $this->davStorage->folder_update($props)) {
return "DAV:{$type}:{$id}";
}
return false;
}
throw new \Exception("Not implemented");
}
/**
* Renames a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $new_name New folder name (UTF8)
* @param ?string $parentid Folder parent identifier
*
* @return bool True on success, False on failure
*/
public function folder_rename($folderid, $deviceid, $new_name, $parentid)
{
// DAV folder
if (strpos($folderid, 'DAV:') === 0) {
[, $type, $id] = explode(':', $folderid);
$props = [
'id' => $id,
'name' => $new_name,
'type' => $type,
];
// TODO: Folder hierarchy support
return $this->davStorage->folder_update($props) !== false;
}
// Mail folder
$old_name = $this->folder_id2name($folderid, $deviceid);
if ($parentid) {
$parent = $this->folder_id2name($parentid, $deviceid);
}
$name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP');
if (isset($parent)) {
$delim = $this->storage->get_hierarchy_delimiter();
$name = $parent . $delim . $name;
}
if ($name === $old_name) {
return true;
}
$this->folder_meta = null;
return $this->storage->rename_folder($old_name, $name);
}
/**
* Deletes folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
*
* @param bool True on success, False otherwise
*/
public function folder_delete($folderid, $deviceid)
{
// DAV folder
if (strpos($folderid, 'DAV:') === 0) {
[, $type, $id] = explode(':', $folderid);
return $this->davStorage->folder_delete($id, $type) !== false;
}
// Mail folder
$name = $this->folder_id2name($folderid, $deviceid);
unset($this->folder_meta[$name]);
return $this->storage->delete_folder($name);
}
/**
* Deletes contents of a folder
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param bool $recursive Apply to the folder and its subfolders
*
* @param bool True on success, False otherwise
*/
public function folder_empty($folderid, $deviceid, $recursive = false)
{
// DAV folder
if (strpos($folderid, 'DAV:') === 0) {
[, $type, $id] = explode(':', $folderid);
if ($folder = $this->davStorage->get_folder($id, $type)) {
return $folder->delete_all();
}
// TODO: $recursive=true
return false;
}
// Mail folder
return parent::folder_empty($folderid, $deviceid, $recursive);
}
/**
* Returns folder data in Syncroton format
*/
protected function folder_data($folder, $type)
{
// Mail folders
if (strpos($type, 'mail') === 0) {
return parent::folder_data($folder, $type);
}
// DAV folders
return [
'serverId' => "DAV:{$type}:{$folder->id}",
'parentId' => 0, // TODO: Folder hierarchy
'displayName' => $folder->get_name(),
'type' => $this->type_kolab2activesync($type),
];
}
/**
* Builds folder ID based on folder name
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Kolab folder type
*
* @return string Folder identifier (up to 64 characters)
*/
- public function folder_id($name, $type = null)
+ protected function folder_id($name, $type = null)
{
if (!$type) {
$type = 'mail';
}
// ActiveSync expects folder identifiers to be max.64 characters
// So we can't use just folder name
$name = (string) $name;
if ($name === '') {
return null;
}
if (strpos($type, 'mail') !== 0) {
throw new Exception("Unsupported folder_id() call on a DAV folder");
}
if (isset($this->folder_uids[$name])) {
return $this->folder_uids[$name];
}
/*
@TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves.
There's one inconvenience of this solution: folder name/type change
would be handled in ActiveSync as delete + create.
@TODO: Consider using MAILBOXID (RFC8474) that Cyrus v3 supports
// get folders unique identifier
$folderdata = $this->storage->get_metadata($name, self::UID_KEY);
if ($folderdata && !empty($folderdata[$name])) {
$uid = $folderdata[$name][self::UID_KEY];
return $this->folder_uids[$name] = $uid;
}
*/
if (strcasecmp($name, 'INBOX') === 0) {
// INBOX is always inbox, prevent from issues related with a change of
// folder type annotation (it can be initially unset).
$type = 'mail.inbox';
}
// Add type to folder UID hash, so type change can be detected by Syncroton
$uid = $name . '!!' . $type;
$uid = md5($uid);
return $this->folder_uids[$name] = $uid;
}
/**
* Returns IMAP folder name
*
* @param string $id Folder identifier
* @param string $deviceid Device dentifier
*
* @return string Folder name (UTF7-IMAP)
*/
public function folder_id2name($id, $deviceid)
{
// TODO: This method should become protected and be used for mail folders only
if (strpos($id, 'DAV:') === 0) {
throw new Exception("Unsupported folder_id2name() call on a DAV folder");
}
// check in cache first
if (!empty($this->folder_uids)) {
if (($name = array_search($id, $this->folder_uids)) !== false) {
return $name;
}
}
// get all folders of specified type
$folderdata = $this->folder_meta();
if (!is_array($folderdata) || $id === null) {
return null;
}
// check if folders are "subscribed" for activesync
foreach ($folderdata as $folder => $meta) {
if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
|| empty($meta['FOLDER'][$deviceid]['S'])
) {
continue;
}
- if ($uid = self::folder_id($folder, 'mail')) {
+ if ($uid = $this->folder_id($folder, 'mail')) {
$this->folder_uids[$folder] = $uid;
}
if ($uid === $id) {
$name = $folder;
}
}
return $name;
}
/**
* Returns list of categories assigned to an object
*
* @param object|string $object UID or rcube_message object
* @param array $categories Addition tag names to merge with
*
* @return array List of categories
*/
public function getCategories($object, $categories = [])
{
// TODO
return $categories;
}
/**
* Gets kolab_storage_folder object from Activesync folder ID.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
*
* @param ?kolab_storage_folder
*/
public function getFolder($folderid, $deviceid, $type)
{
if (strpos($folderid, 'DAV:') !== 0) {
throw new Exception("Unsupported getFolder() call on a mail folder");
}
$unique_key = "$folderid:$deviceid:$type";
if (array_key_exists($unique_key, $this->folders)) {
return $this->folders[$unique_key];
}
[, $type, $id] = explode(':', $folderid);
return $this->folders[$unique_key] = $this->davStorage->get_folder($id, $type);
}
/**
* Gets Activesync preferences for a folder.
*
* @param string $folderid Folder identifier
* @param string $deviceid Device identifier
* @param string $type Activesync model name (folder type)
*
- * @param array
+ * @param array Folder preferences
*/
public function getFolderConfig($folderid, $deviceid, $type)
{
// TODO: Get "alarms" from the DAV folder props, or implement
// a storage for folder properties
return [
'ALARMS' => true,
];
}
/**
* Set categories to an object
*
* @param object|string $object UID or rcube_message object
* @param array $categories List of Category names
*/
public function setCategories($object, $categories)
{
// TODO
}
/**
* Return last storage error
*/
public static function last_error()
{
// TODO
return null;
}
/**
* Subscribe default set of folders on device registration
*/
protected function device_init_subscriptions($deviceid)
{
$config = rcube::get_instance()->config;
$mode = (int) $config->get('activesync_init_subscriptions');
// Special folders only
if (!$mode) {
$all_folders = $this->storage->get_special_folders(true);
// We do not subscribe to the Spam folder by default, same as the old Kolab driver does
unset($all_folders['junk']);
$all_folders = array_unique(array_merge(['INBOX'], array_values($all_folders)));
}
// other modes
elseif (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) {
$all_folders = $this->storage->list_folders();
if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) {
$subscribed_folders = $this->storage->list_folders_subscribed();
}
}
else {
$all_folders = $this->storage->list_folders_subscribed();
}
foreach ($all_folders as $folder) {
$ns = strtoupper($this->storage->folder_namespace($folder));
// subscribe the folder according to configured mode
// and folder namespace/subscription status
if (!$mode
|| ($mode & constant("self::INIT_ALL_{$ns}"))
|| (($mode & constant("self::INIT_SUB_{$ns}")) && (!isset($subscribed_folders) || in_array($folder, $subscribed_folders)))
) {
$this->folder_set($folder, $deviceid, 1);
}
}
// TODO: Subscribe personal DAV folders, for now we assume all are subscribed
// TODO: Subscribe shared DAV folders
}
/**
* Get all DAV folders of all types
*/
protected function dav_all_folders()
{
$list = [];
// TODO: For performance reasons this should be one HTTP request, not three
foreach ([self::MODEL_CONTACTS, self::MODEL_CALENDAR, self::MODEL_TASKS] as $type) {
$folders = $this->davStorage->get_folders($type);
$list = array_merge($list, $folders);
}
return $list;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Fri, Apr 24, 12:55 PM (1 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18896683
Default Alt Text
(194 KB)

Event Timeline