summaryrefslogtreecommitdiff
path: root/mail/src
diff options
context:
space:
mode:
Diffstat (limited to 'mail/src')
-rw-r--r--mail/src/leap/mail/adaptors/soledad.py85
-rw-r--r--mail/src/leap/mail/imap/account.py82
-rw-r--r--mail/src/leap/mail/imap/mailbox.py43
-rw-r--r--mail/src/leap/mail/imap/messages.py28
-rw-r--r--mail/src/leap/mail/imap/server.py10
-rw-r--r--mail/src/leap/mail/imap/service/imap.py20
-rw-r--r--mail/src/leap/mail/imap/tests/test_imap.py61
-rw-r--r--mail/src/leap/mail/mail.py70
-rw-r--r--mail/src/leap/mail/mailbox_indexer.py128
-rw-r--r--mail/src/leap/mail/walk.py4
10 files changed, 255 insertions, 276 deletions
diff --git a/mail/src/leap/mail/adaptors/soledad.py b/mail/src/leap/mail/adaptors/soledad.py
index 46dbe4c..9f0bb30 100644
--- a/mail/src/leap/mail/adaptors/soledad.py
+++ b/mail/src/leap/mail/adaptors/soledad.py
@@ -19,7 +19,6 @@ Soledadad MailAdaptor module.
import re
from collections import defaultdict
from email import message_from_string
-from functools import partial
from pycryptopp.hash import sha256
from twisted.internet import defer
@@ -72,6 +71,7 @@ class SoledadDocumentWrapper(models.DocumentWrapper):
deletion.
"""
# TODO we could also use a _dirty flag (in models)
+ # TODO add a get_count() method ??? -- that is extended over u1db.
# We keep a dictionary with DeferredLocks, that will be
# unique to every subclass of SoledadDocumentWrapper.
@@ -86,10 +86,9 @@ class SoledadDocumentWrapper(models.DocumentWrapper):
"""
return cls._k_locks[cls.__name__]
- def __init__(self, **kwargs):
- doc_id = kwargs.pop('doc_id', None)
+ def __init__(self, doc_id=None, future_doc_id=None, **kwargs):
self._doc_id = doc_id
- self._future_doc_id = kwargs.pop('future_doc_id', None)
+ self._future_doc_id = future_doc_id
self._lock = defer.DeferredLock()
super(SoledadDocumentWrapper, self).__init__(**kwargs)
@@ -123,7 +122,7 @@ class SoledadDocumentWrapper(models.DocumentWrapper):
def update_doc_id(doc):
self._doc_id = doc.doc_id
- self._future_doc_id = None
+ self.set_future_doc_id(None)
return doc
if self.future_doc_id is None:
@@ -201,6 +200,7 @@ class SoledadDocumentWrapper(models.DocumentWrapper):
@classmethod
def _get_or_create(cls, store, index, value):
+ # TODO shorten this method.
assert store is not None
assert index is not None
assert value is not None
@@ -211,6 +211,7 @@ class SoledadDocumentWrapper(models.DocumentWrapper):
except AttributeError:
raise RuntimeError("The model is badly defined")
+ # TODO separate into another method?
def try_to_get_doc_from_index(indexes):
values = []
idx_def = dict(indexes)[index]
@@ -323,9 +324,6 @@ class SoledadDocumentWrapper(models.DocumentWrapper):
d.addCallback(wrap_docs)
return d
- # TODO
- # [ ] get_count() ???
-
def __repr__(self):
try:
idx = getattr(self, self.model.__meta__.index)
@@ -442,29 +440,23 @@ class MessageWrapper(object):
integers, beginning at one, and the values are dictionaries with the
content of the content-docs.
"""
- if isinstance(mdoc, SoledadDocument):
- mdoc_id = mdoc.doc_id
- mdoc = mdoc.content
- else:
- mdoc_id = None
- if not mdoc:
- mdoc = {}
- self.mdoc = MetaMsgDocWrapper(doc_id=mdoc_id, **mdoc)
-
- if isinstance(fdoc, SoledadDocument):
- fdoc_id = fdoc.doc_id
- fdoc = fdoc.content
- else:
- fdoc_id = None
- self.fdoc = FlagsDocWrapper(doc_id=fdoc_id, **fdoc)
+
+ def get_doc_wrapper(doc, cls):
+ if isinstance(doc, SoledadDocument):
+ doc_id = doc.doc_id
+ doc = doc.content
+ else:
+ doc_id = None
+ if not doc:
+ doc = {}
+ return cls(doc_id=doc_id, **doc)
+
+ self.mdoc = get_doc_wrapper(mdoc, MetaMsgDocWrapper)
+
+ self.fdoc = get_doc_wrapper(fdoc, FlagsDocWrapper)
self.fdoc.set_future_doc_id(self.mdoc.fdoc)
- if isinstance(hdoc, SoledadDocument):
- hdoc_id = hdoc.doc_id
- hdoc = hdoc.content
- else:
- hdoc_id = None
- self.hdoc = HeaderDocWrapper(doc_id=hdoc_id, **hdoc)
+ self.hdoc = get_doc_wrapper(hdoc, HeaderDocWrapper)
self.hdoc.set_future_doc_id(self.mdoc.hdoc)
if cdocs is None:
@@ -489,10 +481,6 @@ class MessageWrapper(object):
"Cannot create: fdoc has a doc_id")
# TODO check that the doc_ids in the mdoc are coherent
- # TODO I think we need to tolerate the no hdoc.doc_id case, for when we
- # are doing a copy to another mailbox.
- # leap_assert(self.hdoc.doc_id is None,
- # "Cannot create: hdoc has a doc_id")
d = []
d.append(self.mdoc.create(store))
d.append(self.fdoc.create(store))
@@ -566,8 +554,9 @@ class MessageWrapper(object):
def get_subpart_dict(self, index):
"""
- :param index: index, 1-indexed
+ :param index: the part to lookup, 1-indexed
:type index: int
+ :rtype: dict
"""
return self.hdoc.part_map[str(index)]
@@ -785,16 +774,6 @@ class SoledadMailAdaptor(SoledadIndexMixin):
assert(MessageClass is not None)
return MessageClass(MessageWrapper(mdoc, fdoc, hdoc, cdocs), uid=uid)
- def _get_msg_from_variable_doc_list(self, doc_list, msg_class, uid=None):
- if len(doc_list) == 3:
- mdoc, fdoc, hdoc = doc_list
- cdocs = None
- elif len(doc_list) > 3:
- fdoc, hdoc = doc_list[:3]
- cdocs = dict(enumerate(doc_list[3:], 1))
- return self.get_msg_from_docs(
- msg_class, mdoc, fdoc, hdoc, cdocs, uid=uid)
-
def get_msg_from_mdoc_id(self, MessageClass, store, mdoc_id,
uid=None, get_cdocs=False):
@@ -844,10 +823,21 @@ class SoledadMailAdaptor(SoledadIndexMixin):
else:
d = get_parts_doc_from_mdoc_id()
- d.addCallback(partial(self._get_msg_from_variable_doc_list,
- msg_class=MessageClass, uid=uid))
+ d.addCallback(self._get_msg_from_variable_doc_list,
+ msg_class=MessageClass, uid=uid)
return d
+ def _get_msg_from_variable_doc_list(self, doc_list, msg_class, uid=None):
+ if len(doc_list) == 3:
+ mdoc, fdoc, hdoc = doc_list
+ cdocs = None
+ elif len(doc_list) > 3:
+ # XXX is this case used?
+ mdoc, fdoc, hdoc = doc_list[:3]
+ cdocs = dict(enumerate(doc_list[3:], 1))
+ return self.get_msg_from_docs(
+ msg_class, mdoc, fdoc, hdoc, cdocs, uid=uid)
+
def get_flags_from_mdoc_id(self, store, mdoc_id):
"""
# XXX stuff here...
@@ -875,7 +865,6 @@ class SoledadMailAdaptor(SoledadIndexMixin):
def create_msg(self, store, msg):
"""
:param store: an instance of soledad, or anything that behaves alike
- :type store:
:param msg: a Message object.
:return: a Deferred that is fired when all the underlying documents
@@ -889,8 +878,6 @@ class SoledadMailAdaptor(SoledadIndexMixin):
"""
:param msg: a Message object.
:param store: an instance of soledad, or anything that behaves alike
- :type store:
- :param msg: a Message object.
:return: a Deferred that is fired when all the underlying documents
have been updated (actually, it's only the fdoc that's allowed
to update).
diff --git a/mail/src/leap/mail/imap/account.py b/mail/src/leap/mail/imap/account.py
index 0cf583b..38df845 100644
--- a/mail/src/leap/mail/imap/account.py
+++ b/mail/src/leap/mail/imap/account.py
@@ -49,9 +49,6 @@ if PROFILE_CMD:
# Soledad IMAP Account
#######################################
-# XXX watchout, account needs to be ready... so we should maybe return
-# a deferred to the IMAP service when it's initialized
-
class IMAPAccount(object):
"""
An implementation of an imap4 Account
@@ -67,8 +64,14 @@ class IMAPAccount(object):
"""
Keeps track of the mailboxes and subscriptions handled by this account.
- :param account: The name of the account (user id).
- :type account: str
+ The account is not ready to be used, since the store needs to be
+ initialized and we also need to do some initialization routines.
+ You can either pass a deferred to this constructor, or use
+ `callWhenReady` method.
+
+ :param user_id: The name of the account (user id, in the form
+ user@provider).
+ :type user_id: str
:param store: a Soledad instance.
:type store: Soledad
@@ -87,11 +90,6 @@ class IMAPAccount(object):
self.user_id = user_id
self.account = Account(store, ready_cb=lambda: d.callback(self))
- def _return_mailbox_from_collection(self, collection, readwrite=1):
- if collection is None:
- return None
- mbox = IMAPMailbox(collection, rw=readwrite)
- return mbox
def end_session(self):
"""
Used to mark when the session has closed, and we should not allow any
@@ -103,6 +101,12 @@ class IMAPAccount(object):
self.session_ended = True
def callWhenReady(self, cb, *args, **kw):
+ """
+ Execute callback when the account is ready to be used.
+ XXX note that this callback will be called with a first ignored
+ parameter.
+ """
+ # TODO ignore the first parameter and change tests accordingly.
d = self.account.callWhenReady(cb, *args, **kw)
return d
@@ -129,6 +133,12 @@ class IMAPAccount(object):
d.addCallback(self._return_mailbox_from_collection)
return d
+ def _return_mailbox_from_collection(self, collection, readwrite=1):
+ if collection is None:
+ return None
+ mbox = IMAPMailbox(collection, rw=readwrite)
+ return mbox
+
#
# IAccount
#
@@ -146,7 +156,7 @@ class IMAPAccount(object):
:type creation_ts: int
:returns: a Deferred that will contain the document if successful.
- :rtype: bool
+ :rtype: defer.Deferred
"""
name = normalize_mailbox(name)
@@ -190,25 +200,15 @@ class IMAPAccount(object):
:return:
A deferred that will fire with a true value if the creation
- succeeds.
+ succeeds. The deferred might fail with a MailboxException
+ if the mailbox cannot be added.
:rtype: Deferred
- :raise MailboxException: Raised if this mailbox cannot be added.
"""
- paths = filter(None, normalize_mailbox(pathspec).split('/'))
- subs = []
- sep = '/'
-
def pass_on_collision(failure):
failure.trap(imap4.MailboxCollision)
return True
- for accum in range(1, len(paths)):
- partial_path = sep.join(paths[:accum])
- d = self.addMailbox(partial_path)
- d.addErrback(pass_on_collision)
- subs.append(d)
-
def handle_collision(failure):
failure.trap(imap4.MailboxCollision)
if not pathspec.endswith('/'):
@@ -216,19 +216,26 @@ class IMAPAccount(object):
else:
return defer.succeed(True)
+ def all_good(result):
+ return all(result)
+
+ paths = filter(None, normalize_mailbox(pathspec).split('/'))
+ subs = []
+ sep = '/'
+
+ for accum in range(1, len(paths)):
+ partial_path = sep.join(paths[:accum])
+ d = self.addMailbox(partial_path)
+ d.addErrback(pass_on_collision)
+ subs.append(d)
+
df = self.addMailbox(sep.join(paths))
df.addErrback(handle_collision)
subs.append(df)
- def all_good(result):
- return all(result)
-
- if subs:
- d1 = defer.gatherResults(subs)
- d1.addCallback(all_good)
- return d1
- else:
- return defer.succeed(False)
+ d1 = defer.gatherResults(subs)
+ d1.addCallback(all_good)
+ return d1
def select(self, name, readwrite=1):
"""
@@ -285,8 +292,7 @@ class IMAPAccount(object):
global _mboxes
_mboxes = mailboxes
if name not in mailboxes:
- err = imap4.MailboxException("No such mailbox: %r" % name)
- return defer.fail(err)
+ raise imap4.MailboxException("No such mailbox: %r" % name)
def get_mailbox(_):
return self.getMailbox(name)
@@ -303,10 +309,9 @@ class IMAPAccount(object):
# as part of their root.
for others in _mboxes:
if others != name and others.startswith(name):
- err = imap4.MailboxException(
+ raise imap4.MailboxException(
"Hierarchically inferior mailboxes "
"exist and \\Noselect is set")
- return defer.fail(err)
return mbox
d = self.account.list_all_mailbox_names()
@@ -324,8 +329,6 @@ class IMAPAccount(object):
# if self._inferiorNames(name) > 1:
# self._index.removeMailbox(name)
- # TODO use mail.Account.rename_mailbox
- # TODO finish conversion to deferreds
def rename(self, oldname, newname):
"""
Renames a mailbox.
@@ -460,6 +463,9 @@ class IMAPAccount(object):
:type name: str
:rtype: Deferred
"""
+ # TODO should raise MailboxException if attempted to unsubscribe
+ # from a mailbox that is not currently subscribed.
+ # TODO factor out with subscribe method.
name = normalize_mailbox(name)
def set_unsubscribed(mbox):
diff --git a/mail/src/leap/mail/imap/mailbox.py b/mail/src/leap/mail/imap/mailbox.py
index 58fc514..52f4dd5 100644
--- a/mail/src/leap/mail/imap/mailbox.py
+++ b/mail/src/leap/mail/imap/mailbox.py
@@ -91,8 +91,9 @@ class IMAPMailbox(object):
implements(
imap4.IMailbox,
imap4.IMailboxInfo,
- #imap4.ICloseableMailbox,
imap4.ISearchableMailbox,
+ # XXX I think we do not need to implement CloseableMailbox, do we?
+ # imap4.ICloseableMailbox
imap4.IMessageCopier)
init_flags = INIT_FLAGS
@@ -108,8 +109,6 @@ class IMAPMailbox(object):
def __init__(self, collection, rw=1):
"""
- SoledadMailbox constructor.
-
:param collection: instance of IMAPMessageCollection
:type collection: IMAPMessageCollection
@@ -117,14 +116,10 @@ class IMAPMailbox(object):
:type rw: int
"""
self.rw = rw
- self.closed = False
self._uidvalidity = None
self.collection = collection
- if not self.getFlags():
- self.setFlags(self.init_flags)
-
@property
def mbox_name(self):
return self.collection.mbox_name
@@ -201,6 +196,7 @@ class IMAPMailbox(object):
"flags expected to be a tuple")
return self.collection.set_mbox_attr("flags", flags)
+ # TODO - not used?
@property
def is_closed(self):
"""
@@ -211,6 +207,7 @@ class IMAPMailbox(object):
"""
return self.collection.get_mbox_attr("closed")
+ # TODO - not used?
def set_closed(self, closed):
"""
Set the closed attribute for this mailbox.
@@ -448,9 +445,6 @@ class IMAPMailbox(object):
d.addCallback(remove_mbox)
return d
- def _close_cb(self, result):
- self.closed = True
-
def expunge(self):
"""
Remove all messages flagged \\Deleted
@@ -555,24 +549,23 @@ class IMAPMailbox(object):
# for sequence numbers (uid = 0)
if is_sequence:
- logger.debug("Getting msg by index: INEFFICIENT call!")
# TODO --- implement sequences in mailbox indexer
raise NotImplementedError
else:
- d = self._get_sequence_of_messages(messages_asked)
+ d = self._get_messages_range(messages_asked)
d.addCallback(get_imap_messages_for_sequence)
# TODO -- call signal_to_ui
# d.addCallback(self.cb_signal_unread_to_ui)
return d
- def _get_sequence_of_messages(self, messages_asked):
- def get_sequence(messages_asked):
+ def _get_messages_range(self, messages_asked):
+ def get_range(messages_asked):
return self._filter_msg_seq(messages_asked)
d = defer.maybeDeferred(self._bound_seq, messages_asked)
- d.addCallback(get_sequence)
+ d.addCallback(get_range)
return d
def fetch_flags(self, messages_asked, uid):
@@ -599,6 +592,10 @@ class IMAPMailbox(object):
MessagePart.
:rtype: tuple
"""
+ is_sequence = True if uid == 0 else False
+ if is_sequence:
+ raise NotImplementedError
+
d = defer.Deferred()
reactor.callLater(0, self._do_fetch_flags, messages_asked, uid, d)
if PROFILE_CMD:
@@ -649,7 +646,7 @@ class IMAPMailbox(object):
generator = (item for item in result)
d.callback(generator)
- d_seq = self._get_sequence_of_messages(messages_asked)
+ d_seq = self._get_messages_range(messages_asked)
d_seq.addCallback(get_flags_for_seq)
return d_seq
@@ -677,7 +674,11 @@ class IMAPMailbox(object):
MessagePart.
:rtype: tuple
"""
+ # TODO implement sequences
# TODO how often is thunderbird doing this?
+ is_sequence = True if uid == 0 else False
+ if is_sequence:
+ raise NotImplementedError
class headersPart(object):
def __init__(self, uid, headers):
@@ -753,6 +754,12 @@ class IMAPMailbox(object):
:raise ReadOnlyMailbox: Raised if this mailbox is not open for
read-write.
"""
+ # TODO implement sequences
+ # TODO how often is thunderbird doing this?
+ is_sequence = True if uid == 0 else False
+ if is_sequence:
+ raise NotImplementedError
+
if not self.isWriteable():
log.msg('read only mailbox!')
raise imap4.ReadOnlyMailbox
@@ -777,7 +784,7 @@ class IMAPMailbox(object):
done.
:type observer: deferred
"""
- # XXX implement also sequence (uid = 0)
+ # TODO implement also sequence (uid = 0)
# TODO we should prevent client from setting Recent flag
leap_assert(not isinstance(flags, basestring),
"flags cannot be a string")
@@ -800,7 +807,7 @@ class IMAPMailbox(object):
got_flags_setted.addCallback(return_result_dict)
return got_flags_setted
- d_seq = self._get_sequence_of_messages(messages_asked)
+ d_seq = self._get_messages_range(messages_asked)
d_seq.addCallback(set_flags_for_seq)
return d_seq
diff --git a/mail/src/leap/mail/imap/messages.py b/mail/src/leap/mail/imap/messages.py
index df50323..8f4c953 100644
--- a/mail/src/leap/mail/imap/messages.py
+++ b/mail/src/leap/mail/imap/messages.py
@@ -36,16 +36,38 @@ logger = logging.getLogger(__name__)
class IMAPMessage(object):
"""
- The main representation of a message.
+ The main representation of a message as seen by the IMAP Server.
+ This class implements the semantics specific to IMAP specification.
"""
-
implements(imap4.IMessage)
def __init__(self, message, prefetch_body=True,
store=None, d=defer.Deferred()):
"""
- Initializes a LeapMessage.
+ Get an IMAPMessage. A mail.Message is needed, since many of the methods
+ are proxied to that object.
+
+
+ If you do not need to prefetch the body of the message, you can set
+ `prefetch_body` to False, but the current imap server implementation
+ expect the getBodyFile method to return inmediately.
+
+ When the prefetch_body option is used, a deferred is also expected as a
+ parameter, and this will fire when the deferred initialization has
+ taken place, with this instance of IMAPMessage as a parameter.
+
+ :param message: the abstract message
+ :type message: mail.Message
+ :param prefetch_body: Whether to prefetch the content doc for the body.
+ :type prefetch_body: bool
+ :param store: an instance of soledad, or anything that behaves like it.
+ :param d: an optional deferred, that will be fired with the instance of
+ the IMAPMessage being initialized
+ :type d: defer.Deferred
"""
+ # TODO substitute the use of the deferred initialization by a factory
+ # function, maybe.
+
self.message = message
self.__body_fd = None
self.store = store
diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py
index 23ddefc..027fd7a 100644
--- a/mail/src/leap/mail/imap/server.py
+++ b/mail/src/leap/mail/imap/server.py
@@ -500,13 +500,18 @@ class LEAPIMAPServer(imap4.IMAP4Server):
select_DELETE = auth_DELETE
# Need to override the command table after patching
- # arg_astring and arg_literal
+ # arg_astring and arg_literal, except on the methods that we are already
+ # overriding.
+ # TODO --------------------------------------------
+ # Check if we really need to override these
+ # methods, or we can monkeypatch.
# do_DELETE = imap4.IMAP4Server.do_DELETE
# do_CREATE = imap4.IMAP4Server.do_CREATE
# do_RENAME = imap4.IMAP4Server.do_RENAME
# do_SUBSCRIBE = imap4.IMAP4Server.do_SUBSCRIBE
# do_UNSUBSCRIBE = imap4.IMAP4Server.do_UNSUBSCRIBE
+ # -------------------------------------------------
do_LOGIN = imap4.IMAP4Server.do_LOGIN
do_STATUS = imap4.IMAP4Server.do_STATUS
do_APPEND = imap4.IMAP4Server.do_APPEND
@@ -530,8 +535,11 @@ class LEAPIMAPServer(imap4.IMAP4Server):
auth_EXAMINE = (_selectWork, arg_astring, 0, 'EXAMINE')
select_EXAMINE = auth_EXAMINE
+ # TODO -----------------------------------------------
+ # re-add if we stop overriding DELETE
# auth_DELETE = (do_DELETE, arg_astring)
# select_DELETE = auth_DELETE
+ # ----------------------------------------------------
auth_RENAME = (do_RENAME, arg_astring, arg_astring)
select_RENAME = auth_RENAME
diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py
index 93e4d62..cc76e3a 100644
--- a/mail/src/leap/mail/imap/service/imap.py
+++ b/mail/src/leap/mail/imap/service/imap.py
@@ -17,6 +17,7 @@
"""
IMAP service initialization
"""
+# TODO: leave only an implementor of IService in here
import logging
import os
@@ -29,10 +30,9 @@ from twisted.python import log
logger = logging.getLogger(__name__)
from leap.common import events as leap_events
-from leap.common.check import leap_assert, leap_assert_type, leap_check
+from leap.common.check import leap_assert_type, leap_check
from leap.mail.imap.account import IMAPAccount
from leap.mail.imap.server import LEAPIMAPServer
-from leap.mail.incoming import IncomingMail
from leap.soledad.client import Soledad
from leap.common.events.events_pb2 import IMAP_SERVICE_STARTED
@@ -113,6 +113,10 @@ class LeapIMAPFactory(ServerFactory):
"""
Stops imap service (fetcher, factory and port).
"""
+ # mark account as unusable, so any imap command will fail
+ # with unauth state.
+ self.theAccount.end_session()
+
# TODO should wait for all the pending deferreds,
# the twisted way!
if DO_PROFILE:
@@ -123,23 +127,23 @@ class LeapIMAPFactory(ServerFactory):
return ServerFactory.doStop(self)
-def run_service(*args, **kwargs):
+def run_service(store, **kwargs):
"""
Main entry point to run the service from the client.
+ :param store: a soledad instance
+
:returns: the port as returned by the reactor when starts listening, and
the factory for the protocol.
"""
- leap_assert(len(args) == 2)
- soledad = args
- leap_assert_type(soledad, Soledad)
+ leap_assert_type(store, Soledad)
port = kwargs.get('port', IMAP_PORT)
userid = kwargs.get('userid', None)
leap_check(userid is not None, "need an user id")
- uuid = soledad.uuid
- factory = LeapIMAPFactory(uuid, userid, soledad)
+ uuid = store.uuid
+ factory = LeapIMAPFactory(uuid, userid, store)
try:
tport = reactor.listenTCP(port, factory,
diff --git a/mail/src/leap/mail/imap/tests/test_imap.py b/mail/src/leap/mail/imap/tests/test_imap.py
index 6be41cd..67a24cd 100644
--- a/mail/src/leap/mail/imap/tests/test_imap.py
+++ b/mail/src/leap/mail/imap/tests/test_imap.py
@@ -75,6 +75,7 @@ class TestRealm:
# TestCases
#
+# TODO rename to IMAPMessageCollection
class MessageCollectionTestCase(IMAP4HelperMixin):
"""
Tests for the MessageCollection class
@@ -87,6 +88,7 @@ class MessageCollectionTestCase(IMAP4HelperMixin):
We override mixin method since we are only testing
MessageCollection interface in this particular TestCase
"""
+ # FIXME -- return deferred
super(MessageCollectionTestCase, self).setUp()
# FIXME --- update initialization
@@ -1090,64 +1092,6 @@ class LEAPIMAP4ServerTestCase(IMAP4HelperMixin):
# Okay, that was much fun indeed
- # skipping close test: we just need expunge for now.
- #def testClose(self):
- #"""
- #Test closing the mailbox. We expect to get deleted all messages flagged
- #as such.
- #"""
- #acc = self.server.theAccount
- #mailbox_name = 'mailbox-close'
-#
- #def add_mailbox():
- #return acc.addMailbox(mailbox_name)
-#
- #def login():
- #return self.client.login(TEST_USER, TEST_PASSWD)
-#
- #def select():
- #return self.client.select(mailbox_name)
-#
- #def get_mailbox():
- #def _save_mbox(mailbox):
- #self.mailbox = mailbox
- #d = self.server.theAccount.getMailbox(mailbox_name)
- #d.addCallback(_save_mbox)
- #return d
-#
- #def add_messages():
- #d1 = self.mailbox.addMessage(
- #'test 1', flags=('\\Deleted', 'AnotherFlag'))
- #d2 = self.mailbox.addMessage(
- #'test 2', flags=('AnotherFlag',))
- #d3 = self.mailbox.addMessage(
- #'test 3', flags=('\\Deleted',))
- #d = defer.gatherResults([d1, d2, d3])
- #return d
-#
- #def close():
- #return self.client.close()
-#
- #d = self.connected.addCallback(strip(add_mailbox))
- #d.addCallback(strip(login))
- #d.addCallbacks(strip(select), self._ebGeneral)
- #d.addCallback(strip(get_mailbox))
- #d.addCallbacks(strip(add_messages), self._ebGeneral)
- #d.addCallbacks(strip(close), self._ebGeneral)
- #d.addCallbacks(self._cbStopClient, self._ebGeneral)
- #d2 = self.loopback()
- #d1 = defer.gatherResults([d, d2])
- #d1.addCallback(lambda _: self.mailbox.getMessageCount())
- #d1.addCallback(self._cbTestClose)
- #return d1
-#
- #def _cbTestClose(self, count):
- # TODO is this correct? count should not take into account those
- # flagged as deleted???
- #self.assertEqual(count, 1)
- # TODO --- assert flags are those of the message #2
- #self.failUnless(self.mailbox.closed)
-
def testExpunge(self):
"""
Test expunge command
@@ -1209,6 +1153,7 @@ class LEAPIMAP4ServerTestCase(IMAP4HelperMixin):
self.assertItemsEqual(self.results, [1, 3])
+# TODO -------- Fix this testcase
class AccountTestCase(IMAP4HelperMixin):
"""
Test the Account.
diff --git a/mail/src/leap/mail/mail.py b/mail/src/leap/mail/mail.py
index 9b7a562..59fd57c 100644
--- a/mail/src/leap/mail/mail.py
+++ b/mail/src/leap/mail/mail.py
@@ -58,9 +58,16 @@ def _write_and_rewind(payload):
class MessagePart(object):
- # TODO pass cdocs in init
-
def __init__(self, part_map, cdocs={}):
+ """
+ :param part_map: a dictionary mapping the subparts for
+ this MessagePart (1-indexed).
+ :type part_map: dict
+ :param cdoc: optional, a dict of content documents
+ """
+ # TODO document the expected keys in the part_map dict.
+ # TODO add abstraction layer between the cdocs and this class. Only
+ # adaptor should know about the format of the cdocs.
self._pmap = part_map
self._cdocs = cdocs
@@ -266,6 +273,10 @@ class MessageCollection(object):
# [ ] To guarantee synchronicity of the documents sent together during a
# sync, we could get hold of a deferredLock that inhibits
# synchronization while we are updating (think more about this!)
+ # [ ] review the serveral count_ methods. I think it's better to patch
+ # server to accept deferreds.
+ # [ ] Use inheritance for the mailbox-collection instead of handling the
+ # special cases everywhere?
# Account should provide an adaptor instance when creating this collection.
adaptor = None
@@ -285,9 +296,6 @@ class MessageCollection(object):
self.mbox_indexer = mbox_indexer
self.mbox_wrapper = mbox_wrapper
- # TODO --- review this count shit. I think it's better to patch server
- # to accept deferreds.
-
def is_mailbox_collection(self):
"""
Return True if this collection represents a Mailbox.
@@ -297,22 +305,26 @@ class MessageCollection(object):
@property
def mbox_name(self):
- wrapper = getattr(self, "mbox_wrapper", None)
- if not wrapper:
+ # TODO raise instead?
+ if self.mbox_wrapper is None:
return None
- return wrapper.mbox
+ return self.mbox_wrapper.mbox
@property
def mbox_uuid(self):
- wrapper = getattr(self, "mbox_wrapper", None)
- if not wrapper:
+ # TODO raise instead?
+ if self.mbox_wrapper is None:
return None
- return wrapper.uuid
+ return self.mbox_wrapper.uuid
def get_mbox_attr(self, attr):
+ if self.mbox_wrapper is None:
+ raise RuntimeError("This is not a mailbox collection")
return getattr(self.mbox_wrapper, attr)
def set_mbox_attr(self, attr, value):
+ if self.mbox_wrapper is None:
+ raise RuntimeError("This is not a mailbox collection")
setattr(self.mbox_wrapper, attr, value)
return self.mbox_wrapper.update(self.store)
@@ -323,10 +335,10 @@ class MessageCollection(object):
Retrieve a message by its content hash.
:rtype: Deferred
"""
-
if not self.is_mailbox_collection():
- # instead of getting the metamsg by chash, query by (meta) index
- # or use the internal collection of pointers-to-docs.
+ # TODO instead of getting the metamsg by chash, in this case we
+ # should query by (meta) index or use the internal collection of
+ # pointers-to-docs.
raise NotImplementedError()
metamsg_id = _get_mdoc_id(self.mbox_name, chash)
@@ -462,6 +474,9 @@ class MessageCollection(object):
raise NotImplementedError()
def insert_copied_mdoc_id(wrapper):
+ # TODO this needs to be implemented before the copy
+ # interface works.
+ newmailbox_uuid = get_mbox_uuid_from_msg_wrapper(wrapper)
return self.mbox_indexer.insert_doc(
newmailbox_uuid, wrapper.mdoc.doc_id)
@@ -525,15 +540,6 @@ class MessageCollection(object):
d.addCallback(del_all_uid)
return d
- def _update_flags_or_tags(self, old, new, mode):
- if mode == Flagsmode.APPEND:
- final = list((set(tuple(old) + new)))
- elif mode == Flagsmode.REMOVE:
- final = list(set(old).difference(set(new)))
- elif mode == Flagsmode.SET:
- final = new
- return final
-
def update_flags(self, msg, flags, mode):
"""
Update flags for a given message.
@@ -563,6 +569,15 @@ class MessageCollection(object):
d.addCallback(newtags)
return d
+ def _update_flags_or_tags(self, old, new, mode):
+ if mode == Flagsmode.APPEND:
+ final = list((set(tuple(old) + new)))
+ elif mode == Flagsmode.REMOVE:
+ final = list(set(old).difference(set(new)))
+ elif mode == Flagsmode.SET:
+ final = new
+ return final
+
class Account(object):
"""
@@ -573,7 +588,7 @@ class Account(object):
basic collection handled by traditional MUAs, but it can also handle other
types of Collections (tag based, for instance).
- leap.mail.imap.SoledadBackedAccount partially proxies methods in this
+ leap.mail.imap.IMAPAccount partially proxies methods in this
class.
"""
@@ -582,7 +597,6 @@ class Account(object):
# the Account class.
adaptor_class = SoledadMailAdaptor
- store = None
def __init__(self, store, ready_cb=None):
self.store = store
@@ -612,6 +626,12 @@ class Account(object):
return d
def callWhenReady(self, cb, *args, **kw):
+ """
+ Execute the callback when the initialization of the Account is ready.
+ Note that the callback will receive a first meaningless parameter.
+ """
+ # TODO this should ignore the first parameter explicitely
+ # lambda _: cb(*args, **kw)
self.deferred_initialization.addCallback(cb, *args, **kw)
return self.deferred_initialization
diff --git a/mail/src/leap/mail/mailbox_indexer.py b/mail/src/leap/mail/mailbox_indexer.py
index 43a1f60..4eb0fa8 100644
--- a/mail/src/leap/mail/mailbox_indexer.py
+++ b/mail/src/leap/mail/mailbox_indexer.py
@@ -38,23 +38,23 @@ class WrongMetaDocIDError(Exception):
pass
-def sanitize(mailbox_id):
- return mailbox_id.replace("-", "_")
+def sanitize(mailbox_uuid):
+ return mailbox_uuid.replace("-", "_")
-def check_good_uuid(mailbox_id):
+def check_good_uuid(mailbox_uuid):
"""
Check that the passed mailbox identifier is a valid UUID.
- :param mailbox_id: the uuid to check
- :type mailbox_id: str
+ :param mailbox_uuid: the uuid to check
+ :type mailbox_uuid: str
:return: None
:raises: AssertionError if a wrong uuid was passed.
"""
try:
- uuid.UUID(str(mailbox_id))
+ uuid.UUID(str(mailbox_uuid))
except (AttributeError, ValueError):
raise AssertionError(
- "the mbox_id is not a valid uuid: %s" % mailbox_id)
+ "the mbox_id is not a valid uuid: %s" % mailbox_uuid)
class MailboxIndexer(object):
@@ -88,51 +88,33 @@ class MailboxIndexer(object):
assert self.store is not None
return self.store.raw_sqlcipher_query(*args, **kw)
- def create_table(self, mailbox_id):
+ def create_table(self, mailbox_uuid):
"""
Create the UID table for a given mailbox.
:param mailbox: the mailbox identifier.
:type mailbox: str
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
sql = ("CREATE TABLE if not exists {preffix}{name}( "
"uid INTEGER PRIMARY KEY AUTOINCREMENT, "
"hash TEXT UNIQUE NOT NULL)".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
return self._query(sql)
- def delete_table(self, mailbox_id):
+ def delete_table(self, mailbox_uuid):
"""
Delete the UID table for a given mailbox.
:param mailbox: the mailbox name
:type mailbox: str
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
sql = ("DROP TABLE if exists {preffix}{name}".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
return self._query(sql)
- def rename_table(self, oldmailbox, newmailbox):
- """
- Delete the UID table for a given mailbox.
- :param oldmailbox: the old mailbox name
- :type oldmailbox: str
- :param newmailbox: the new mailbox name
- :type newmailbox: str
- :rtype: Deferred
- """
- assert oldmailbox
- assert newmailbox
- assert oldmailbox != newmailbox
- sql = ("ALTER TABLE {preffix}{old} "
- "RENAME TO {preffix}{new}".format(
- preffix=self.table_preffix,
- old=sanitize(oldmailbox), new=sanitize(newmailbox)))
- return self._query(sql)
-
- def insert_doc(self, mailbox_id, doc_id):
+ def insert_doc(self, mailbox_uuid, doc_id):
"""
Insert the doc_id for a MetaMsg in the UID table for a given mailbox.
@@ -148,11 +130,11 @@ class MailboxIndexer(object):
document.
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
assert doc_id
- mailbox_id = mailbox_id.replace('-', '_')
+ mailbox_uuid = mailbox_uuid.replace('-', '_')
- if not re.findall(METAMSGID_RE.format(mbox_uuid=mailbox_id), doc_id):
+ if not re.findall(METAMSGID_RE.format(mbox_uuid=mailbox_uuid), doc_id):
raise WrongMetaDocIDError("Wrong format for the MetaMsg doc_id")
def get_rowid(result):
@@ -160,12 +142,12 @@ class MailboxIndexer(object):
sql = ("INSERT INTO {preffix}{name} VALUES ("
"NULL, ?)".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (doc_id,)
sql_last = ("SELECT MAX(rowid) FROM {preffix}{name} "
"LIMIT 1;").format(
- preffix=self.table_preffix, name=sanitize(mailbox_id))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid))
d = self._query(sql, values)
d.addCallback(lambda _: self._query(sql_last))
@@ -173,25 +155,25 @@ class MailboxIndexer(object):
d.addErrback(lambda f: f.printTraceback())
return d
- def delete_doc_by_uid(self, mailbox_id, uid):
+ def delete_doc_by_uid(self, mailbox_uuid, uid):
"""
Delete the entry for a MetaMsg in the UID table for a given mailbox.
- :param mailbox_id: the mailbox uuid
+ :param mailbox_uuid: the mailbox uuid
:type mailbox: str
:param uid: the UID of the message.
:type uid: int
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
assert uid
sql = ("DELETE FROM {preffix}{name} "
"WHERE uid=?".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (uid,)
return self._query(sql, values)
- def delete_doc_by_hash(self, mailbox_id, doc_id):
+ def delete_doc_by_hash(self, mailbox_uuid, doc_id):
"""
Delete the entry for a MetaMsg in the UID table for a given mailbox.
@@ -199,7 +181,7 @@ class MailboxIndexer(object):
M-<mailbox_uuid>-<content-hash-of-the-message>
- :param mailbox_id: the mailbox uuid
+ :param mailbox_uuid: the mailbox uuid
:type mailbox: str
:param doc_id: the doc_id for the MetaMsg
:type doc_id: str
@@ -207,82 +189,80 @@ class MailboxIndexer(object):
document.
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
assert doc_id
sql = ("DELETE FROM {preffix}{name} "
"WHERE hash=?".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (doc_id,)
return self._query(sql, values)
- def get_doc_id_from_uid(self, mailbox_id, uid):
+ def get_doc_id_from_uid(self, mailbox_uuid, uid):
"""
Get the doc_id for a MetaMsg in the UID table for a given mailbox.
- :param mailbox_id: the mailbox uuid
+ :param mailbox_uuid: the mailbox uuid
:type mailbox: str
:param uid: the uid for the MetaMsg for this mailbox
:type uid: int
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
- mailbox_id = mailbox_id.replace('-', '_')
+ check_good_uuid(mailbox_uuid)
+ mailbox_uuid = mailbox_uuid.replace('-', '_')
def get_hash(result):
return _maybe_first_query_item(result)
sql = ("SELECT hash from {preffix}{name} "
"WHERE uid=?".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (uid,)
d = self._query(sql, values)
d.addCallback(get_hash)
return d
- def get_uid_from_doc_id(self, mailbox_id, doc_id):
- check_good_uuid(mailbox_id)
- mailbox_id = mailbox_id.replace('-', '_')
+ def get_uid_from_doc_id(self, mailbox_uuid, doc_id):
+ check_good_uuid(mailbox_uuid)
+ mailbox_uuid = mailbox_uuid.replace('-', '_')
def get_uid(result):
return _maybe_first_query_item(result)
sql = ("SELECT uid from {preffix}{name} "
"WHERE hash=?".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (doc_id,)
d = self._query(sql, values)
d.addCallback(get_uid)
return d
-
-
- def get_doc_ids_from_uids(self, mailbox_id, uids):
+ def get_doc_ids_from_uids(self, mailbox_uuid, uids):
# For IMAP relative numbering /sequences.
# XXX dereference the range (n,*)
raise NotImplementedError()
- def count(self, mailbox_id):
+ def count(self, mailbox_uuid):
"""
Get the number of entries in the UID table for a given mailbox.
- :param mailbox_id: the mailbox uuid
- :type mailbox_id: str
+ :param mailbox_uuid: the mailbox uuid
+ :type mailbox_uuid: str
:return: a deferred that will fire with an integer returning the count.
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
def get_count(result):
return _maybe_first_query_item(result)
sql = ("SELECT Count(*) FROM {preffix}{name};".format(
- preffix=self.table_preffix, name=sanitize(mailbox_id)))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
d = self._query(sql)
d.addCallback(get_count)
d.addErrback(lambda _: 0)
return d
- def get_next_uid(self, mailbox_id):
+ def get_next_uid(self, mailbox_uuid):
"""
Get the next integer beyond the highest UID count for a given mailbox.
@@ -291,13 +271,13 @@ class MailboxIndexer(object):
only thing that can be assured is that it will be equal or greater than
the value returned.
- :param mailbox_id: the mailbox uuid
+ :param mailbox_uuid: the mailbox uuid
:type mailbox: str
:return: a deferred that will fire with an integer returning the next
uid.
:rtype: Deferred
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
def increment(result):
uid = _maybe_first_query_item(result)
@@ -305,18 +285,18 @@ class MailboxIndexer(object):
return 1
return uid + 1
- d = self.get_last_uid(mailbox_id)
+ d = self.get_last_uid(mailbox_uuid)
d.addCallback(increment)
return d
- def get_last_uid(self, mailbox_id):
+ def get_last_uid(self, mailbox_uuid):
"""
Get the highest UID for a given mailbox.
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
sql = ("SELECT MAX(rowid) FROM {preffix}{name} "
"LIMIT 1;").format(
- preffix=self.table_preffix, name=sanitize(mailbox_id))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid))
def getit(result):
return _maybe_first_query_item(result)
@@ -325,17 +305,17 @@ class MailboxIndexer(object):
d.addCallback(getit)
return d
- def all_uid_iter(self, mailbox_id):
+ def all_uid_iter(self, mailbox_uuid):
"""
Get a sequence of all the uids in this mailbox.
- :param mailbox_id: the mailbox uuid
- :type mailbox_id: str
+ :param mailbox_uuid: the mailbox uuid
+ :type mailbox_uuid: str
"""
- check_good_uuid(mailbox_id)
+ check_good_uuid(mailbox_uuid)
sql = ("SELECT uid from {preffix}{name} ").format(
- preffix=self.table_preffix, name=sanitize(mailbox_id))
+ preffix=self.table_preffix, name=sanitize(mailbox_uuid))
def get_results(result):
return [x[0] for x in result]
diff --git a/mail/src/leap/mail/walk.py b/mail/src/leap/mail/walk.py
index 8653a5f..891abdc 100644
--- a/mail/src/leap/mail/walk.py
+++ b/mail/src/leap/mail/walk.py
@@ -122,7 +122,7 @@ def walk_msg_tree(parts, body_phash=None):
documents that will be stored in Soledad.
It walks down the subparts in the parsed message tree, and collapses
- the leaf docuents into a wrapper document until no multipart submessages
+ the leaf documents into a wrapper document until no multipart submessages
are left. To achieve this, it iteratively calculates a wrapper vector of
all documents in the sequence that have more than one part and have unitary
documents to their right. To collapse a multipart, take as many
@@ -171,7 +171,7 @@ def walk_msg_tree(parts, body_phash=None):
HEADERS: dict(parts[wind][HEADERS])
}
- # remove subparts and substitue wrapper
+ # remove subparts and substitute wrapper
map(lambda i: parts.remove(i), slic)
parts[wind] = cwra