Page MenuHomePhorge

No OneTemporary

Size
120 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/kolab/kolab_storage.php b/lib/kolab/kolab_storage.php
index e14156d..1292b8b 100644
--- a/lib/kolab/kolab_storage.php
+++ b/lib/kolab/kolab_storage.php
@@ -1,624 +1,626 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 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/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const SERVERSIDE_SUBSCRIPTION = 0;
const CLIENTSIDE_SUBSCRIPTION = 1;
public static $last_error;
private static $ready = false;
private static $config;
private static $cache;
private static $imap;
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// set imap options
self::$imap->set_options(array(
'skip_deleted' => true,
'threading' => false,
));
self::$imap->set_pagesize(9999);
}
return self::$ready;
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder)
{
return self::setup() ? new kolab_storage_folder($folder) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,distribution-list,event,task,note)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername);
else
$folder->set_folder($foldername);
if ($object = $folder->get_object($uid))
return $object;
}
return false;
}
/**
*
*/
public static function get_freebusy_server()
{
return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
}
/**
* Compose an URL to query the free/busy status for the given user
*/
public static function get_freebusy_url($email)
{
return self::get_freebusy_server() . '/' . $email . '.ifb';
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder ID string
*/
public static function folder_id($folder)
{
return asciiwords(strtr($folder, '/.-', '___'));
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false)
{
self::setup();
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::$imap->set_metadata($name, array(self::CTYPE_KEY => $type));
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* @return mixed New folder name or False on failure
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $delimiter) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else
$result = true;
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'] === self::SERVERSIDE_SUBSCRIPTION);
}
// save color in METADATA
// TODO: also save 'showalarams' and other properties here
if ($result && $prop['color']) {
$meta_saved = false;
$ns = self::$imap->folder_namespace($folder);
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_SHARED => $prop['color']));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_PRIVATE => $prop['color']));
if ($meta_saved)
unset($prop['color']); // unsetting will prevent fallback to local user prefs
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
self::setup();
$found = false;
$namespace = self::$imap->get_namespace();
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username
$pos = strpos($folder, $delim);
if ($pos) {
$prefix = '('.substr($folder, 0, $pos).') ';
$folder = substr($folder, $pos+1);
}
else {
$prefix = '('.$folder.')';
$folder = '';
}
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
if ($prefix)
$folder = html::quote($prefix) . ' ' . $folder;
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Helper method to generate a truncated folder name to display
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type
$folders = self::get_folders($type);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
continue;
}
// always show the parent of current folder
if ($p_len && $name == $parent) { }
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = rcube_charset::convert($name, 'UTF7-IMAP');
}
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = rcube_charset::convert($parent, 'UTF7-IMAP');
}
// Sort folders list
asort($names, SORT_LOCALE_STRING);
$folders = array_keys($names);
$names = array();
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
foreach ($folders as $name) {
$imap_name = $name;
$name = $origname = self::object_name($name);
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,distribution-list,event,task,note,mail)
* @param string Enable to return subscribed folders only
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
return self::$imap->list_folders_subscribed($root, $mbox);
}
else {
return self::$imap->list_folders($root, $mbox);
}
}
$prefix = $root . $mbox;
// get folders types
$folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($folderdata)) {
return array();
}
$folderdata = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// In some conditions we can skip LIST command (?)
if ($subscribed == false && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
return array_keys($folderdata);
}
// Get folders list
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
}
else {
$folders = self::$imap->list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
/**
* Callback for array_map to select the correct annotation value
*/
static function folder_select_metadata($types)
{
return $types[self::CTYPE_KEY_PRIVATE] ?: $types[self::CTYPE_KEY];
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
static function folder_type($folder)
{
+ self::setup();
+
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
}
diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index 522e1cf..dd29629 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -1,1008 +1,733 @@
<?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> |
+--------------------------------------------------------------------------+
*/
class kolab_sync_backend
{
/**
* Singleton instace of kolab_sync_backend
*
* @var kolab_sync_backend
*/
static protected $instance;
protected $storage;
protected $folder_meta;
protected $folder_uids;
protected $root_meta;
static protected $types = array(
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',
);
static protected $classes = array(
Syncroton_Data_Factory::CLASS_CALENDAR => 'event',
Syncroton_Data_Factory::CLASS_CONTACTS => 'contact',
Syncroton_Data_Factory::CLASS_EMAIL => 'mail',
Syncroton_Data_Factory::CLASS_TASKS => 'task',
);
const ROOT_MAILBOX = 'INBOX';
// const ROOT_MAILBOX = '';
const ASYNC_KEY = '/private/vendor/kolab/activesync';
const UID_KEY = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
/**
* This implements the 'singleton' design pattern
*
* @return kolab_sync_backend The one and only instance
*/
static function get_instance()
{
if (!self::$instance) {
self::$instance = new kolab_sync_backend;
self::$instance->startup(); // init AFTER object was linked with self::$instance
}
return self::$instance;
}
/**
* Class initialization
*/
public function startup()
{
$this->storage = rcube::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(array(
// @TODO: there can be Roundcube plugins defining additional headers,
// we maybe would need to add them here
'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION',
'skip_deleted' => true,
'threading' => false,
));
// Disable paging
$this->storage->set_pagesize(999999);
}
/**
* 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 = array();
}
}
if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) {
return $this->root_meta['DEVICE'];
}
return array();
}
/**
* Get list of folders available for sync
*
* @param string $deviceid Device identifier
* @param string $type Folder type
*
* @return array|bool List of mailbox folders, False on backend failure
*/
public function folders_list($deviceid, $type)
{
// get all folders of specified type
$folders = (array) 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 = array();
// 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 (!empty($type) && !in_array($folder, $folders)) {
continue;
}
// Activesync folder identifier (serverId)
$folder_id = self::folder_id($folder, $typedata[$folder]);
$folders_list[$folder_id] = $this->folder_data($folder, $typedata[$folder]);
}
return $folders_list;
}
/**
* Getter for folder metadata
*
* @return array|bool Hash array with meta data for each folder, False on backend failure
*/
public function folder_meta()
{
if (!isset($this->folder_meta)) {
$this->folder_meta = array();
// get folders activesync config
$folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY);
if (!is_array($folderdata)) {
return false;
}
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 (UTF7-IMAP)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
*
* @return bool True on success, False on failure
*/
public function folder_create($name, $type, $deviceid)
{
if ($this->storage->folder_exists($name)) {
$created = true;
}
else {
$type = self::type_activesync2kolab($type);
$created = kolab_storage::folder_create($name, $kolab_type, true);
}
if ($created) {
// Set ActiveSync subscription flag
$this->folder_set($name, $deviceid, 1);
return true;
}
return false;
}
/**
* Renames a folder
*
* @param string $old_name Old folder name (UTF7-IMAP)
* @param string $new_name New folder name (UTF7-IMAP)
* @param int $type Folder (ActiveSync) type
* @param string $deviceid Device identifier
*
* @return bool True on success, False on failure
*/
public function folder_rename($old_name, $new_name, $type, $deviceid)
{
$type = self::type_activesync2kolab($type);
$moved = kolab_storage::folder_rename($old_name, $new_name);
if ($moved) {
// UnSet ActiveSync subscription flag
$this->folder_set($old_name, $deviceid, 0);
// Set ActiveSync subscription flag
$this->folder_set($new_name, $deviceid, 1);
return true;
}
return false;
}
/**
* Deletes folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $deviceid Device identifier
*
*/
public function folder_delete($name, $deviceid)
{
unset($this->folder_meta[$name]);
return kolab_storage::folder_delete($name);
}
/**
* 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)
*/
public 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 = $metadata[$name];
if ($flag) {
if (empty($metadata)) {
$metadata = array();
}
if (empty($metadata['FOLDER'])) {
$metadata['FOLDER'] = array();
}
if (empty($metadata['FOLDER'][$deviceid])) {
$metadata['FOLDER'][$deviceid] = array();
}
// Z-Push uses:
// 1 - synchronize, no alarms
// 2 - synchronize with alarms
$metadata['FOLDER'][$deviceid]['S'] = $flag;
}
if (!$flag) {
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($this->folder_meta[$name], $metadata)) {
return true;
}
$this->folder_meta[$name] = $metadata;
return $this->storage->set_metadata($name, array(
self::ASYNC_KEY => $this->serialize_metadata($metadata)));
}
public function device_get($id)
{
$devices_list = $this->devices_list();
$result = $devices_list[$id];
return $result;
}
/**
* 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();
// 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 = array(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 to default folders
$foldertypes = $this->storage->get_metadata('*', kolab_storage::CTYPE_KEY);
$types = array(
'mail.drafts',
'mail.wastebasket',
'mail.sentitems',
'mail.outbox',
'event.default',
'contact.default',
'task.default',
'event',
'contact',
'task'
);
$foldertypes = array_map('implode', $foldertypes);
$foldertypes = array_intersect($foldertypes, $types);
// get default folders
foreach ($foldertypes as $folder => $type) {
// only personal folders
if ($this->storage->folder_namespace($folder) == 'personal') {
$this->folder_set($folder, $id, 1);
}
}
// INBOX always exists
$this->folder_set('INBOX', $id, 1);
}
return $result;
}
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 = array(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
*
* @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 = array(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 = array(self::ASYNC_KEY => $this->serialize_metadata($meta));
$res = $this->storage->set_metadata($folder, $metadata);
if ($res && $meta) {
$this->folder_meta[$folder] = $meta;
}
}
}
}
return $result;
}
/**
* Helper method to decode saved IMAP metadata
*/
private 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
*/
private 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)
{
if (!empty(self::$types[$type])) {
return self::$types[$type];
}
return '';
}
/**
* Returns ActiveSync folder type for specified Kolab type
*/
public static function type_kolab2activesync($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 '';
}
private 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);
// Syncroton folder data array
return array(
'serverId' => $folder_id,
'parentId' => count($items) ? self::folder_id(implode($delim, $items)) : 0,
'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET),
'type' => self::type_kolab2activesync($type),
);
}
/**
* Builds folder ID based on folder name
*/
public function folder_id($name, $type = null)
{
// ActiveSync expects folder identifiers to be max.64 characters
// So we can't use just folder name
if ($name === '' || !is_string($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;
}
*/
// Add type to folder UID hash, so type change can be detected by Syncroton
$uid = $name . '!!' . ($type !== null ? $type : kolab_storage::folder_type($name));
$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)) {
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;
}
$uid = self::folder_id($folder);
$this->folder_uids[$folder] = $uid;
if ($uid == $id) {
$name = $folder;
}
}
return $name;
}
/**
* Compares two arrays
*
* @param array $array1
* @param array $array2
*
* @return bool True if arrays differs, False otherwise
*/
private 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;
}
-
- /**
- * Send the given message using the configured method.
- *
- * @param string $message Complete message source
- * @param array $smtp_error SMTP error array (reference)
- * @param array $smtp_opts SMTP options (e.g. DSN request)
- *
- * @return boolean Send status.
- */
- public function send_message(&$message, &$smtp_error, $smtp_opts = null)
- {
- $rcube = rcube::get_instance();
-
- list($headers, $message) = $this->parse_mime($message);
-
- $mailto = $headers['To'];
-
- $headers['User-Agent'] .= sprintf('%s v.%.1f', $rcube->app_name, kolab_sync::VERSION);
- if ($agent = $rcube->config->get('useragent')) {
- $headers['User-Agent'] .= '/' . $agent;
- }
-
- if (empty($headers['From'])) {
- $headers['From'] = $this->get_identity();
- }
- if (empty($headers['Message-ID'])) {
- $headers['Message-ID'] = $this->gen_message_id();
- }
-
- // remove empty headers
- $headers = array_filter($headers);
-
- // send thru SMTP server using custom SMTP library
- if ($rcube->config->get('smtp_server')) {
- $smtp_headers = $headers;
- // generate list of recipients
- $recipients = array();
-
- if (!empty($headers['To']))
- $recipients[] = $headers['To'];
- if (!empty($headers['Cc']))
- $recipients[] = $headers['Cc'];
- if (!empty($headers['Bcc']))
- $recipients[] = $headers['Bcc'];
-
- // remove Bcc header
- unset($smtp_headers['Bcc']);
-
- // send message
- if (!is_object($rcube->smtp)) {
- $rcube->smtp_init(true);
- }
-
- $sent = $rcube->smtp->send_mail($headers['From'], $recipients, $smtp_headers, $message, $smtp_opts);
- $smtp_response = $rcube->smtp->get_response();
- $smtp_error = $rcube->smtp->get_error();
-
- // log error
- if (!$sent) {
- rcube::raise_error(array('code' => 800, 'type' => 'smtp',
- 'line' => __LINE__, 'file' => __FILE__,
- 'message' => "SMTP error: ".join("\n", $smtp_response)), true, false);
- }
- }
- // send mail using PHP's mail() function
- else {
- $mail_headers = $headers;
- $delim = $rcube->config->header_delimiter();
- $subject = $headers['Subject'];
- $to = $headers['To'];
-
- // unset some headers because they will be added by the mail() function
- unset($mail_headers['To'], $mail_headers['Subject']);
-
- // #1485779
- if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
- if (preg_match_all('/<([^@]+@[^>]+)>/', $to, $m)) {
- $to = implode(', ', $m[1]);
- }
- }
-
- foreach ($mail_headers as $header => $header_value) {
- $mail_headers[$header] = $header . ': ' . $header_value;
- }
- $header_str = rtrim(implode("\r\n", $mail_headers));
-
- if ($delim != "\r\n") {
- $header_str = str_replace("\r\n", $delim, $header_str);
- $msg_body = str_replace("\r\n", $delim, $message);
- $to = str_replace("\r\n", $delim, $to);
- $subject = str_replace("\r\n", $delim, $subject);
- }
-
- if (ini_get('safe_mode'))
- $sent = mail($to, $subject, $message, $header_str);
- else
- $sent = mail($to, $subject, $message, $header_str, "-f$from");
- }
-
- if ($sent) {
- $rcube->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $message));
-
- // remove MDN headers after sending
- unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
-
- // get all recipients
- if ($headers['Cc'])
- $mailto .= ' ' . $headers['Cc'];
- if ($headers['Bcc'])
- $mailto .= ' ' . $headers['Bcc'];
- if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
- $mailto = implode(', ', array_unique($m[1]));
-
- if ($rcube->config->get('smtp_log')) {
- rcube::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
- $rcube->get_user_name(),
- $_SERVER['REMOTE_ADDR'],
- $mailto,
- !empty($smtp_response) ? join('; ', $smtp_response) : ''));
- }
- }
-
- unset($headers['Bcc']);
-
- // Build the message back
- foreach ($headers as $header => $header_value) {
- $headers[$header] = $header . ': ' . $header_value;
- }
- $message = trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($message);
-
- return $sent;
- }
-
- /**
- * MIME message parser
- *
- * @param string|resource $message MIME message source
- * @param bool $decode_body Enables body decoding
- *
- * @return array Message headers array and message body
- */
- public function parse_mime($message, $decode_body = false)
- {
- if (is_resource($message)) {
- $message = stream_get_contents($message);
- }
-
- list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY);
-
- // Parse headers to get sender and recipients
- $headers = str_replace("\r\n", "\n", $headers);
- $headers = explode("\n", trim($headers));
-
- $ln = 0;
- $lines = array();
-
- foreach ($headers as $line) {
- if (ord($line[0]) <= 32) {
- $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . $line;
- }
- else {
- $lines[++$ln] = trim($line);
- }
- }
-
- $headers = array();
- $headers_map = array(
- 'subject' => 'Subject',
- 'from' => 'From',
- 'to' => 'To',
- 'cc' => 'Cc',
- 'bcc' => 'Bcc',
- 'message-id' => 'Message-ID',
- 'references' => 'References',
- 'content-type' => 'Content-Type',
- 'content-transfer-encoding' => 'Content-Transfer-Encoding',
- );
-
- foreach ($lines as $line) {
- list($field, $string) = explode(':', $line, 2);
- $_field = strtolower($field);
-
- if (isset($headers_map[$_field])) {
- $field = $headers_map[$_field];
- }
-
- $headers[$field] = trim($string);
- }
-
- // Decode body
- if ($decode_body) {
- $message = str_replace("\r\n", "\n", $message);
- $encoding = strtolower($headers['Content-Transfer-Encoding']);
-
- switch ($encoding) {
- case 'base64':
- $message = base64_decode($message);
- break;
- case 'quoted-printable':
- $message = quoted_printable_decode($message);
- break;
- }
- }
-
- return array($headers, $message);
- }
-
- /**
- * Creates complete MIME message body
- *
- * @param array $headers Message headers
- * @param string $body Message body
- *
- * @return string Message source
- */
- public function build_mime($headers, $body)
- {
- // Encode the body
- $encoding = strtolower($headers['Content-Transfer-Encoding']);
-
- switch ($encoding) {
- case 'base64':
- $body = base64_encode($body);
- $body = chunk_split($body, 76, "\r\n");
- break;
- case 'quoted-printable':
- $body = quoted_printable_encode($body);
- break;
- }
-
- foreach ($headers as $header => $header_value) {
- $headers[$header] = $header . ': ' . $header_value;
- }
-
- // Build the complete message
- return trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($body);
- }
-
- /**
- * Returns email address string from default identity of the current user
- */
- protected function get_identity()
- {
- $user = kolab_sync::get_instance()->user;
-
- if ($identity = $user->get_identity()) {
- return format_email_recipient(format_email($identity['email']), $identity['name']);
- }
- }
-
- /**
- * Unique Message-ID generator.
- *
- * @return string Message-ID
- */
- protected function gen_message_id()
- {
- $user = kolab_sync::get_instance()->user;
- $local_part = md5(uniqid('rcmail'.mt_rand(),true));
- $domain_part = $user->get_username('domain');
-
- // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
- if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
- foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
- $host = preg_replace('/:[0-9]+$/', '', $host);
- if ($host && preg_match('/\.[a-z]+$/i', $host)) {
- $domain_part = $host;
- }
- }
- }
-
- return sprintf('<%s@%s>', $local_part, $domain_part);
- }
-
}
diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php
index e197b23..1f6aed4 100644
--- a/lib/kolab_sync_data_email.php
+++ b/lib/kolab_sync_data_email.php
@@ -1,1274 +1,1273 @@
<?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',
);
/**
* 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();
}
/**
* 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);
if (empty($message)) {
// @TODO: exception
return;
}
$msg = $this->parseMessageId($serverId);
$headers = $message->headers; // rcube_message_header
// Calendar namespace fields
foreach ($this->mapping as $key => $name) {
$value = null;
switch ($name) {
case 'internaldate':
$value = self::date_from_kolab(rcube_imap_generic::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) {
$name = $part['name'];
$mailto = $part['mailto'];
$string = $part['string'];
// @TODO: convert to utf8?
// @FIXME: set name + address or address only?
//rcube_utils::idn_to_utf8();
$addresses[$idx] = format_email_recipient($mailto, $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;
}
$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,
));
}
// Importance/Priority
if ($headers->priority) {
if ($headers->priority < 3) {
$result['importance'] = 2; // High
}
else if ($headers->priority > 3) {
$result['importance'] = 0; // Low
}
}
// get truncation
$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 if (!empty($prefs[Syncroton_Command_Sync::BODY_TYPE_HTML])) {
if (!empty($prefs[Syncroton_Command_Sync::BODY_TYPE_HTML]['truncationSize'])) {
$truncateAt = $prefs[Syncroton_Command_Sync::BODY_TYPE_HTML]['truncationSize'];
}
$airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_HTML;
}
else {
if (!empty($prefs[Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]) && !empty($prefs[Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]['truncationSize'])) {
$truncateAt = $prefs[Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT]['truncationSize'];
}
$airSyncBaseType = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
}
// 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
if (empty($prefs)) {
$messageBody = '';
$real_length = $message->size;
$truncateAt = 0;
$body_length = 0;
}
else if ($airSyncBaseType == Syncroton_Command_Sync::BODY_TYPE_MIME) {
$messageBody = $this->storage->get_raw_body($message->uid);
// 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);
}
if ($truncateAt !== null) {
if ($body_length > $truncateAt) {
$messageBody = substr($messageBody, 0, $truncateAt);
$body_length = $truncateAt;
}
$isTruncacted = 1;
}
else {
$isTruncacted = 0;
}
$body_params = array(
'type' => $airSyncBaseType,
'truncated' => $isTruncacted,
);
if ($isTruncated) {
$body_params['estimatedDataSize'] = $real_length;
}
$result['body'] = $this->setBody($messageBody, $body_params);
$result['nativeBodyType'] = intval($message->has_html_part(false));
// Message class
$result['messageClass'] = 'IPM.Note' . ($airSyncBaseType == Syncroton_Command_Sync::MIMESUPPORT_SEND_MIME ? '.SMIME' : '');
$result['contentClass'] = 'urn:content-classes:message';
// attachments
$attachments = array_merge($message->attachments, $message->inline_parts);
if (!empty($attachments)) {
$result['attachments'] = array();
foreach ($attachments as $attachment) {
$att = array();
$filename = $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'] = $attachment->content_id;
}
if (!empty($attachment->content_location)) {
$att['contentLocation'] = $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_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->backend->folders_list($this->device->deviceid, $this->modelName);
if (!is_array($list)) {
throw new Syncroton_Exception_Status_FolderSync(Syncroton_Esception_Status_FolderSync::FOLDER_SERVER_ERROR);
}
// device doesn't support multiple folders
if (!in_array(strtolower($this->device->devicetype), array('iphone', 'ipad', 'thundertine', 'windowsphone'))) {
// We'll return max. one folder of supported type
$result = array();
$types = $this->folder_types;
foreach ($list as $idx => $folder) {
if (isset($types[$folder['type']])) {
$folder_id = $types[$folder['type']];
$result[$folder_id] = array(
'displayName' => $folder_id,
'serverId' => $folder_id,
'parentId' => 0,
'type' => $folder['type'],
);
unset($types[$folder['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
*
* @return array Folder identifiers list
*/
protected function extractFolders($folder_id)
{
$list = $this->backend->folders_list($this->device->deviceid, $this->modelName);
$result = array();
if (!is_array($list)) {
throw new Syncroton_Exception_NotFound('Folder not found');
}
// device supports multiple folders?
if (in_array(strtolower($this->device->devicetype), array('iphone', 'ipad', 'thundertine', 'windowsphone'))) {
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);
$dstname = $this->backend->folder_id2name(array_shift($dest), $this->device->deviceid);
if (empty($msg)) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
}
if ($dstname === null) {
throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
}
if (!$this->storage->move_message($msg['uid'], $dstname, $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 = $this->storage->conn->data['COPYUID'];
if (is_array($copyuid) && ($uid = $copyuid[1])) {
return $uid;
}
}
/**
* add entry from xml data
*
* @param string $folderId
* @param Syncroton_Model_IEntry $entry
*
* @return array
*/
public function createEntry($folderId, Syncroton_Model_IEntry $entry)
{
}
/**
* update existing entry
*
* @param string $folderId
* @param string $serverId
* @param Syncroton_Model_IEntry $entry
* @return array
*/
public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
{
$msg = $this->parseMessageId($serverId);
$message = $this->getObject($serverId);
if (empty($message)) {
throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
}
$is_flagged = !empty($message->headers->flags['FLAGGED']);
// Read status change
if (isset($entry->read)) {
// here we update only Read flag
$flag = (((int)$entry->read != 1) ? 'UN' : '') . 'SEEN';
$this->storage->set_flag($msg['uid'], $flag, $msg['foldername']);
}
// Flag change
if (empty($entry->flag)) {
if ($is_flagged) {
$this->storage->set_flag($msg['uid'], 'UNFLAGGED', $msg['foldername']);
}
}
else if (!$is_flagged && !empty($entry->flag)) {
if ($entry->flag->flagType && preg_match('/^follow\s*up/i', $entry->flag->flagType)) {
$this->storage->set_flag($msg['uid'], 'FLAGGED', $msg['foldername']);
}
}
}
/**
* delete entry
*
* @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);
}
// move message to trash folder
if ($collection->deletesAsMoves
&& strlen($trash)
&& $trash != $msg['foldername']
&& $this->storage->folder_exists($trash)
) {
$this->storage->move_message($msg['uid'], $trash, $msg['foldername']);
}
// set delete flag
else {
$this->storage->set_flag($msg['uid'], 'DELETED', $msg['foldername']);
}
}
/**
* Send an email
*
- * @param resource|string $body MIME message
- * @param boolean $saveInSent Enables saving the sent message in Sent folder
+ * @param mixed $message MIME message
+ * @param boolean $saveInSent Enables saving the sent message in Sent folder
*
* @param throws Syncroton_Exception_Status
*/
- public function sendEmail($body, $saveInSent)
+ public function sendEmail($message, $saveInSent)
{
- $sent = $this->backend->send_message($body, $smtp_error);
+ 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, $body);
+ return $this->storage->save_message($sent_folder, $message->source());
}
}
}
/**
* 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
*
* @param 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);
+ $msg = $this->parseMessageId($itemId);
- if (!$message) {
+ if (empty($msg)) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
}
// Parse message
- list($headers, $body) = $this->backend->parse_mime($body, true);
+ $sync_msg = new kolab_sync_message($body);
- // Get original message body
+ // forward original message as attachment
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
- // @TODO: what about forward-as-attachment?
- $message_body = $this->getMessageBody($message, false);
- $message_body = trim($message_body);
+ $this->storage->set_folder($msg['foldername']);
+ $attachment = $this->storage->get_raw_body($msg['uid']);
- // Join bodies
- $body = rtrim($body) . "\n\n" . ltrim($message_body);
+ if (empty($attachment)) {
+ throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
+ }
- // @TODO: add attachments from the original message
+ $sync_msg->add_attachment($attachment, array(
+ 'encoding' => '8bit',
+ 'content_type' => 'message/rfc822',
+ 'disposition' => 'inline',
+ //'name' => 'message.eml',
+ ));
}
- // Create complete message source
- $body = $this->backend->build_mime($headers, $body);
-
// Send message
- $sent = $this->sendEmail($body, $saveInSent);
+ $sent = $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
*
* @param throws Syncroton_Exception_Status
*/
public function replyEmail($itemId, $body, $saveInSent, $replaceMime)
{
$msg = $this->parseMessageId($itemId);
$message = $this->getObject($itemId);
if (!$message) {
throw new Syncroton_Exception_Status(Syncroton_Exception_Status::ITEM_NOT_FOUND);
}
- // Parse message
- // @TODO: messages with attachments
- list($headers, $body) = $this->backend->parse_mime($body, true);
+ $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
- $body = rtrim($body) . "\n" . ltrim($message_body);
- }
-
- // Add References header
- if (empty($headers['References'])) {
- $headers['References'] = trim($message->headers->references . ' ' . $message->headers->messageID);
+ $sync_msg->append("\n" . ltrim($message_body));
}
- // Create complete message source
- $body = $this->backend->build_mime($headers, $body);
-
// Send message
- $sent = $this->sendEmail($body, $saveInSent);
+ $sent = $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']);
}
}
/**
* Search for existing entries
*
* @param string $folderid
* @param array $filter
* @param int $result_type Type of the result (see RESULT_* constants)
*
* @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);
$filter_str = 'ALL UNDELETED';
// convert filter into one IMAP search string
foreach ($filter as $idx => $filter_item) {
if (is_array($filter_item)) {
// @TODO
// convert 'changed' entries into IMAP search string
// for now we just return empty result
return $result_type == self::RESULT_COUNT ? 0 : array();
}
else {
$filter_str .= ' ' . $filter_item;
}
}
$result = $result_type == self::RESULT_COUNT ? 0 : array();
// no sorting for best performance
$sort_by = null;
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, $filter_str);
if (!($search instanceof rcube_result_index)) {
continue;
}
switch ($result_type) {
case self::RESULT_COUNT:
$result += (int) $search->count();
break;
case self::RESULT_UID:
if ($uids = $search->get()) {
foreach ($uids as $idx => $uid) {
$uids[$idx] = $this->createMessageId($folderid, $uid);
}
$result = array_merge($result, $uids);
}
break;
/*
case self::RESULT_OBJECT:
default:
if ($objects = $folder->select($filter)) {
$result = array_merge($result, $objects);
}
*/
}
}
return $result;
}
/**
* 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();
// no sorting for best performance
$sort_by = null;
// @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->createMessageId($folderid, $uid),
'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->backend->folders_list($this->device->deviceid, $this->modelName);
if (is_array($folders)) {
$folders = array_keys($folders);
}
}
return array($folders, $search_str);
}
/**
* Fetches the entry from the backend
*/
protected function getObject($entryid, &$folder = null)
{
$message = $this->parseMessageId($entryid);
if (empty($message)) {
// @TODO: exception?
return null;
}
// set current folder
$this->storage->set_folder($message['foldername']);
// get message
$message = new rcube_message($message['uid']);
return $message;
}
/**
* @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_content($part_id);
$content_type = $part->mimetype;
return new Syncroton_Model_FileReference(array(
'contentType' => $content_type,
'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'];
}
list($folderid, $uid) = explode('::', $entryid);
$foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
if ($foldername === null || $foldername === false) {
// @TODO exception?
return null;
}
return array(
'uid' => $uid,
'folderid' => $folderid,
'foldername' => $foldername,
);
}
/**
* Creates entry ID of the message
*/
public function createMessageId($folderid, $uid)
{
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)
{
// Check if we have enough memory to handle the message in it
// @FIXME: we need up to 5x more memory than the body
if (!rcube_utils::mem_check($part->size * 5)) {
return '';
}
if (empty($part->ctype_parameters) || empty($part->ctype_parameters['charset'])) {
$part->ctype_parameters['charset'] = $message->headers->charset;
}
// fetch part if not available
if (!isset($part->body)) {
$part->body = $message->get_part_content($part->mime_id);
}
// message is cached but not exists, or other error
if ($part->body === false) {
return '';
}
$body = $part->body;
if ($html) {
if ($part->ctype_secondary == 'html') {
}
else {
$body = '<pre>' . $body . '</pre>';
}
}
else {
if ($part->ctype_secondary == 'html') {
$txt = new html2text($body, false, true);
$body = $txt->get_text();
}
else {
if ($part->ctype_secondary == 'plain' && $part->ctype_parameters['format'] == 'flowed') {
$body = rcube_mime::unfold_flowed($body);
}
}
}
return $body;
}
public static function charset_to_cp($charset)
{
// @TODO: ?????
// The body is converted to utf-8 in get_part_content(), 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 ($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;
}
}
diff --git a/lib/kolab_sync_message.php b/lib/kolab_sync_message.php
new file mode 100644
index 0000000..a31a6c1
--- /dev/null
+++ b/lib/kolab_sync_message.php
@@ -0,0 +1,448 @@
+<?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> |
+ +--------------------------------------------------------------------------+
+*/
+
+class kolab_sync_message
+{
+ protected $headers = array();
+ protected $body;
+ protected $ctype;
+ protected $ctype_params = array();
+
+ /**
+ * Constructor
+ *
+ * @param string|resource $source MIME message source
+ */
+ function __construct($source)
+ {
+ $this->parse_mime($source);
+ }
+
+ /**
+ * Returns message headers
+ *
+ * @return array Message headers
+ */
+ public function headers()
+ {
+ return $this->headers;
+ }
+
+ public function source()
+ {
+ $headers = array();
+
+ // Build the message back
+ foreach ($this->headers as $header => $header_value) {
+ $headers[$header] = $header . ': ' . $header_value;
+ }
+
+ return trim(implode("\r\n", $headers)) . "\r\n\r\n" . ltrim($this->body);
+ // @TODO: work with file streams
+ }
+
+ /**
+ * Appends text at the end of the message body
+ *
+ * @todo: HTML support
+ *
+ * @param string $text Text to append
+ * @param string $charset Text charset
+ */
+ public function append($text, $charset = null)
+ {
+ if ($this->ctype == 'text/plain') {
+ // decode body
+ $body = $this->decode($this->body, $this->headers['Content-Transfer-Encoding']);
+ $body = rcube_charset::convert($body, $this->ctype_params['charset'], $charset);
+ // append text
+ $body .= $text;
+ // encode and save
+ $body = rcube_charset::convert($body, $charset, $this->ctype_params['charset']);
+ $this->body = $this->encode($body, $this->headers['Content-Transfer-Encoding']);
+ }
+ }
+
+ /**
+ * Adds attachment to the message
+ *
+ * @param string $body Attachment body (not encoded)
+ * @param string $params Attachment parameters (Mail_mimePart format)
+ */
+ public function add_attachment($body, $params = array())
+ {
+ // convert the message into multipart/mixed
+ if ($this->ctype != 'multipart/mixed') {
+ $boundary = '_' . md5(rand() . microtime());
+
+ $this->body = "--$boundary\r\n"
+ ."Content-Type: " . $this->headers['Content-Type']."\r\n"
+ ."Content-Transfer-Encoding: " . $this->headers['Content-Transfer-Encoding']."\r\n"
+ ."\r\n" . trim($this->body) . "\r\n"
+ ."--$boundary\r\n";
+
+ $this->ctype = 'multipart/mixed';
+ $this->ctype_params = array('boundary' => $boundary);
+ unset($this->headers['Content-Transfer-Encoding']);
+ $this->save_content_type($this->ctype, $this->ctype_params);
+ }
+
+ // make sure MIME-Version header is set, it's required by some servers
+ if (empty($this->headers['MIME-Version'])) {
+ $this->headers['MIME-Version'] = '1.0';
+ }
+
+ $boundary = $this->ctype_params['boundary'];
+
+ $part = new Mail_mimePart($body, $params);
+ $body = $part->encode();
+
+ foreach ($body['headers'] as $name => $value) {
+ $body['headers'][$name] = $name . ': ' . $value;
+ }
+
+ // add the attachment to the end of the message
+ $this->body = rtrim($this->body) . "\r\n"
+ .implode("\r\n", $body['headers']) . "\r\n\r\n"
+ .$body['body'] . "\r\n--$boundary\r\n";
+ }
+
+ /**
+ * Sets the value of specified message header
+ *
+ * @param string $name Header name
+ * @param string $value Header value
+ */
+ public function set_header($name, $value)
+ {
+ $name = $this->normalize_header_name($name);
+
+ if ($name != 'Content-Type') {
+ $this->headers[$name] = $value;
+ }
+ }
+
+ /**
+ * Send the given message using the configured method.
+ *
+ * @param array $smtp_error SMTP error array (reference)
+ * @param array $smtp_opts SMTP options (e.g. DSN request)
+ *
+ * @return boolean Send status.
+ */
+ public function send(&$smtp_error = null, $smtp_opts = null)
+ {
+ $rcube = rcube::get_instance();
+ $headers = $this->headers;
+
+ $mailto = $headers['To'];
+
+ $headers['User-Agent'] .= sprintf('%s v.%.1f', $rcube->app_name, kolab_sync::VERSION);
+ if ($agent = $rcube->config->get('useragent')) {
+ $headers['User-Agent'] .= '/' . $agent;
+ }
+
+ if (empty($headers['From'])) {
+ $headers['From'] = $this->get_identity();
+ }
+ if (empty($headers['Message-ID'])) {
+ $headers['Message-ID'] = $this->gen_message_id();
+ }
+
+ // remove empty headers
+ $headers = array_filter($headers);
+
+ // send thru SMTP server using custom SMTP library
+ if ($rcube->config->get('smtp_server')) {
+ $smtp_headers = $headers;
+ // generate list of recipients
+ $recipients = array();
+
+ if (!empty($headers['To']))
+ $recipients[] = $headers['To'];
+ if (!empty($headers['Cc']))
+ $recipients[] = $headers['Cc'];
+ if (!empty($headers['Bcc']))
+ $recipients[] = $headers['Bcc'];
+
+ // remove Bcc header
+ unset($smtp_headers['Bcc']);
+
+ // send message
+ if (!is_object($rcube->smtp)) {
+ $rcube->smtp_init(true);
+ }
+
+ $sent = $rcube->smtp->send_mail($headers['From'], $recipients, $smtp_headers, $this->body, $smtp_opts);
+ $smtp_response = $rcube->smtp->get_response();
+ $smtp_error = $rcube->smtp->get_error();
+
+ // log error
+ if (!$sent) {
+ rcube::raise_error(array('code' => 800, 'type' => 'smtp',
+ 'line' => __LINE__, 'file' => __FILE__,
+ 'message' => "SMTP error: ".join("\n", $smtp_response)), true, false);
+ }
+ }
+ // send mail using PHP's mail() function
+ else {
+ $mail_headers = $headers;
+ $delim = $rcube->config->header_delimiter();
+ $subject = $headers['Subject'];
+ $to = $headers['To'];
+
+ // unset some headers because they will be added by the mail() function
+ unset($mail_headers['To'], $mail_headers['Subject']);
+
+ // #1485779
+ if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
+ if (preg_match_all('/<([^@]+@[^>]+)>/', $to, $m)) {
+ $to = implode(', ', $m[1]);
+ }
+ }
+
+ foreach ($mail_headers as $header => $header_value) {
+ $mail_headers[$header] = $header . ': ' . $header_value;
+ }
+ $header_str = rtrim(implode("\r\n", $mail_headers));
+
+ if ($delim != "\r\n") {
+ $header_str = str_replace("\r\n", $delim, $header_str);
+ $msg_body = str_replace("\r\n", $delim, $this->body);
+ $to = str_replace("\r\n", $delim, $to);
+ $subject = str_replace("\r\n", $delim, $subject);
+ }
+
+ if (ini_get('safe_mode')) {
+ $sent = mail($to, $subject, $msg_body, $header_str);
+ }
+ else {
+ $sent = mail($to, $subject, $msg_body, $header_str, "-f$from");
+ }
+ }
+
+ if ($sent) {
+ $rcube->plugins->exec_hook('message_sent', array('headers' => $headers, 'body' => $this->body));
+
+ // remove MDN headers after sending
+ unset($headers['Return-Receipt-To'], $headers['Disposition-Notification-To']);
+
+ // get all recipients
+ if ($headers['Cc'])
+ $mailto .= ' ' . $headers['Cc'];
+ if ($headers['Bcc'])
+ $mailto .= ' ' . $headers['Bcc'];
+ if (preg_match_all('/<([^@]+@[^>]+)>/', $mailto, $m))
+ $mailto = implode(', ', array_unique($m[1]));
+
+ if ($rcube->config->get('smtp_log')) {
+ rcube::write_log('sendmail', sprintf("User %s [%s]; Message for %s; %s",
+ $rcube->get_user_name(),
+ $_SERVER['REMOTE_ADDR'],
+ $mailto,
+ !empty($smtp_response) ? join('; ', $smtp_response) : ''));
+ }
+ }
+
+ unset($headers['Bcc']);
+
+ $this->headers = $headers;
+
+ return $sent;
+ }
+ /**
+ * MIME message parser
+ *
+ * @param string|resource $message MIME message source
+ * @param bool $decode_body Enables body decoding
+ *
+ * @return array Message headers array and message body
+ */
+ protected function parse_mime($message)
+ {
+ // @TODO: work with stream, to workaround memory issues with big messages
+ if (is_resource($message)) {
+ $message = stream_get_contents($message);
+ }
+
+ list($headers, $message) = preg_split('/\r?\n\r?\n/', $message, 2, PREG_SPLIT_NO_EMPTY);
+
+ // Parse headers
+ $headers = str_replace("\r\n", "\n", $headers);
+ $headers = explode("\n", trim($headers));
+
+ $ln = 0;
+ $lines = array();
+
+ foreach ($headers as $line) {
+ if (ord($line[0]) <= 32) {
+ $lines[$ln] .= (empty($lines[$ln]) ? '' : "\n") . $line;
+ }
+ else {
+ $lines[++$ln] = trim($line);
+ }
+ }
+
+ // Unify char-case of header names
+ $headers = array();
+ foreach ($lines as $line) {
+ list($field, $string) = explode(':', $line, 2);
+ $field = $this->normalize_header_name($field);
+ $headers[$field] = trim($string);
+ }
+
+ // parse Content-Type header
+ $ctype_parts = preg_split('/[; ]+/', $headers['Content-Type']);
+ $this->ctype = strtolower(array_shift($ctype_parts));
+ foreach ($ctype_parts as $part) {
+ if (preg_match('/^([a-z-_]+)\s*=\s*(.+)$/i', trim($part), $m)) {
+ $this->ctype_params[strtolower($m[1])] = trim($m[2], '"');
+ }
+ }
+
+ if (!empty($headers['Content-Transfer-Encoding'])) {
+ $headers['Content-Transfer-Encoding'] = strtolower($headers['Content-Transfer-Encoding']);
+ }
+
+ $this->headers = $headers;
+ $this->body = $message;
+ }
+
+ protected function normalize_header_name($name)
+ {
+ $headers_map = array(
+ 'subject' => 'Subject',
+ 'from' => 'From',
+ 'to' => 'To',
+ 'cc' => 'Cc',
+ 'bcc' => 'Bcc',
+ 'message-id' => 'Message-ID',
+ 'references' => 'References',
+ 'content-type' => 'Content-Type',
+ 'content-transfer-encoding' => 'Content-Transfer-Encoding',
+ );
+
+ $name_lc = strtolower($name);
+
+ return isset($headers_map[$name_lc]) ? $headers_map[$name_lc] : $name;
+ }
+
+ /**
+ * Encodes message/part body
+ *
+ * @param string $body Message/part body
+ * @param string $encoding Content encoding
+ *
+ * @return string Encoded body
+ */
+ protected function encode($body, $encoding)
+ {
+ switch ($encoding) {
+ case 'base64':
+ $body = base64_encode($body);
+ $body = chunk_split($body, 76, "\r\n");
+ break;
+ case 'quoted-printable':
+ $body = quoted_printable_encode($body);
+ break;
+ }
+
+ return $body;
+ }
+
+ /**
+ * Decodes message/part body
+ *
+ * @param string $body Message/part body
+ * @param string $encoding Content encoding
+ *
+ * @return string Decoded body
+ */
+ protected function decode($body, $encoding)
+ {
+ $body = str_replace("\r\n", "\n", $body);
+
+ switch ($encoding) {
+ case 'base64':
+ $body = base64_decode($body);
+ break;
+ case 'quoted-printable':
+ $body = quoted_printable_decode($body);
+ break;
+ }
+
+ return $body;
+ }
+
+ /**
+ * Returns email address string from default identity of the current user
+ */
+ protected function get_identity()
+ {
+ $user = kolab_sync::get_instance()->user;
+
+ if ($identity = $user->get_identity()) {
+ return format_email_recipient(format_email($identity['email']), $identity['name']);
+ }
+ }
+
+ /**
+ * Unique Message-ID generator.
+ *
+ * @return string Message-ID
+ */
+ protected function gen_message_id()
+ {
+ $user = kolab_sync::get_instance()->user;
+ $local_part = md5(uniqid('rcmail'.mt_rand(),true));
+ $domain_part = $user->get_username('domain');
+
+ // Try to find FQDN, some spamfilters doesn't like 'localhost' (#1486924)
+ if (!preg_match('/\.[a-z]+$/i', $domain_part)) {
+ foreach (array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']) as $host) {
+ $host = preg_replace('/:[0-9]+$/', '', $host);
+ if ($host && preg_match('/\.[a-z]+$/i', $host)) {
+ $domain_part = $host;
+ }
+ }
+ }
+
+ return sprintf('<%s@%s>', $local_part, $domain_part);
+ }
+
+ protected function save_content_type($ctype, $params = array())
+ {
+ $this->ctype = $ctype;
+ $this->ctype_params = $params;
+
+ $this->headers['Content-Type'] = $ctype;
+ if (!empty($params)) {
+ foreach ($params as $name => $value) {
+ $this->headers['Content-Type'] .= sprintf('; %s="%s"', $name, $value);
+ }
+ }
+ }
+
+}
diff --git a/tests/message.php b/tests/message.php
new file mode 100644
index 0000000..1cafa5f
--- /dev/null
+++ b/tests/message.php
@@ -0,0 +1,99 @@
+<?php
+
+class message extends PHPUnit_Framework_TestCase
+{
+ function setUp()
+ {
+ }
+
+
+ /**
+ * Test message parsing and headers setting
+ */
+ function test_headers()
+ {
+ $source = file_get_contents(TESTS_DIR . '/src/mail.plain');
+ $message = new kolab_sync_message($source);
+ $headers = $message->headers();
+
+ $this->assertArrayHasKey('MIME-Version', $headers);
+ $this->assertCount(8, $headers);
+ $this->assertEquals('kolab@domain.tld', $headers['To']);
+
+ // test set_header()
+ $message->set_header('to', 'test@domain.tld');
+ $headers = $message->headers();
+
+ $this->assertCount(8, $headers);
+ $this->assertEquals('test@domain.tld', $headers['To']);
+ }
+
+ /**
+ * Test message parsing
+ */
+ function test_source()
+ {
+ $source = file_get_contents(TESTS_DIR . '/src/mail.plain');
+ $message = new kolab_sync_message($source);
+ $result = $message->source();
+
+ $this->assertEquals($source, str_replace("\r\n", "\n", $result));
+ }
+
+ /**
+ * Test adding attachments to the message
+ */
+ function test_attachment()
+ {
+ $source = file_get_contents(TESTS_DIR . '/src/mail.plain');
+ $mixed = file_get_contents(TESTS_DIR . '/src/mail.plain.mixed');
+ $mixed2 = file_get_contents(TESTS_DIR . '/src/mail.mixed');
+
+ // test adding attachment to text/plain message
+ $message = new kolab_sync_message($source);
+ $message->add_attachment('aaa', array(
+ 'content_type' => 'text/plain',
+ 'encoding' => '8bit',
+ ));
+
+ $result = $message->source();
+ $result = str_replace("\r\n", "\n", $result);
+ if (preg_match('/boundary="([^"]+)"/', $result, $m)) {
+ $mixed = str_replace('BOUNDARY', $m[1], $mixed);
+ }
+
+ $this->assertEquals($mixed, $result);
+
+ // test adding attachment to multipart/mixed message
+ $message = new kolab_sync_message($mixed);
+ $message->add_attachment('aaa', array(
+ 'content_type' => 'text/plain',
+ 'encoding' => 'base64',
+ ));
+
+ $result = $message->source();
+ $result = str_replace("\r\n", "\n", $result);
+ if (preg_match('/boundary="([^"]+)"/', $result, $m)) {
+ $mixed2 = str_replace('BOUNDARY', $m[1], $mixed2);
+ }
+
+ $this->assertEquals($mixed2, $result);
+ }
+
+ /**
+ * Test appending a text to the message
+ */
+ function test_append()
+ {
+ // test appending text to text/plain message
+ $source = file_get_contents(TESTS_DIR . '/src/mail.plain');
+ $append = file_get_contents(TESTS_DIR . '/src/mail.plain.append');
+
+ $message = new kolab_sync_message($source);
+ $message->append('a');
+
+ $result = $message->source();
+ $result = str_replace("\r\n", "\n", $result);
+ $this->assertEquals($append, $result);
+ }
+}
diff --git a/tests/phpunit.xml b/tests/phpunit.xml
index 34fab9a..e49675a 100644
--- a/tests/phpunit.xml
+++ b/tests/phpunit.xml
@@ -1,10 +1,11 @@
<phpunit backupGlobals="false"
bootstrap="bootstrap.php"
colors="true">
<testsuites>
<testsuite name="All Tests">
<file>body_converter.php</file>
<file>data.php</file>
+ <file>message.php</file>
</testsuite>
</testsuites>
</phpunit>
diff --git a/tests/src/mail.alternative b/tests/src/mail.alternative
new file mode 100644
index 0000000..555dda6
--- /dev/null
+++ b/tests/src/mail.alternative
@@ -0,0 +1,27 @@
+MIME-Version: 1.0
+content-class:
+From:
+Subject: eee
+Date: Sun, 2 Sep 2012 11:41:14 +0200
+Importance: normal
+X-Priority: 3
+To: User <user@domain.tld>
+Content-Type: multipart/alternative;
+ boundary="_3EE5AD33-49F4-2808-9917-6969FE639CA7_"
+
+--_3EE5AD33-49F4-2808-9917-6969FE639CA7_
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/plain; charset="iso-8859-1"
+
+tt=
+
+--_3EE5AD33-49F4-2808-9917-6969FE639CA7_
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset="iso-8859-1"
+
+<HTML><HEAD><META HTTP-EQUIV=3D'Content-Type' CONTENT=3D'text/html; charset=
+=3Diso-8859-1'></HEAD><BODY><SPAN style=3D'FONT-SIZE: 10pt; FONT-FAMILY: Ar=
+ial; FONT-WEIGHT:Normal;'>tt</SPAN></BODY></HTML>=
+
+--_3EE5AD33-49F4-2808-9917-6969FE639CA7_--
+
diff --git a/tests/src/mail.alternative2 b/tests/src/mail.alternative2
new file mode 100644
index 0000000..bacf4fc
--- /dev/null
+++ b/tests/src/mail.alternative2
@@ -0,0 +1,30 @@
+Date: Thu, 16 Aug 2012 07:38:43 +0000
+Subject: Re: test
+Message-ID: <1h4e6qv8o4ib0mhoj2hmyj0s.1345102723450@email.android.com>
+From: user@domain.tld
+To: Kolab User <kolab@domain.tld>
+MIME-Version: 1.0
+Content-Type: multipart/alternative; boundary="--_com.android.email_15071870071490"
+
+----_com.android.email_15071870071490
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+Q3NkZnNkZnNkZgoKIkEuTC5FLkMiIDxhbGVjQGFsZWMucGw+IHdyb3RlOgoKPnRlc3QgLS0gQWxl
+a3NhbmRlciAnQS5MLkUuQycgTWFjaG5pYWsgTEFOIE1hbmFnZW1lbnQgU3lzdGVtIERldmVsb3Bl
+ciBbaHR0cDovL2xtcy5vcmcucGxdIFJvdW5kY3ViZSBXZWJtYWlsIERldmVsb3BlciBbaHR0cDov
+L3JvdW5kY3ViZS5uZXRdIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0t
+LS0tLS0tLS0tLSBQR1A6IDE5MzU5REMxIEBAIEdHOiAyMjc1MjUyIEBAIFdXVzogaHR0cDovL2Fs
+ZWMucGwg
+----_com.android.email_15071870071490
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: base64
+
+Q3NkZnNkZnNkZjxicj48YnI+JnF1b3Q7QS5MLkUuQyZxdW90OyAmbHQ7YWxlY0BhbGVjLnBsJmd0
+OyB3cm90ZTo8YnI+PGJyPjxwcmU+dGVzdAoKLS0gCkFsZWtzYW5kZXIgJ0EuTC5FLkMnIE1hY2hu
+aWFrCkxBTiBNYW5hZ2VtZW50IFN5c3RlbSBEZXZlbG9wZXIgW2h0dHA6Ly9sbXMub3JnLnBsXQpS
+b3VuZGN1YmUgV2VibWFpbCBEZXZlbG9wZXIgIFtodHRwOi8vcm91bmRjdWJlLm5ldF0KLS0tLS0t
+LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tClBHUDogMTkzNTlE
+QzEgQEAgR0c6IDIyNzUyNTIgQEAgV1dXOiBodHRwOi8vPHNwYW4gc3R5bGU9ImJhY2tncm91bmQt
+Y29sb3I6ICNmZmZmMDAiPmFsZWM8L3NwYW4+LnBsCjwvcHJlPg==
+----_com.android.email_15071870071490--
diff --git a/tests/src/mail.mixed b/tests/src/mail.mixed
new file mode 100644
index 0000000..be5f4fb
--- /dev/null
+++ b/tests/src/mail.mixed
@@ -0,0 +1,24 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869@email.android.com>
+From: user@domain.tld
+To: kolab@domain.tld
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="BOUNDARY"
+
+--BOUNDARY
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVl
+--BOUNDARY
+Content-Transfer-Encoding: 8bit
+Content-Type: text/plain
+
+aaa
+--BOUNDARY
+Content-Transfer-Encoding: base64
+Content-Type: text/plain
+
+YWFh
+--BOUNDARY
diff --git a/tests/src/mail.plain b/tests/src/mail.plain
new file mode 100644
index 0000000..9b6129e
--- /dev/null
+++ b/tests/src/mail.plain
@@ -0,0 +1,10 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869@email.android.com>
+From: user@domain.tld
+To: kolab@domain.tld
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVl
diff --git a/tests/src/mail.plain.append b/tests/src/mail.plain.append
new file mode 100644
index 0000000..ddffadf
--- /dev/null
+++ b/tests/src/mail.plain.append
@@ -0,0 +1,10 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869@email.android.com>
+From: user@domain.tld
+To: kolab@domain.tld
+MIME-Version: 1.0
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVlYQ==
diff --git a/tests/src/mail.plain.mixed b/tests/src/mail.plain.mixed
new file mode 100644
index 0000000..e7bc4e2
--- /dev/null
+++ b/tests/src/mail.plain.mixed
@@ -0,0 +1,19 @@
+Date: Thu, 09 Aug 2012 13:18:31 +0000
+Subject: Fwd: test html xx
+Message-ID: <qma5x35ckynysjn2ee1jwwn8.1344518311869@email.android.com>
+From: user@domain.tld
+To: kolab@domain.tld
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="BOUNDARY"
+
+--BOUNDARY
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: base64
+
+ZWVl
+--BOUNDARY
+Content-Transfer-Encoding: 8bit
+Content-Type: text/plain
+
+aaa
+--BOUNDARY

File Metadata

Mime Type
text/x-diff
Expires
Fri, Mar 20, 1:32 AM (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
458834
Default Alt Text
(120 KB)

Event Timeline