summaryrefslogtreecommitdiff
path: root/src/leap/mail/imap/account.py
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-01-01 18:21:44 -0400
committerKali Kaneko <kali@leap.se>2015-02-11 14:05:43 -0400
commitfa60b76ce9cdf6684945c6bc724f10818104166b (patch)
tree8c11ca5d65aaa54c3a3ab2fd5b1db59110a0621f /src/leap/mail/imap/account.py
parent4fa3d27225f44d9598d24d9e59a97baaa9bbb90e (diff)
cleanup imap implementation
Diffstat (limited to 'src/leap/mail/imap/account.py')
-rw-r--r--src/leap/mail/imap/account.py306
1 files changed, 113 insertions, 193 deletions
diff --git a/src/leap/mail/imap/account.py b/src/leap/mail/imap/account.py
index 7dfbbd1..0baf078 100644
--- a/src/leap/mail/imap/account.py
+++ b/src/leap/mail/imap/account.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# account.py
-# Copyright (C) 2013 LEAP
+# Copyright (C) 2013-2015 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -15,12 +15,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
-Soledad Backed Account.
+Soledad Backed IMAP Account.
"""
-import copy
import logging
import os
import time
+from functools import partial
from twisted.internet import defer
from twisted.mail import imap4
@@ -29,9 +29,9 @@ from zope.interface import implements
from leap.common.check import leap_assert, leap_assert_type
+from leap.mail.constants import MessageFlags
from leap.mail.mail import Account
-from leap.mail.imap.fields import WithMsgFields
-from leap.mail.imap.mailbox import SoledadMailbox, normalize_mailbox
+from leap.mail.imap.mailbox import IMAPMailbox, normalize_mailbox
from leap.soledad.client import Soledad
logger = logging.getLogger(__name__)
@@ -49,9 +49,10 @@ if PROFILE_CMD:
# Soledad IMAP Account
#######################################
-# TODO remove MsgFields too
+# XXX watchout, account needs to be ready... so we should maybe return
+# a deferred to the IMAP service when it's initialized
-class IMAPAccount(WithMsgFields):
+class IMAPAccount(object):
"""
An implementation of an imap4 Account
that is backed by Soledad Encrypted Documents.
@@ -72,37 +73,20 @@ class IMAPAccount(WithMsgFields):
:param store: a Soledad instance.
:type store: Soledad
"""
- # XXX assert a generic store interface instead, so that we
- # can plug the memory store wrapper seamlessly.
leap_assert(store, "Need a store instance to initialize")
leap_assert_type(store, Soledad)
- # XXX SHOULD assert too that the name matches the user/uuid with which
+ # TODO assert too that the name matches the user/uuid with which
# soledad has been initialized.
self.user_id = user_id
self.account = Account(store)
- # XXX should hide this in the adaptor...
- def _get_mailbox_by_name(self, name):
- """
- Return an mbox document by name.
-
- :param name: the name of the mailbox
- :type name: str
-
- :rtype: SoledadDocument
- """
- def get_first_if_any(docs):
- return docs[0] if docs else None
-
- d = self._store.get_from_index(
- self.TYPE_MBOX_IDX, self.MBOX_KEY,
- normalize_mailbox(name))
- d.addCallback(get_first_if_any)
- return d
+ def _return_mailbox_from_collection(self, collection, readwrite=1):
+ if collection is None:
+ return None
+ return IMAPMailbox(collection, rw=readwrite)
- # XXX move to Account?
- # XXX needed?
+ # XXX Where's this used from? -- self.delete...
def getMailbox(self, name):
"""
Return a Mailbox with that name, without selecting it.
@@ -110,31 +94,25 @@ class IMAPAccount(WithMsgFields):
:param name: name of the mailbox
:type name: str
- :returns: a a SoledadMailbox instance
- :rtype: SoledadMailbox
+ :returns: an IMAPMailbox instance
+ :rtype: IMAPMailbox
"""
name = normalize_mailbox(name)
- if name not in self.account.mailboxes:
- raise imap4.MailboxException("No such mailbox: %r" % name)
+ def check_it_exists(mailboxes):
+ if name not in mailboxes:
+ raise imap4.MailboxException("No such mailbox: %r" % name)
- # XXX Does mailbox really need reference to soledad?
- return SoledadMailbox(name, self._store)
+ d = self.account.list_all_mailbox_names()
+ d.addCallback(check_it_exists)
+ d.addCallback(lambda _: self.account.get_collection_by_mailbox, name)
+ d.addCallbacK(self._return_mailbox_from_collection)
+ return d
#
# IAccount
#
- def _get_empty_mailbox(self):
- """
- Returns an empty mailbox.
-
- :rtype: dict
- """
- # XXX move to mailbox module
- return copy.deepcopy(mailbox.EMPTY_MBOX)
-
- # TODO use mail.Account.add_mailbox
def addMailbox(self, name, creation_ts=None):
"""
Add a mailbox to the account.
@@ -154,8 +132,9 @@ class IMAPAccount(WithMsgFields):
leap_assert(name, "Need a mailbox name to create a mailbox")
- if name in self.mailboxes:
- raise imap4.MailboxCollision(repr(name))
+ def check_it_does_not_exist(mailboxes):
+ if name in mailboxes:
+ raise imap4.MailboxCollision(repr(name))
if creation_ts is None:
# by default, we pass an int value
@@ -164,21 +143,18 @@ class IMAPAccount(WithMsgFields):
# mailbox-uidvalidity.
creation_ts = int(time.time() * 10E2)
- mbox = self._get_empty_mailbox()
- mbox[self.MBOX_KEY] = name
- mbox[self.CREATED_KEY] = creation_ts
-
- def load_mbox_cache(result):
- d = self._load_mailboxes()
- d.addCallback(lambda _: result)
+ def set_mbox_creation_ts(collection):
+ d = collection.set_mbox_attr("created")
+ d.addCallback(lambda _: collection)
return d
- d = self._store.create_doc(mbox)
- d.addCallback(load_mbox_cache)
+ d = self.account.list_all_mailbox_names()
+ d.addCallback(check_it_does_not_exist)
+ d.addCallback(lambda _: self.account.get_collection_by_mailbox, name)
+ d.addCallback(set_mbox_creation_ts)
+ d.addCallback(self._return_mailbox_from_collection)
return d
- # TODO use mail.Account.create_mailbox?
- # Watch out, imap specific exceptions raised here.
def create(self, pathspec):
"""
Create a new mailbox from the given hierarchical name.
@@ -204,9 +180,10 @@ class IMAPAccount(WithMsgFields):
for accum in range(1, len(paths)):
try:
- partial = sep.join(paths[:accum])
- d = self.addMailbox(partial)
+ partial_path = sep.join(paths[:accum])
+ d = self.addMailbox(partial_path)
subs.append(d)
+ # XXX should this be handled by the deferred?
except imap4.MailboxCollision:
pass
try:
@@ -222,21 +199,13 @@ class IMAPAccount(WithMsgFields):
def all_good(result):
return all(result)
- def load_mbox_cache(result):
- d = self._load_mailboxes()
- d.addCallback(lambda _: result)
- return d
-
if subs:
d1 = defer.gatherResults(subs, consumeErrors=True)
- d1.addCallback(load_mbox_cache)
d1.addCallback(all_good)
else:
d1 = defer.succeed(False)
- d1.addCallback(load_mbox_cache)
return d1
- # TODO use mail.Account.get_collection_by_mailbox
def select(self, name, readwrite=1):
"""
Selects a mailbox.
@@ -250,15 +219,28 @@ class IMAPAccount(WithMsgFields):
:rtype: SoledadMailbox
"""
name = normalize_mailbox(name)
- if name not in self.mailboxes:
- logger.warning("No such mailbox!")
- return None
- self.selected = name
- sm = SoledadMailbox(name, self._store, readwrite)
- return sm
+ def check_it_exists(mailboxes):
+ if name not in mailboxes:
+ logger.warning("SELECT: No such mailbox!")
+ return None
+ return name
+
+ def set_selected(_):
+ self.selected = name
+
+ def get_collection(name):
+ if name is None:
+ return None
+ return self.account.get_collection_by_mailbox(name)
+
+ d = self.account.list_all_mailbox_names()
+ d.addCallback(check_it_exists)
+ d.addCallback(get_collection)
+ d.addCallback(partial(
+ self._return_mailbox_from_collection, readwrite=readwrite))
+ return d
- # TODO use mail.Account.delete_mailbox
def delete(self, name, force=False):
"""
Deletes a mailbox.
@@ -276,37 +258,52 @@ class IMAPAccount(WithMsgFields):
:rtype: Deferred
"""
name = normalize_mailbox(name)
+ _mboxes = []
- if name not in self.mailboxes:
- err = imap4.MailboxException("No such mailbox: %r" % name)
- return defer.fail(err)
- mbox = self.getMailbox(name)
+ def check_it_exists(mailboxes):
+ # FIXME works? -- pass variable ref to outer scope
+ _mboxes = mailboxes
+ if name not in mailboxes:
+ err = imap4.MailboxException("No such mailbox: %r" % name)
+ return defer.fail(err)
- if not force:
+ def get_mailbox(_):
+ return self.getMailbox(name)
+
+ def destroy_mailbox(mbox):
+ return mbox.destroy()
+
+ def check_can_be_deleted(mbox):
# See if this box is flagged \Noselect
- # XXX use mbox.flags instead?
mbox_flags = mbox.getFlags()
- if self.NOSELECT_FLAG in mbox_flags:
+ if MessageFlags.NOSELECT_FLAG in mbox_flags:
# Check for hierarchically inferior mailboxes with this one
# as part of their root.
- for others in self.mailboxes:
+ for others in _mboxes:
if others != name and others.startswith(name):
err = imap4.MailboxException(
"Hierarchically inferior mailboxes "
"exist and \\Noselect is set")
return defer.fail(err)
- self.__mailboxes.discard(name)
- return mbox.destroy()
+ return mbox
- # XXX FIXME --- not honoring the inferior names...
+ d = self.account.list_all_mailbox_names()
+ d.addCallback(check_it_exists)
+ d.addCallback(get_mailbox)
+ if not force:
+ d.addCallback(check_can_be_deleted)
+ d.addCallback(destroy_mailbox)
+ return d
+ # FIXME --- not honoring the inferior names...
# if there are no hierarchically inferior names, we will
# delete it from our ken.
+ # XXX is this right?
# if self._inferiorNames(name) > 1:
- # ??! -- can this be rite?
- # self._index.removeMailbox(name)
+ # self._index.removeMailbox(name)
# TODO use mail.Account.rename_mailbox
+ # TODO finish conversion to deferreds
def rename(self, oldname, newname):
"""
Renames a mailbox.
@@ -320,6 +317,9 @@ class IMAPAccount(WithMsgFields):
oldname = normalize_mailbox(oldname)
newname = normalize_mailbox(newname)
+ # FIXME check that scope works (test)
+ _mboxes = []
+
if oldname not in self.mailboxes:
raise imap4.NoSuchMailbox(repr(oldname))
@@ -327,34 +327,19 @@ class IMAPAccount(WithMsgFields):
inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
for (old, new) in inferiors:
- if new in self.mailboxes:
+ if new in _mboxes:
raise imap4.MailboxCollision(repr(new))
rename_deferreds = []
- def load_mbox_cache(result):
- d = self._load_mailboxes()
- d.addCallback(lambda _: result)
- return d
-
- def update_mbox_doc_name(mbox, oldname, newname, update_deferred):
- mbox.content[self.MBOX_KEY] = newname
- d = self._soledad.put_doc(mbox)
- d.addCallback(lambda r: update_deferred.callback(True))
-
for (old, new) in inferiors:
- self.__mailboxes.discard(old)
- self._memstore.rename_fdocs_mailbox(old, new)
-
- d0 = defer.Deferred()
- d = self._get_mailbox_by_name(old)
- d.addCallback(update_mbox_doc_name, old, new, d0)
- rename_deferreds.append(d0)
+ d = self.account.rename_mailbox(old, new)
+ rename_deferreds.append(d)
d1 = defer.gatherResults(rename_deferreds, consumeErrors=True)
- d1.addCallback(load_mbox_cache)
return d1
+ # FIXME use deferreds (list_all_mailbox_names, etc)
def _inferiorNames(self, name):
"""
Return hierarchically inferior mailboxes.
@@ -387,16 +372,15 @@ class IMAPAccount(WithMsgFields):
:type wildcard: str
"""
# XXX use wildcard in index query
- ref = self._inferiorNames(normalize_mailbox(ref))
+ # TODO get deferreds
wildcard = imap4.wildcardToRegexp(wildcard, '/')
+ ref = self._inferiorNames(normalize_mailbox(ref))
return [(i, self.getMailbox(i)) for i in ref if wildcard.match(i)]
#
# The rest of the methods are specific for leap.mail.imap.account.Account
#
- # TODO ------------------ can we preserve the attr?
- # maybe add to memory store.
def isSubscribed(self, name):
"""
Returns True if user is subscribed to this mailbox.
@@ -406,63 +390,13 @@ class IMAPAccount(WithMsgFields):
:rtype: Deferred (will fire with bool)
"""
- # TODO use Flags class
- subscribed = self.SUBSCRIBED_KEY
-
- def is_subscribed(mbox):
- subs_bool = bool(mbox.content.get(subscribed, False))
- return subs_bool
-
- d = self._get_mailbox_by_name(name)
- d.addCallback(is_subscribed)
- return d
-
- # TODO ------------------ can we preserve the property?
- # maybe add to memory store.
-
- def _get_subscriptions(self):
- """
- Return a list of the current subscriptions for this account.
-
- :returns: A deferred that will fire with the subscriptions.
- :rtype: Deferred
- """
- def get_docs_content(docs):
- return [doc.content[self.MBOX_KEY] for doc in docs]
-
- d = self._store.get_from_index(
- self.TYPE_SUBS_IDX, self.MBOX_KEY, '1')
- d.addCallback(get_docs_content)
- return d
-
- def _set_subscription(self, name, value):
- """
- Sets the subscription value for a given mailbox
-
- :param name: the mailbox
- :type name: str
-
- :param value: the boolean value
- :type value: bool
- """
- # XXX Note that this kind of operation has
- # no guarantees of atomicity. We should not be accessing mbox
- # documents concurrently.
-
- subscribed = self.SUBSCRIBED_KEY
+ name = normalize_mailbox(name)
- def update_subscribed_value(mbox):
- mbox.content[subscribed] = value
- return self._store.put_doc(mbox)
+ def get_subscribed(mbox):
+ return mbox.get_mbox_attr("subscribed")
- # maybe we should store subscriptions in another
- # document...
- if name not in self.mailboxes:
- d = self.addMailbox(name)
- d.addCallback(lambda v: self._get_mailbox_by_name(name))
- else:
- d = self._get_mailbox_by_name(name)
- d.addCallback(update_subscribed_value)
+ d = self.getMailbox(name)
+ d.addCallback(get_subscribed)
return d
def subscribe(self, name):
@@ -475,11 +409,11 @@ class IMAPAccount(WithMsgFields):
"""
name = normalize_mailbox(name)
- def check_and_subscribe(subscriptions):
- if name not in subscriptions:
- return self._set_subscription(name, True)
- d = self._get_subscriptions()
- d.addCallback(check_and_subscribe)
+ def set_subscribed(mbox):
+ return mbox.set_mbox_attr("subscribed", True)
+
+ d = self.getMailbox(name)
+ d.addCallback(set_subscribed)
return d
def unsubscribe(self, name):
@@ -492,17 +426,17 @@ class IMAPAccount(WithMsgFields):
"""
name = normalize_mailbox(name)
- def check_and_unsubscribe(subscriptions):
- if name not in subscriptions:
- raise imap4.MailboxException(
- "Not currently subscribed to %r" % name)
- return self._set_subscription(name, False)
- d = self._get_subscriptions()
- d.addCallback(check_and_unsubscribe)
+ def set_unsubscribed(mbox):
+ return mbox.set_mbox_attr("subscribed", False)
+
+ d = self.getMailbox(name)
+ d.addCallback(set_unsubscribed)
return d
+ # TODO -- get__all_mboxes, return tuple
+ # with ... name? and subscribed bool...
def getSubscriptions(self):
- return self._get_subscriptions()
+ raise NotImplementedError()
#
# INamespacePresenter
@@ -517,20 +451,6 @@ class IMAPAccount(WithMsgFields):
def getOtherNamespaces(self):
return None
- # extra, for convenience
-
- def deleteAllMessages(self, iknowhatiamdoing=False):
- """
- Deletes all messages from all mailboxes.
- Danger! high voltage!
-
- :param iknowhatiamdoing: confirmation parameter, needs to be True
- to proceed.
- """
- if iknowhatiamdoing is True:
- for mbox in self.mailboxes:
- self.delete(mbox, force=True)
-
def __repr__(self):
"""
Representation string for this object.