diff --git a/app/model/kolabobject.py b/app/model/kolabobject.py index 60c1664..1a102d4 100644 --- a/app/model/kolabobject.py +++ b/app/model/kolabobject.py @@ -1,349 +1,349 @@ # -*- coding: utf-8 -*- # # Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) # # Thomas Bruederli # # 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 . # import json import pytz import hashlib import datetime import logging from dateutil.parser import parse as parse_date from pykolab.xml.utils import compute_diff from collections import OrderedDict from email import message_from_string from app import storage log = logging.getLogger('model.kolabobject') class KolabObject(object): """ Base Model class for accessing Kolab Groupware Object data """ folder_type = 'unknown' x_kolab_type = 'application/x-vnd.kolab.*' def __init__(self, env={}): from flask import current_app self.env = env self.config = current_app.config self.storage = storage.factory() def created(self, uid, mailbox, msguid=None): """ Provide created date and user """ changelog = self._object_changelog(uid, mailbox, msguid, 1) if changelog and len(changelog) > 0: for change in changelog: if change['op'] == 'APPEND': change['uid'] = uid change.pop('op', None) return change return False def lastmodified(self, uid, mailbox, msguid=None): """ Provide last change information """ changelog = self._object_changelog(uid, mailbox, msguid, -3) if changelog and len(changelog) > 0: for change in changelog: if change['op'] == 'APPEND': change['uid'] = uid change.pop('op', None) return change return False def changelog(self, uid, mailbox, msguid=None): """ Full changelog """ changelog = self._object_changelog(uid, mailbox, msguid) if changelog: return dict(uid=uid, changes=changelog) return False def get(self, uid, rev, mailbox, msguid=None): """ Retrieve an old revision """ obj = self._get(uid, mailbox, msguid, rev) if obj is not None: return dict(uid=uid, rev=rev, xml=str(obj), mailbox=mailbox) return False def _get(self, uid, mailbox, msguid, rev): """ Get an old revision and return the pykolab.xml object """ obj = False rec = self.storage.get_revision(uid, self._resolve_mailbox_uri(mailbox), msguid, rev) if rec is not None: raw = self.storage.get_message_data(rec) try: message = message_from_string(raw.encode('utf8','replace')) obj = self._object_from_message(message) or False except Exception, e: log.warning("Failed to parse mime message for UID %s @%s: %r", uid, rev, e) if obj is False: log.warning("Failed to parse mime message for UID %s @%s", uid, rev) return obj def diff(self, uid, rev1, rev2, mailbox, msguid=None): """ Compare two revisions of an object and return a list of property changes """ rev_old = rev1 rev_new = rev2 if rev_old >= rev_new: raise ValueError("Invalid argument 'rev'") old = self._get(uid, mailbox, msguid, rev_old) if old == False: raise ValueError("Object %s @rev:%d not found" % (uid, rev_old)) new = self._get(uid, mailbox, msguid, rev_new) if new == False: raise ValueError("Object %s @rev:%d not found" % (uid, rev_new)) - return dict(uid=uid, rev=rev_new, changes=convert2primitives(compute_diff(old.to_dict(), new.to_dict(), True))) + return dict(uid=uid, rev=rev_new, changes=convert2primitives(compute_diff(old.to_dict(), new.to_dict(), False))) def rawdata(self, uid, mailbox, rev, msguid=None): """ Get the full message payload of an old revision """ rec = self.storage.get_revision(uid, self._resolve_mailbox_uri(mailbox), msguid, rev) if rec is not None: return self.storage.get_message_data(rec) return False def _object_from_message(self, message): """ To be implemented in derived classes """ return None def _object_changelog(self, uid, mailbox, msguid, limit=None): """ Query storage for changelog events related to the given UID """ # this requires a user context if not self.env.has_key('REQUEST_USER') or not self.env['REQUEST_USER']: return None # fetch event log from storage eventlog = self.storage.get_events(uid, self._resolve_mailbox_uri(mailbox), msguid, limit) # convert logstash entries into a sane changelog event_op_map = { 'MessageNew': 'APPEND', 'MessageAppend': 'APPEND', 'MessageTrash': 'DELETE', 'MessageMove': 'MOVE', } last_append_uid = 0 result = [] if eventlog is not None: for log in eventlog: # filter MessageTrash following a MessageAppend event (which is an update operation) if log['event'] == 'MessageTrash' and last_append_uid > int(log['uidset']): continue # remember last appended message uid if log['event'] == 'MessageAppend' and log.has_key('uidset'): last_append_uid = int(log['uidset']) # compose log entry to return logentry = { 'rev': log.get('revision', None), 'op': event_op_map.get(log['event'], 'UNKNOWN'), 'mailbox': self._convert_mailbox_uri(log.get('mailbox', None)) } try: timestamp = parse_date(log['timestamp']) logentry['date'] = datetime.datetime.strftime(timestamp, "%Y-%m-%dT%H:%M:%SZ") except: logentry['date'] = log['timestamp'] # TODO: translate mailbox identifier back to a relative folder path? logentry['user'] = self._get_user_info(log) result.append(logentry) return result def _resolve_username(self, user): """ Resovle the given username to the corresponding nsuniqueid from LDAP """ # find existing entry in our storage backend result = self.storage.get_user(username=user) if result and result.has_key('id'): # TODO: cache this lookup in memory? return result['id'] # fall-back: return md5 sum of the username to make usernames work as fields/keys in elasticsearch return hashlib.md5(user).hexdigest() def _get_user_info(self, rec): """ Return user information (name, email) related to the given log entry """ if rec.has_key('user_id'): # get real user name from rec['user_id'] user = self.storage.get_user(id=rec['user_id']) if user is not None: return "%(cn)s <%(user)s>" % user if rec.has_key('user'): return rec['user'] elif rec['event'] == 'MessageAppend' and rec['headers'].has_key('From'): # fallback to message headers return rec['headers']['From'][0] return 'unknown' def _resolve_mailbox_uri(self, mailbox): """ Convert the given mailbox string into an absolute URI regarding the context of the requesting user. """ # this requires a user context if not self.env.has_key('REQUEST_USER') or not self.env['REQUEST_USER']: return mailbox if mailbox is None: return None # mailbox already is an absolute path if mailbox.startswith('user/') or mailbox.startswith('shared/'): return mailbox domain = '' user = self.env['REQUEST_USER'] if '@' in user: (user,_domain) = user.split('@', 1) domain = '@' + _domain owner = user path = '/' + mailbox # TODO: make this configurable or read from IMAP shared_prefix = 'Shared Folders/' others_prefix = 'Other Users/' imap_delimiter = '/' # case: shared folder if mailbox.startswith(shared_prefix): return mailbox[len(shared_prefix):] + domain # case: other users folder if mailbox.startswith(others_prefix): (owner, subpath) = mailbox[len(others_prefix):].split(imap_delimiter, 1) path = imap_delimiter + subpath if mailbox.upper() == 'INBOX': path = '' # default: personal namespace folder return 'user/' + owner + path + domain def _convert_mailbox_uri(self, mailbox): """ Convert the given absolute mailbox URI into a relative folder name regarding the context of the requesting user. """ if mailbox is None: return None # this requires a user context request_user = str(self.env.get('REQUEST_USER', '')).lower() # TODO: make this configurable or read from IMAP shared_prefix = 'Shared Folders' others_prefix = 'Other Users' imap_delimiter = '/' domain = '' if '@' in mailbox: (folder,domain) = mailbox.split('@', 1) else: folder = mailbox if folder.startswith('user/'): parts = folder.split(imap_delimiter, 2) if len(parts) > 2: (prefix,user,path) = parts else: (prefix,user) = parts path = '' if len(path) == 0: path = 'INBOX' if not (user + '@' + domain).lower() == request_user: folder = imap_delimiter.join([others_prefix, user, path]) else: folder = path elif folder.startswith('shared/'): folder = imap_delimiter.join([shared_prefix, folder]) return folder ##### Utility functions def convert2primitives(struct): """ Convert complex types like datetime into primitives which can be serialized into JSON """ out = None if isinstance(struct, datetime.datetime): tz = 'Z' if struct.tzinfo == pytz.utc else '%z' out = struct.strftime('%Y-%m-%dT%H:%M:%S' + tz) elif isinstance(struct, datetime.date): out = struct.strftime('%Y-%m-%d') elif isinstance(struct, list): out = [convert2primitives(x) for x in struct] elif isinstance(struct, OrderedDict): out = OrderedDict([(key,convert2primitives(struct[key])) for key in struct.keys()]) elif isinstance(struct, dict): out = dict(zip(struct.keys(), map(convert2primitives, struct.values()))) else: out = struct return out diff --git a/app/storage/riak_storage.py b/app/storage/riak_storage.py index d5861e7..793f256 100644 --- a/app/storage/riak_storage.py +++ b/app/storage/riak_storage.py @@ -1,369 +1,368 @@ # -*- coding: utf-8 -*- # # Copyright 2015 Kolab Systems AG (http://www.kolabsys.com) # # Thomas Bruederli # # 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 . # import logging, datetime, urllib, urlparse from riak import RiakClient from riak.mapreduce import RiakKeyFilter, RiakMapReduce from dateutil.parser import parse as parse_date from flask import current_app from . import AbstractStorage conf = current_app.config log = logging.getLogger('storage') class RiakStorage(AbstractStorage): bucket_types = { 'users': 'egara-lww', 'users-current': 'egara-unique', 'imap-events': 'egara-lww', 'imap-folders': 'egara-lww', 'imap-folders-current': 'egara-unique', 'imap-message-timeline': 'egara-lww' } def __init__(self, *args, **kw): riak_host = 'localhost' riak_port = 8098 self.client = RiakClient( protocol='http', host=conf['STORAGE'].get('riak_host', riak_host), http_port=conf['STORAGE'].get('riak_port', riak_port) ) self.client.set_decoder('application/octet-stream', self._decode_binary) def _decode_binary(self, data): return str(data).encode("utf-8") def _get_bucket(self, bucketname): _type = self.bucket_types.get(bucketname, None) if _type: return self.client.bucket_type(_type).bucket(bucketname) return None def get(self, key, index, doctype=None, fields=None, **kw): """ Standard API for accessing key/value storage """ result = None log.debug("Riak get key %r from %r", key, index) try: bucket = self._get_bucket(index) res = bucket.get(key) if res and res.data: result = res.data except Exception, e: log.warning("Riak exception: %r", e) result = None return result def set(self, key, value, index, doctype=None, **kw): """ Standard API for writing to key/value storage """ return False def select(self, query, index, doctype=None, fields=None, sortby=None, limit=None, **kw): """ Standard API for querying storage """ result = None try: pass except Exception, e: log.warning("Riak exception: %r", e) result = None return result def _get_keyfilter(self, index, starts_with=None, ends_with=None, sortby=None, limit=None): """ Helper function to execute a key filter query """ results = None fs = None fe = None if starts_with is not None: fs = RiakKeyFilter().starts_with(starts_with) if ends_with is not None: fe = RiakKeyFilter().ends_with(ends_with) if fs and fe: keyfilter = fs & fe else: keyfilter = fs or fe return self._mapreduce_keyfilter(index, keyfilter, sortby, limit) def _mapreduce_keyfilter(self, index, keyfilter, sortby=None, limit=None): """ Helper function to execute a map-reduce query using the given key filter """ results = None log.debug("Riak query %r with key filter %r", index, keyfilter) mapred = RiakMapReduce(self.client) mapred.add_bucket(self._get_bucket(index)) mapred.add_key_filters(keyfilter) # custom Riak.mapValuesJson() function that also adds the entry key to the data structure mapred.map(""" function(value, keyData, arg) { if (value.not_found) { return [value]; } var _data, data = value["values"][0]["data"]; if (Riak.getClassName(data) !== "Array") { _data = JSON.parse(data); _data["_key"] = value.key; return [_data]; } else { return data } } """) if sortby is not None: comp = '<' if limit is not None and limit < 0 else '>' mapred.reduce_sort('function(a,b){ return (a.%s || 0) %s (b.%s || 0) ? 1 : 0; }' % (sortby, comp, sortby)) if limit is not None: mapred.reduce_limit(abs(limit)) try: results = mapred.run() except Exception, e: log.warning("Riak MapReduce exception: %r", e) results = None return results def get_user(self, id=None, username=None): """ API for resolving usernames and reading user info """ # search by ID using a key filter if id is not None: results = self._get_keyfilter('users', starts_with=id + '::', limit=1) if results and len(results) > 0: return results[0] elif username is not None: user = self.get(username, 'users-current') if user is not None: return user # TODO: query 'users' bucket with an ends_with key filter # TODO: add a very short-term cache for lookups by ID return None def get_folder(self, mailbox=None, user=None): """ API for finding IMAP folders and their unique identifiers """ folder_id = self.get(mailbox, 'imap-folders-current') if folder_id is not None: return dict(uri=mailbox, id=folder_id) return None def get_events(self, objuid, mailbox, msguid, limit=None): """ API for querying event notifications """ # 1. get timeline entries for current folder folder = self.get_folder(mailbox) if folder is None: log.info("Folder %r not found in storage", mailbox) return None; object_event_keys = self._get_timeline_keys(objuid, folder['id']) # sanity check with msguid if msguid is not None: key_prefix = 'message::%s::%s' % (folder['id'], str(msguid)) if len([k for k in object_event_keys if k.startswith(key_prefix)]) == 0: log.warning("Sanity check failed: requested msguid %r not in timeline keys %r", msguid, object_event_keys) # TODO: abort? # 3. read each corresponding entry from imap-events filters = None for key in object_event_keys: f = RiakKeyFilter().starts_with(key) if filters is None: filters = f else: filters |= f log.debug("Querying imap-events for keys %r", object_event_keys) if filters is not None: # TODO: query directly using key? results = self._mapreduce_keyfilter('imap-events', filters, sortby='timestamp', limit=limit) return [self._transform_result(x, 'imap-events') for x in results if x.has_key('event') and not x['event'] == 'MessageExpunge'] \ if results is not None else results return None def _get_timeline_keys(self, objuid, folder_id, length=3): """ Helper method to fetch timeline keys recursively following moves accross folders """ object_event_keys = [] results = self._get_keyfilter('imap-message-timeline', starts_with='message::' + folder_id + '::', ends_with='::' + objuid) if not results or len(results) == 0: log.info("No timeline entry found for %r in folder %r", objuid, folder_id) return object_event_keys; for rec in results: key = '::'.join(rec['_key'].split('::', 4)[0:length]) object_event_keys.append(key) - # TODO: follow moves and add more :: tuples to our list - # by calling self._get_timeline_keys(objuid, folder['id'], length) recursively + + # follow moves and add more :: tuples to our list + if rec.has_key('history') and isinstance(rec['history'], dict) and rec['history'].has_key('imap'): + old_folder_id = rec['history']['imap'].get('previous_folder', None) + if old_folder_id: + object_event_keys += self._get_timeline_keys(objuid, old_folder_id, length) return object_event_keys def get_revision(self, objuid, mailbox, msguid, rev): """ API to get a certain revision of a stored object """ # resolve mailbox first folder = self.get_folder(mailbox) if folder is None: log.info("Folder %r not found in storage", mailbox) return None; # expand revision into the ISO timestamp format try: ts = datetime.datetime.strptime(str(rev), "%Y%m%d%H%M%S%f") timestamp = ts.strftime("%Y-%m-%dT%H:%M:%S.%f")[0:23] except Exception, e: log.warning("Invalid revision %r for object %r: %r", rev, objuid, e) return None # query message-timeline entries starting at peak with current folder (aka mailbox) object_event_keys = self._get_timeline_keys(objuid, folder['id'], length=4) # get the one key matching the revision timestamp keys = [k for k in object_event_keys if '::' + timestamp in k] log.debug("Get revision entry %r from candidates %r", timestamp, object_event_keys) if len(keys) == 1: result = self.get(keys[0], 'imap-events') if result is not None: return self._transform_result(result, 'imap-events') else: log.info("Revision timestamp %r doesn't match a single key from: %r", timestamp, object_event_keys) return None def get_message_data(self, rec): """ Getter for the full IMAP message payload for the given event record as previously fetched with get_events() or get_revision() """ - # compose the full message payload by contcatenating the message headers with the body part - if rec.has_key('body') and rec.has_key('headers'): - # TODO: encode header values? - return "\r\n".join([h + ": " + v for (h, v) in rec['headers'].iteritems()]) + "\r\n\r\n" + rec['body'] - - return None + return rec.get('message', None) def _transform_result(self, result, index): """ Turn an imap-event record into a dict to match the storage API """ result['_index'] = index # derrive (numeric) revision from timestamp if result.has_key('timestamp') and result.get('event','') in ['MessageAppend','MessageMove']: try: ts = parse_date(result['timestamp']) result['revision'] = ts.strftime("%Y%m%d%H%M%S%f")[0:17] except: pass # extract folder name from uri if result.has_key('uri') and not result.has_key('mailbox'): uri = self._parse_imap_uri(result['uri']) username = uri['user'] domain = uri['domain'] folder_name = uri['path'] folder_path = uri['path'] imap_delimiter = '/' if not username == None: if folder_name == "INBOX": folder_path = imap_delimiter.join(['user', '%s@%s' % (username, domain)]) else: folder_path = imap_delimiter.join(['user', username, '%s@%s' % (folder_name, domain)]) result['mailbox'] = folder_path return result def _parse_imap_uri(self, uri): """ Split the given URI string into its components """ split_uri = urlparse.urlsplit(uri) if len(split_uri.netloc.split('@')) == 3: (username, domain, server) = split_uri.netloc.split('@') elif len(split_uri.netloc.split('@')) == 2: (username, server) = split_uri.netloc.split('@') domain = None elif len(split_uri.netloc.split('@')) == 1: username = None domain = None server = split_uri.netloc result = dict(user=username, domain=domain, host=server) # First, .path == '/Calendar/Personal%20Calendar;UIDVALIDITY=$x[/;UID=$y] # Take everything after the first slash, and omit any INBOX/ stuff. path_str = '/'.join([x for x in split_uri.path.split('/') if not x == 'INBOX'][1:]) path_arr = path_str.split(';') result['path'] = urllib.unquote(path_arr[0]) # parse the path/query parameters into a dict param = dict() for p in path_arr[1:]: if '=' in p: (key,val) = p.split('=', 2) result[key] = urllib.unquote(val) return result diff --git a/tests/insert_testdata.sh b/tests/insert_testdata.sh index f8ac814..e63650f 100755 --- a/tests/insert_testdata.sh +++ b/tests/insert_testdata.sh @@ -1,246 +1,446 @@ #!/bin/sh RIAK_HOST='localhost' RIAK_PORT='10018' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-unique/buckets/users-current/keys/john.doe@example.org" \ -H 'Content-Type: application/json' \ -d '{ "dn": "uid=doe,ou=People,dc=example,dc=org", "cn": "Doe, John", "user": "john.doe@example.org", "id": "55475201-bdc211e4-881c96ef-f248ab46" }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/users/keys/55475201-bdc211e4-881c96ef-f248ab46::2015-03-07T14:10:16.941541::john.doe@example.org" \ -H 'Content-Type: application/json' \ -d '{ "dn": "uid=doe,ou=People,dc=example,dc=org", "cn": "Doe, John", "user": "john.doe@example.org", "id": "55475201-bdc211e4-881c96ef-f248ab46" }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-unique/buckets/imap-folders-current/keys/user%2Fjohn.doe%2FCalendar%40example.org" \ -H 'Content-Type: application/octet-stream' \ -d 'a5660caa-3165-4a84-bacd-ef4b58ef3663' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::3::2015-03-04T09:11:39.711Z" \ -H 'Content-Type: application/json' \ -d '{ "event": "MessageAppend", "vnd.cmu.envelope": "(...)", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", - "body": "--=_2bf2e936dd4806dab1e774f4bf4cb5b5\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_2bf2e936dd4806dab1e774f4bf4cb5b5\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1950\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.06EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA2712015-03-02T23:13:57Z2015-03-02T23:52:22Z2PUBLIC/kolab.org/Europe/Berlin2015-03-03T12:00:00/kolab.org/Europe/Berlin2015-03-03T14:00:00Todays Egara Testingkolab34.example.orgDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_2bf2e936dd4806dab1e774f4bf4cb5b5--", + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_2bf2e936dd4806dab1e774f4bf4cb5b5\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271\r\n\r\n--=_2bf2e936dd4806dab1e774f4bf4cb5b5\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_2bf2e936dd4806dab1e774f4bf4cb5b5\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1950\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.06EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA2712015-03-02T23:13:57Z2015-03-02T23:52:22Z2PUBLIC/kolab.org/Europe/Berlin2015-03-03T12:00:00/kolab.org/Europe/Berlin2015-03-03T14:00:00Todays Egara Testingkolab34.example.orgDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_2bf2e936dd4806dab1e774f4bf4cb5b5--", "bodyStructure": "(...)", "service": "imap", "modseq": 2, "timestamp": "2015-03-04T09:11:39.711Z", "pid": 28071, "vnd.cmu.sessionId": "kolab34.example.org-28071-1425478299-1-14630748386624877725", "messages": 1, "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960274/;UID=3", "uidset": "3", "messageSize": 2843, "uidnext": 4, "user": "john.doe@example.org", "vnd.cmu.unseenMessages": 1, "vnd.cmu.midset": [ "NIL" ], "headers": { "Content-Type": "multipart/mixed; boundary=\"=_2bf2e936dd4806dab1e774f4bf4cb5b5\"", "Subject": "6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271", "From": "john.doe@example.org", "To": "john.doe@example.org", "X-Kolab-Type": "application/x-vnd.kolab.event" } }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::3::2015-03-04T09:11:39.711Z::6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271" \ -H 'Content-Type: application/json' \ -d '{}' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::4::2015-03-04T21:56:46.465Z" \ -H 'Content-Type: application/json' \ -d '{ "event": "MessageAppend", "vnd.cmu.envelope": "(...)", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", - "body": "--=_fc7d881734b2f8b6cf99458899d79664\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_fc7d881734b2f8b6cf99458899d79664\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1950\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.06EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA2712015-03-02T23:13:57Z2015-03-05T02:56:46Z3PUBLIC/kolab.org/Europe/Berlin2015-03-05T13:00:00/kolab.org/Europe/Berlin2015-03-05T15:00:00Todays Egara Testingkolab34.example.orgDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_fc7d881734b2f8b6cf99458899d79664--", + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_fc7d881734b2f8b6cf99458899d79664\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271\r\n\r\n--=_fc7d881734b2f8b6cf99458899d79664\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_fc7d881734b2f8b6cf99458899d79664\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1950\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.06EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA2712015-03-02T23:13:57Z2015-03-05T02:56:46Z3PUBLIC/kolab.org/Europe/Berlin2015-03-05T13:00:00/kolab.org/Europe/Berlin2015-03-05T15:00:00Todays Egara Testingkolab34.example.orgDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_fc7d881734b2f8b6cf99458899d79664--", "bodyStructure": "(...)", "service": "imap", "modseq": 9, "timestamp": "2015-03-04T21:56:46.465Z", "pid": 981, "vnd.cmu.sessionId": "kolab34.example.org-981-1425524206-1-5416454962879660084", "messages": 2, "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330/;UID=4", "uidset": "4", "messageSize": 2910, "uidnext": 5, "user": "john.doe@example.org", "vnd.cmu.unseenMessages": 2, "vnd.cmu.midset": [ "NIL" ], "headers": { "Content-Type": "multipart/mixed; boundary=\"=_fc7d881734b2f8b6cf99458899d79664\"", "Subject": "6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271", "From": "john.doe@example.org", "To": "john.doe@example.org", "X-Kolab-Mime-Version": "3.0", "X-Kolab-Type": "application/x-vnd.kolab.event" } }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::4::2015-03-04T21:56:46.465Z::6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271" \ -H 'Content-Type: application/json' \ -d '{}' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::3::2015-03-04T21:56:46.500Z" \ -H 'Content-Type: application/json' \ -d '{ "event": "MessageTrash", "uidset": "3", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", "service": "imap", "modseq": 10, "timestamp": "2015-03-04T21:56:46.500Z", "pid": 981, "vnd.cmu.sessionId": "kolab34.example.org-981-1425524206-1-5416454962879660084", "messages": 2, "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330", "uidnext": 5, "user": "john.doe@example.org", "vnd.cmu.unseenMessages": 2, "vnd.cmu.midset": [ "NIL" ], "headers": { "Subject": "6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271", "From": "john.doe@example.org", "To": "john.doe@example.org", "X-Kolab-Mime-Version": "3.0", "X-Kolab-Type": "application/x-vnd.kolab.event" } }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::21::2015-03-17T15:58:43.016000Z" \ -H 'Content-Type: application/json' \ -d '{ - "body": "--=_c54c7baa744029b81ae04981de13c8b5\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_c54c7baa744029b81ae04981de13c8b5\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1960\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.05A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA2712015-03-17T15:58:42Z2015-03-17T15:58:42Z0PUBLIC/kolab.org/Europe/Berlin2015-03-19T11:00:00/kolab.org/Europe/Berlin2015-03-19T13:00:00Thursday TestThis is a test event for EgaraDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_c54c7baa744029b81ae04981de13c8b5--\r\n", + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_c54c7baa744029b81ae04981de13c8b5\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271\r\n\r\n--=_c54c7baa744029b81ae04981de13c8b5\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_c54c7baa744029b81ae04981de13c8b5\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1960\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.05A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA2712015-03-17T15:58:42Z2015-03-17T15:58:42Z0PUBLIC/kolab.org/Europe/Berlin2015-03-19T11:00:00/kolab.org/Europe/Berlin2015-03-19T13:00:00Thursday TestThis is a test event for EgaraDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_c54c7baa744029b81ae04981de13c8b5--\r\n", "bodyStructure": "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 206 4 NIL NIL NIL NIL)(\"APPLICATION\" \"CALENDAR+XML\" (\"CHARSET\" \"UTF-8\" \"NAME\" \"kolab.xml\") NIL NIL \"8BIT\" 1960 NIL (\"ATTACHMENT\" (\"FILENAME\" \"kolab.xml\" \"SIZE\" \"1960\")) NIL NIL) \"MIXED\" (\"BOUNDARY\" \"=_c54c7baa744029b81ae04981de13c8b5\") NIL NIL NIL)", "event": "MessageAppend", "flags": [ "\\Recent" ], "headers": { "Content-Type": "multipart/mixed; boundary=\"=_c54c7baa744029b81ae04981de13c8b5\"", "Date": "Tue, 17 Mar 2015 15:58:42 +0000", "From": "john.doe@example.org", "MIME-Version": "1.0", "Subject": "5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271", "To": "john.doe@example.org", "X-Kolab-Mime-Version": "3.0", "X-Kolab-Type": "application/x-vnd.kolab.event" }, "messageSize": 2920, "messages": 4, "modseq": 57, "pid": 1579, "service": "imap", "timestamp": "2015-03-17T15:58:43.016000Z", "uidnext": 22, "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330/;UID=21", "user": "john.doe@example.org", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", "vnd.cmu.envelope": "(\"Tue, 17 Mar 2015 15:58:42 +0000\" \"5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271\" ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) NIL NIL NIL NIL)", "vnd.cmu.midset": [ "NIL" ], "vnd.cmu.sessionId": "kolab34.example.org-1579-1426607922-1-10050682032034970758", "vnd.cmu.unseenMessages": 4 }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::21::2015-03-17T15:58:43.016000Z::5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271" \ -H 'Content-Type: application/json' \ -d '{}' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::22::2015-03-17T17:17:37.514000Z" \ -H 'Content-Type: application/json' \ -d '{ - "body": "--=_e8dedd96d48a3bdb20e76ee77e234363\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_e8dedd96d48a3bdb20e76ee77e234363\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=2123\r\n\r\n\r\n\r\n\r\n \r\n \r\n \r\n Roundcube-libkolab-1.1 Libkolabxml-1.2\r\n \r\n \r\n 2.0\r\n \r\n \r\n 3.1.0\r\n \r\n \r\n \r\n \r\n \r\n \r\n 5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271\r\n \r\n \r\n 2015-03-17T15:58:42Z\r\n \r\n \r\n 2015-03-17T17:17:37Z\r\n \r\n \r\n 1\r\n \r\n \r\n PUBLIC\r\n \r\n \r\n \r\n \r\n /kolab.org/Europe/Berlin\r\n \r\n \r\n 2015-03-19T10:00:00\r\n \r\n \r\n \r\n \r\n /kolab.org/Europe/Berlin\r\n \r\n \r\n 2015-03-19T11:00:00\r\n \r\n \r\n Thursday Test\r\n \r\n \r\n This is a test event for Egara\r\n \r\n \r\n CONFIRMED\r\n \r\n \r\n Somewhere else\r\n \r\n \r\n \r\n \r\n Doe, John\r\n \r\n \r\n mailto:%3Cjohn.doe%40example.org%3E\r\n \r\n \r\n \r\n \r\n \r\n\r\n\r\n\r\n--=_e8dedd96d48a3bdb20e76ee77e234363--\r\n", + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_e8dedd96d48a3bdb20e76ee77e234363\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271\r\n\r\n--=_e8dedd96d48a3bdb20e76ee77e234363\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_e8dedd96d48a3bdb20e76ee77e234363\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=2123\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.22.03.1.05A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA2712015-03-17T15:58:42Z2015-03-17T17:17:37Z1PUBLIC/kolab.org/Europe/Berlin2015-03-19T10:00:00/kolab.org/Europe/Berlin2015-03-19T11:00:00Thursday TestThis is a test event for EgaraCONFIRMEDSomewhere elseDoe, Johnmailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_e8dedd96d48a3bdb20e76ee77e234363--\r\n", "bodyStructure": "((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\") NIL NIL \"QUOTED-PRINTABLE\" 206 4 NIL NIL NIL NIL)(\"APPLICATION\" \"CALENDAR+XML\" (\"CHARSET\" \"UTF-8\" \"NAME\" \"kolab.xml\") NIL NIL \"8BIT\" 2123 NIL (\"ATTACHMENT\" (\"FILENAME\" \"kolab.xml\" \"SIZE\" \"2123\")) NIL NIL) \"MIXED\" (\"BOUNDARY\" \"=_e8dedd96d48a3bdb20e76ee77e234363\") NIL NIL NIL)", "event": "MessageAppend", "flags": [ "\\Recent" ], "headers": { "Content-Type": "multipart/mixed; boundary=\"=_e8dedd96d48a3bdb20e76ee77e234363\"", "Date": "Tue, 17 Mar 2015 17:17:37 +0000", "From": "john.doe@example.org", "MIME-Version": "1.0", "Subject": "5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271", "To": "john.doe@example.org", "X-Kolab-Mime-Version": "3.0", "X-Kolab-Type": "application/x-vnd.kolab.event" }, "messageSize": 3083, "messages": 5, "modseq": 58, "pid": 7493, "service": "imap", "timestamp": "2015-03-17T17:17:37.514000Z", "uidnext": 23, "uidset": "22", "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330/;UID=22", "user": "john.doe@example.org", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", "vnd.cmu.envelope": "(\"Tue, 17 Mar 2015 17:17:37 +0000\" \"5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271\" ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) NIL NIL NIL NIL)", "vnd.cmu.midset": [ "NIL" ], "vnd.cmu.sessionId": "kolab34.example.org-7493-1426612657-1-6878020795100368520", "vnd.cmu.unseenMessages": 5 }' curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::22::2015-03-17T17:17:37.514000Z::5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271" \ -H 'Content-Type: application/json' \ -d '{}' -curl -XPUT 'http://localhost:10018/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::21::2015-03-17T17:17:37.562000Z' \ +curl -XPUT "$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::21::2015-03-17T17:17:37.562000Z" \ -H 'Content-Type: application/json' \ -d '{ "event": "MessageExpunge", "messages": 4, "modseq": 60, "pid": 7493, "service": "imap", "timestamp": "2015-03-17T17:17:37.562000Z", "uidnext": 23, "uidset": "21", "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330", "user": "john.doe@example.org", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", "vnd.cmu.midset": [ "NIL" ], "vnd.cmu.sessionId": "kolab34.example.org-7493-1426612657-1-6878020795100368520", "vnd.cmu.unseenMessages": 4 }' -curl -XPUT 'http://localhost:10018/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::21::2015-03-17T17:17:37.554000Z' \ - -H 'Content-Type: application/json' \ - -d '{ +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::a5660caa-3165-4a84-bacd-ef4b58ef3663::21::2015-03-17T17:17:37.554000Z" \ + -H 'Content-Type: application/json' \ + -d '{ "event": "MessageTrash", "messages": 5, "modseq": 59, "pid": 7493, "service": "imap", "timestamp": "2015-03-17T17:17:37.554000Z", "uidnext": 23, "uidset": "21", "uri": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330", "user": "john.doe@example.org", "user_id": "55475201-bdc211e4-881c96ef-f248ab46", "vnd.cmu.midset": [ "NIL" ], "vnd.cmu.sessionId": "kolab34.example.org-7493-1426612657-1-6878020795100368520", "vnd.cmu.unseenMessages": 5 }' + +# move event to folder Testing + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-unique/buckets/imap-folders-current/keys/user%2Fjohn.doe%2FTesting%40example.org" \ + -H 'Content-Type: application/octet-stream' \ + -d 'fe0137f5-6828-474f-9cf6-fdf135123679' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::1::2015-03-18T04:13:31.158000Z" \ + -H 'Content-Type: application/json' \ + -d '{ + "event": "MessageMove", + "messages": 1, + "modseq": 3, + "oldMailboxID": "imap://john.doe@example.org@kolab34.example.org/Calendar;UIDVALIDITY=1424960330", + "pid": 12489, + "service": "imap", + "timestamp": "2015-03-18T04:13:31.158000Z", + "uidnext": 2, + "uidset": "1", + "uri": "imap://john.doe@example.org@kolab34.example.org/Testing;UIDVALIDITY=1426651436", + "user": "john.doe@example.org", + "user_id": "55475201-bdc211e4-881c96ef-f248ab46", + "vnd.cmu.midset": [ + "NIL" + ], + "headers": { + "Content-Type": "multipart/mixed; boundary=\"=_8e9630d5eb4cee74209f3d6f1736c0b5\"", + "Date": "Wed, 18 Mar 2015 14:13:07 +0100", + "From": "john.doe@example.org", + "MIME-Version": "1.0", + "Subject": "6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271", + "To": "john.doe@example.org", + "X-Kolab-Mime-Version": "3.0", + "X-Kolab-Type": "application/x-vnd.kolab.event" + }, + "vnd.cmu.oldUidset": "4", + "vnd.cmu.sessionId": "kolab34.example.org-12489-1426652010-1-1342732898036616806", + "vnd.cmu.unseenMessages": 1 +}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::1::2015-03-18T04:13:31.158000Z::6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271" \ + -H 'Content-Type: application/json' \ + -d '{ + "history": { + "imap": { + "previous_folder": "a5660caa-3165-4a84-bacd-ef4b58ef3663", + "previous_id": "4" + } + } +}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::2::2015-03-18T04:13:31.454000Z" \ + -H 'Content-Type: application/json' \ + -d '{ + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_8e9630d5eb4cee74209f3d6f1736c0b5\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271\r\n\r\n--=_8e9630d5eb4cee74209f3d6f1736c0b5\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_8e9630d5eb4cee74209f3d6f1736c0b5\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=1950\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.12.03.1.06EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA2712015-03-02T23:13:57Z2015-03-18T13:35:07Z13PUBLIC/kolab.org/Europe/Berlin2015-03-17T13:00:00/kolab.org/Europe/Berlin2015-03-17T15:00:00Todays Egara Testingkolab34.example.orgJohn Doemailto:%3Cjohn.doe%40example.org%3E\r\n\r\n\r\n\r\n--=_8e9630d5eb4cee74209f3d6f1736c0b5--\r\n", + "bodyStructure": "(...)", + "event": "MessageAppend", + "flags": [ + "\\Recent" + ], + "headers": { + "Content-Type": "multipart/mixed; boundary=\"=_8e9630d5eb4cee74209f3d6f1736c0b5\"", + "Date": "Wed, 18 Mar 2015 14:35:07 +0100", + "From": "john.doe@example.org", + "MIME-Version": "1.0", + "Subject": "6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271", + "To": "john.doe@example.org", + "X-Kolab-Mime-Version": "3.0", + "X-Kolab-Type": "application/x-vnd.kolab.event" + }, + "messageSize": 2910, + "messages": 2, + "modseq": 4, + "pid": 12489, + "service": "imap", + "timestamp": "2015-03-18T04:13:31.454000Z", + "uidnext": 3, + "uidset": "2", + "uri": "imap://john.doe@example.org@kolab34.example.org/Testing;UIDVALIDITY=1426651436/;UID=2", + "user": "john.doe@example.org", + "user_id": "55475201-bdc211e4-881c96ef-f248ab46", + "vnd.cmu.envelope": "(\"Wed, 18 Mar 2015 14:35:07 +0100\" \"6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271\" ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) NIL NIL NIL NIL)", + "vnd.cmu.midset": [ + "NIL" + ], + "vnd.cmu.sessionId": "kolab34.example.org-12489-1426652010-1-1342732898036616806", + "vnd.cmu.unseenMessages": 2 +}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::2::2015-03-18T04:13:31.454000Z::6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271" \ + -H 'Content-Type: application/json' \ + -d '{}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::1::2015-03-18T04:13:31.467000Z" \ + -H 'Content-Type: application/json' \ + -d '{ + "event": "MessageTrash", + "messages": 2, + "modseq": 5, + "pid": 12489, + "service": "imap", + "timestamp": "2015-03-18T04:13:31.467000Z", + "uidnext": 3, + "uidset": "1", + "uri": "imap://john.doe@example.org@kolab34.example.org/Testing;UIDVALIDITY=1426651436", + "user": "john.doe@example.org", + "user_id": "55475201-bdc211e4-881c96ef-f248ab46", + "vnd.cmu.midset": [ + "NIL" + ], + "vnd.cmu.sessionId": "kolab34.example.org-12489-1426652010-1-1342732898036616806", + "vnd.cmu.unseenMessages": 2 +}' + +# new event with attachment +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::3::2015-03-18T04:32:29.376000Z" \ + -H 'Content-Type: application/json' \ + -d '{ + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_03f3c3e51790c8ff68ca5414a293ae51\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271\r\n\r\n--=_03f3c3e51790c8ff68ca5414a293ae51\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_03f3c3e51790c8ff68ca5414a293ae51\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=2195\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.12.03.1.0390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA2712015-03-18T13:54:05Z2015-03-18T13:54:05Z0PUBLIC/kolab.org/Europe/Berlin2015-03-20T16:00:00/kolab.org/Europe/Berlin2015-03-20T17:00:00Attachments TestJohn Doemailto:%3Cjohn.doe%40example.org%3Etext/plainattachment.txtcid:attachment.1426686845.2073.txt\r\n\r\n\r\n\r\n--=_03f3c3e51790c8ff68ca5414a293ae51\r\nContent-ID: \r\nContent-Transfer-Encoding: base64\r\nContent-Type: text/plain;\r\n name=attachment.txt\r\nContent-Disposition: attachment;\r\n filename=attachment.txt;\r\n size=37\r\n\r\nVGhpcyBpcyBhIHRleHQgYXR0YWNobWVudCAodmVyc2lvbiAxKQ==\r\n--=_03f3c3e51790c8ff68ca5414a293ae51--\r\n", + "bodyStructure": "(...)", + "event": "MessageAppend", + "flags": [ + "\\Recent" + ], + "headers": { + "Content-Type": "multipart/mixed; boundary=\"=_03f3c3e51790c8ff68ca5414a293ae51\"", + "Date": "Wed, 18 Mar 2015 14:54:05 +0100", + "From": "john.doe@example.org", + "MIME-Version": "1.0", + "Subject": "390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271", + "To": "john.doe@example.org", + "X-Kolab-Mime-Version": "3.0", + "X-Kolab-Type": "application/x-vnd.kolab.event" + }, + "messageSize": 3450, + "messages": 2, + "modseq": 7, + "pid": 12618, + "service": "imap", + "timestamp": "2015-03-18T04:32:29.376000Z", + "uidset": 3, + "uidnext": 4, + "uri": "imap://john.doe@example.org@kolab34.example.org/Testing;UIDVALIDITY=1426651436/;UID=3", + "user": "john.doe@example.org", + "user_id": "55475201-bdc211e4-881c96ef-f248ab46", + "vnd.cmu.envelope": "(\"Wed, 18 Mar 2015 14:54:05 +0100\" \"390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271\" ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) NIL NIL NIL NIL)", + "vnd.cmu.midset": [ + "NIL" + ], + "vnd.cmu.sessionId": "kolab34.example.org-12618-1426653149-1-11265804361548763525", + "vnd.cmu.unseenMessages": 2 +}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::3::2015-03-18T04:32:29.376000Z::390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271" \ + -H 'Content-Type: application/json' \ + -d '{}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-events/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::4::2015-03-18T04:36:34.162000Z" \ + -H 'Content-Type: application/json' \ + -d '{ + "message": "MIME-Version: 1.0\r\nContent-Type: multipart/mixed;\r\n boundary=\"=_531f398ebbeecbf523661f67827bea1e\"\r\nFrom: john.doe@example.org\r\nTo: john.doe@example.org\r\nX-Kolab-Type: application/x-vnd.kolab.event\r\nX-Kolab-Mime-Version: 3.0\r\nSubject: 390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271\r\n\r\n--=_531f398ebbeecbf523661f67827bea1e\r\nContent-Transfer-Encoding: quoted-printable\r\nContent-Type: text/plain; charset=ISO-8859-1\r\n\r\nThis is a Kolab Groupware object. To view this object you will need an emai=\r\nl client that understands the Kolab Groupware format. For a list of such em=\r\nail clients please visit http://www.kolab.org/\r\n\r\n\r\n--=_531f398ebbeecbf523661f67827bea1e\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: application/calendar+xml; charset=UTF-8;\r\n name=kolab.xml\r\nContent-Disposition: attachment;\r\n filename=kolab.xml;\r\n size=2195\r\n\r\n\r\n\r\nRoundcube-libkolab-1.1 Libkolabxml-1.12.03.1.0390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA2712015-03-18T13:54:05Z2015-03-18T13:58:09Z0PUBLIC/kolab.org/Europe/Berlin2015-03-20T16:00:00/kolab.org/Europe/Berlin2015-03-20T17:00:00Attachments TestJohn Doemailto:%3Cjohn.doe%40example.org%3Etext/plainattachment.txtcid:attachment.1426687089.9628.txt\r\n\r\n\r\n\r\n--=_531f398ebbeecbf523661f67827bea1e\r\nContent-ID: \r\nContent-Transfer-Encoding: base64\r\nContent-Type: text/plain;\r\n name=attachment.txt\r\nContent-Disposition: attachment;\r\n filename=attachment.txt;\r\n size=56\r\n\r\nVGhpcyBpcyBhIHRleHQgYXR0YWNobWVudCAodmVyc2lvbiAyKQp3aXRoIGEgc2Vjb25kIGxpbmU=\r\n--=_531f398ebbeecbf523661f67827bea1e--\r\n", + "bodyStructure": "(...)", + "event": "MessageAppend", + "flags": [ + "\\Recent" + ], + "headers": { + "Content-Type": "multipart/mixed; boundary=\"=_531f398ebbeecbf523661f67827bea1e\"", + "Date": "Wed, 18 Mar 2015 14:58:10 +0100", + "From": "john.doe@example.org", + "MIME-Version": "1.0", + "Subject": "390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271", + "To": "john.doe@example.org", + "X-Kolab-Mime-Version": "3.0", + "X-Kolab-Type": "application/x-vnd.kolab.event" + }, + "messageSize": 3474, + "messages": 3, + "modseq": 8, + "pid": 12671, + "service": "imap", + "timestamp": "2015-03-18T04:36:34.162000Z", + "uidset": 4, + "uidnext": 5, + "uri": "imap://john.doe@example.org@kolab34.example.org/Testing;UIDVALIDITY=1426651436/;UID=4", + "user": "john.doe@example.org", + "user_id": "55475201-bdc211e4-881c96ef-f248ab46", + "vnd.cmu.envelope": "(\"Wed, 18 Mar 2015 14:58:10 +0100\" \"390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271\" ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) ((NIL NIL \"john.doe\" \"example.org\")) NIL NIL NIL NIL)", + "vnd.cmu.midset": [ + "NIL" + ], + "vnd.cmu.sessionId": "kolab34.example.org-12671-1426653393-1-5448614388380127124", + "vnd.cmu.unseenMessages": 3 +}' + +curl -XPUT "http://$RIAK_HOST:$RIAK_PORT/types/egara-lww/buckets/imap-message-timeline/keys/message::fe0137f5-6828-474f-9cf6-fdf135123679::4::2015-03-18T04:36:34.162000Z::390582E807A257686D51A6BF87F342E9-A4BF5BBB9FEAA271" \ + -H 'Content-Type: application/json' \ + -d '{}' + + diff --git a/tests/test_api.py b/tests/test_api.py index b91517c..8f44f4b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,200 +1,200 @@ import httplib, json, base64, hmac, hashlib, email from twisted.trial import unittest class TestAPI(unittest.TestCase): reqid = 0 # test config config = { 'api_host': '127.0.0.1', 'api_port': 8080, 'api_user': 'webclient', 'api_pass': 'Welcome2KolabSystems', 'api_secret': '8431f19170e7f90d4107bf4b169baf' } @classmethod def setUp(self): # TODO: reset riak buckets and fill with sample data # TODO: fire up the web service pass def _api_request(self, user, method, sign=True, **kwargs): """ Helper method to send JSON-RPC calls to the API """ headers = { 'Authorization': 'Basic ' + base64.b64encode(self.config['api_user'] + ':' + self.config['api_pass']), 'Content-type': "application/json", 'X-Request-User': user } self.rpcerror = None self.reqid += 1 body = { 'jsonrpc': '2.0', 'id': self.reqid, 'method': method, 'params': kwargs } sbody = json.dumps(body) if sign: headers['X-Request-Sign'] = hmac.new( key=self.config['api_secret'], msg=headers.get('X-Request-User') + ':' + sbody, digestmod=hashlib.sha256 ).hexdigest() try: conn = httplib.HTTPConnection(self.config['api_host'], self.config['api_port']) conn.request('POST', '/api/rpc', sbody, headers) result = None response = conn.getresponse() if response.status == 200: result = json.loads(response.read()) if result.has_key('error'): self.rpcerror = result['error'] if result.has_key('result') and result.has_key('id') and result['id'] == self.reqid: return result['result'] # print "JSON-RPC response: %d %s; %r" % (response.status, response.reason, result) except Exception, e: print "JSON-RPC error: %r" % (e) return False def assertRPCError(self, code=None): self.assertIsInstance(self.rpcerror, dict) if code is not None: self.assertEqual(self.rpcerror['code'], code) def test_001_json_rpc(self): res = self._api_request('', 'system.keygen', sign=False) self.assertFalse(res) self.assertRPCError(-32600) res = self._api_request('', 'system.keygen', sign=True) self.assertIsInstance(res, dict) self.assertTrue(res.has_key('key')) self.assertTrue(res.has_key('expires')) def test_002_invalid_request(self): res = self._api_request('john.doe@example.org', 'event.bogus', uid='6EE0570E8CA21DDB67FC9ADE5EE38E7F-XXXXXXXXXXXXXXX' ) self.assertFalse(res) self.assertRPCError(-32601) # Method not found res = self._api_request('john.doe@example.org', 'event.created', uid='6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271' ) self.assertFalse(res) self.assertRPCError(-32602) # Invalid params res = self._api_request('john.doe@example.org', 'event.created', uid='6EE0570E8CA21DDB67FC9ADE5EE38E7F-INVALID-UID', mailbox='Calendar' ) self.assertFalse(res) def test_003_created_lastmodified(self): created = self._api_request('john.doe@example.org', 'event.created', uid='6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271', - mailbox='Calendar', - msguid=4 + mailbox='Testing', + msguid=2 ) self.assertIsInstance(created, dict) self.assertTrue(created.has_key('date')) self.assertTrue(created.has_key('user')) self.assertTrue(created.has_key('rev')) self.assertIn('john.doe@example.org', created['user']) changed = self._api_request('john.doe@example.org', 'event.lastmodified', uid='6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271', - mailbox='Calendar' + mailbox='Testing' ) self.assertIsInstance(changed, dict) self.assertTrue(changed.has_key('rev')) self.assertTrue(changed['rev'] > created['rev']) self.assertTrue(changed['date'] > created['date']) def test_004_changelog(self): changelog = self._api_request('john.doe@example.org', 'event.changelog', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar', msguid=22 ) # print json.dumps(changelog, indent=4) self.assertIsInstance(changelog, dict) self.assertTrue(changelog.has_key('changes')) self.assertTrue(changelog.has_key('uid')) self.assertEqual(len(changelog['changes']), 2) one = changelog['changes'][0] two = changelog['changes'][1] self.assertTrue(two['rev'] > one['rev']) def test_005_get_revision(self): changelog = self._api_request('john.doe@example.org', 'event.changelog', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar' ) self.assertIsInstance(changelog, dict) self.assertEqual(len(changelog.get('changes', [])), 2) rev1 = self._api_request('john.doe@example.org', 'event.get', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar', rev=changelog['changes'][0]['rev'] ) self.assertIsInstance(rev1, dict) self.assertTrue(rev1.has_key('xml')) self.assertNotIn('', rev1['xml']) rev2 = self._api_request('john.doe@example.org', 'event.get', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar', rev=changelog['changes'][1]['rev'] ) self.assertIsInstance(rev2, dict) self.assertTrue(rev2.has_key('xml')) self.assertIn('Somewhere else', rev2['xml']) msgdata = self._api_request('john.doe@example.org', 'event.rawdata', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar', rev=changelog['changes'][0]['rev'] ) self.assertIsInstance(msgdata, unicode) message = email.message_from_string(msgdata.encode('utf8','replace')) self.assertIsInstance(message, email.message.Message) self.assertTrue(message.is_multipart()) def test_006_diff(self): changelog = self._api_request('john.doe@example.org', 'event.changelog', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar' ) self.assertIsInstance(changelog, dict) self.assertEqual(len(changelog.get('changes', [])), 2) rev1 = changelog['changes'][0]['rev'] rev2 = changelog['changes'][1]['rev'] diff = self._api_request('john.doe@example.org', 'event.diff', uid='5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271', mailbox='Calendar', rev1=rev1, rev2=rev2 ) # print json.dumps(diff, indent=4) self.assertIsInstance(diff, dict) self.assertTrue(diff.has_key('changes')) self.assertTrue(diff.has_key('uid')) self.assertEqual(len(diff['changes']), 6) \ No newline at end of file diff --git a/tests/test_storage.py b/tests/test_storage.py index 800d7a2..38fb22f 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -1,83 +1,91 @@ import os, flask, json, email from app import storage from twisted.trial import unittest class FlaskCurrentApp(object): config = dict( STORAGE=dict( backend='riak', riak_host='127.0.0.1', riak_port='10018' ), CONFIG_DIR=os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'config') ) def __init__(self): import logging.config logging.config.fileConfig(self.config['CONFIG_DIR'] + '/bonnie-flask.conf') class TestStorage(unittest.TestCase): def setUp(self): # patch current_app to return static config self.patch(flask, 'current_app', FlaskCurrentApp()) def test_000_instance(self): strg = storage.factory() self.assertIsInstance(strg, storage.AbstractStorage) self.assertIsInstance(strg, storage.riak_storage.RiakStorage) def test_001_get_user_by_name(self): strg = storage.factory() user = strg.get_user(username='john.doe@example.org') self.assertIsInstance(user, dict) self.assertEqual(user['id'], '55475201-bdc211e4-881c96ef-f248ab46') self.assertEqual(user['user'], 'john.doe@example.org') def test_002_get_user_by_id(self): strg = storage.factory() user = strg.get_user(id='55475201-bdc211e4-881c96ef-f248ab46') self.assertIsInstance(user, dict) self.assertEqual(user['user'], 'john.doe@example.org') self.assertEqual(user['id'], '55475201-bdc211e4-881c96ef-f248ab46') def test_010_get_folder_id(self): strg = storage.factory() folder = strg.get_folder('user/john.doe/Calendar@example.org') self.assertIsInstance(folder, dict) self.assertEqual(folder['id'], 'a5660caa-3165-4a84-bacd-ef4b58ef3663') def test_020_get_events(self): strg = storage.factory() - mailbox = 'user/john.doe/Calendar@example.org' - events = strg.get_events('6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271', mailbox, 4) - self.assertEqual(len(events), 3) + oldmailbox = 'user/john.doe/Calendar@example.org' + mailbox = 'user/john.doe/Testing@example.org' + events = strg.get_events('6EE0570E8CA21DDB67FC9ADE5EE38E7F-A4BF5BBB9FEAA271', mailbox, 2) + self.assertEqual(len(events), 6) self.assertEqual(events[0]['event'], 'MessageAppend') self.assertEqual(events[0]['uidset'], '3') - self.assertEqual(events[0]['mailbox'], mailbox) + self.assertEqual(events[0]['mailbox'], oldmailbox) self.assertEqual(events[1]['event'], 'MessageAppend') self.assertEqual(events[1]['uidset'], '4') - self.assertEqual(events[1]['mailbox'], mailbox) + self.assertEqual(events[1]['mailbox'], oldmailbox) self.assertEqual(events[2]['event'], 'MessageTrash') self.assertEqual(events[2]['uidset'], '3') - #print json.dumps(events, indent=4) + self.assertEqual(events[3]['event'], 'MessageMove') + self.assertEqual(events[3]['uidset'], '1') + self.assertEqual(events[4]['event'], 'MessageAppend') + self.assertEqual(events[4]['uidset'], '2') + self.assertEqual(events[4]['mailbox'], mailbox) + self.assertEqual(events[5]['event'], 'MessageTrash') + self.assertEqual(events[5]['uidset'], '1') + self.assertEqual(events[5]['mailbox'], mailbox) def test_025_get_revision(self): strg = storage.factory() uid = '5A637BE7895D785671E1732356E65CC8-A4BF5BBB9FEAA271' mailbox = 'user/john.doe/Calendar@example.org' events = strg.get_events(uid, mailbox, None, limit=1) self.assertEqual(len(events), 1) rec = strg.get_revision(uid, mailbox, None, events[0]['revision']) self.assertIsInstance(rec, dict) self.assertEqual(rec['event'], 'MessageAppend') msgsource = strg.get_message_data(rec) self.assertIsInstance(msgsource, unicode) message = email.message_from_string(msgsource.encode('utf8','replace')) self.assertIsInstance(message, email.message.Message) self.assertTrue(message.is_multipart())