summaryrefslogtreecommitdiff
path: root/src/leap/mail
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mail')
-rw-r--r--src/leap/mail/imap/account.py426
-rw-r--r--src/leap/mail/imap/fields.py127
-rw-r--r--src/leap/mail/imap/index.py69
-rw-r--r--src/leap/mail/imap/mailbox.py617
-rw-r--r--src/leap/mail/imap/messages.py735
-rw-r--r--src/leap/mail/imap/parser.py93
-rw-r--r--src/leap/mail/imap/server.py1897
-rw-r--r--src/leap/mail/imap/service/imap.py2
8 files changed, 2068 insertions, 1898 deletions
diff --git a/src/leap/mail/imap/account.py b/src/leap/mail/imap/account.py
new file mode 100644
index 0000000..fd861e7
--- /dev/null
+++ b/src/leap/mail/imap/account.py
@@ -0,0 +1,426 @@
+# -*- coding: utf-8 -*-
+# account.py
+# Copyright (C) 2013 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
+# 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 General Public License for more details.
+#
+# 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.
+"""
+import copy
+import time
+
+from twisted.mail import imap4
+from zope.interface import implements
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.mail.imap.index import IndexedDB
+from leap.mail.imap.fields import WithMsgFields
+from leap.mail.imap.parser import MBoxParser
+from leap.mail.imap.mailbox import SoledadMailbox
+from leap.soledad.client import Soledad
+
+
+#######################################
+# Soledad Account
+#######################################
+
+
+class SoledadBackedAccount(WithMsgFields, IndexedDB, MBoxParser):
+ """
+ An implementation of IAccount and INamespacePresenteer
+ that is backed by Soledad Encrypted Documents.
+ """
+
+ implements(imap4.IAccount, imap4.INamespacePresenter)
+
+ _soledad = None
+ selected = None
+
+ def __init__(self, account_name, soledad=None):
+ """
+ Creates a SoledadAccountIndex that keeps track of the mailboxes
+ and subscriptions handled by this account.
+
+ :param acct_name: The name of the account (user id).
+ :type acct_name: str
+
+ :param soledad: a Soledad instance.
+ :param soledad: Soledad
+ """
+ leap_assert(soledad, "Need a soledad instance to initialize")
+ leap_assert_type(soledad, Soledad)
+
+ # XXX SHOULD assert too that the name matches the user/uuid with which
+ # soledad has been initialized.
+
+ self._account_name = self._parse_mailbox_name(account_name)
+ self._soledad = soledad
+
+ self.initialize_db()
+
+ # every user should have the right to an inbox folder
+ # at least, so let's make one!
+
+ if not self.mailboxes:
+ self.addMailbox(self.INBOX_NAME)
+
+ def _get_empty_mailbox(self):
+ """
+ Returns an empty mailbox.
+
+ :rtype: dict
+ """
+ return copy.deepcopy(self.EMPTY_MBOX)
+
+ 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
+ """
+ doc = self._soledad.get_from_index(
+ self.TYPE_MBOX_IDX, self.MBOX_KEY,
+ self._parse_mailbox_name(name))
+ return doc[0] if doc else None
+
+ @property
+ def mailboxes(self):
+ """
+ A list of the current mailboxes for this account.
+ """
+ return [doc.content[self.MBOX_KEY]
+ for doc in self._soledad.get_from_index(
+ self.TYPE_IDX, self.MBOX_KEY)]
+
+ @property
+ def subscriptions(self):
+ """
+ A list of the current subscriptions for this account.
+ """
+ return [doc.content[self.MBOX_KEY]
+ for doc in self._soledad.get_from_index(
+ self.TYPE_SUBS_IDX, self.MBOX_KEY, '1')]
+
+ def getMailbox(self, name):
+ """
+ Returns a Mailbox with that name, without selecting it.
+
+ :param name: name of the mailbox
+ :type name: str
+
+ :returns: a a SoledadMailbox instance
+ :rtype: SoledadMailbox
+ """
+ name = self._parse_mailbox_name(name)
+
+ if name not in self.mailboxes:
+ raise imap4.MailboxException("No such mailbox")
+
+ return SoledadMailbox(name, soledad=self._soledad)
+
+ ##
+ ## IAccount
+ ##
+
+ def addMailbox(self, name, creation_ts=None):
+ """
+ Add a mailbox to the account.
+
+ :param name: the name of the mailbox
+ :type name: str
+
+ :param creation_ts: an optional creation timestamp to be used as
+ mailbox id. A timestamp will be used if no
+ one is provided.
+ :type creation_ts: int
+
+ :returns: True if successful
+ :rtype: bool
+ """
+ name = self._parse_mailbox_name(name)
+
+ if name in self.mailboxes:
+ raise imap4.MailboxCollision, name
+
+ if not creation_ts:
+ # by default, we pass an int value
+ # taken from the current time
+ # we make sure to take enough decimals to get a unique
+ # mailbox-uidvalidity.
+ creation_ts = int(time.time() * 10E2)
+
+ mbox = self._get_empty_mailbox()
+ mbox[self.MBOX_KEY] = name
+ mbox[self.CREATED_KEY] = creation_ts
+
+ doc = self._soledad.create_doc(mbox)
+ return bool(doc)
+
+ def create(self, pathspec):
+ """
+ Create a new mailbox from the given hierarchical name.
+
+ :param pathspec: The full hierarchical name of a new mailbox to create.
+ If any of the inferior hierarchical names to this one
+ do not exist, they are created as well.
+ :type pathspec: str
+
+ :return: A true value if the creation succeeds.
+ :rtype: bool
+
+ :raise MailboxException: Raised if this mailbox cannot be added.
+ """
+ # TODO raise MailboxException
+ paths = filter(
+ None,
+ self._parse_mailbox_name(pathspec).split('/'))
+ for accum in range(1, len(paths)):
+ try:
+ self.addMailbox('/'.join(paths[:accum]))
+ except imap4.MailboxCollision:
+ pass
+ try:
+ self.addMailbox('/'.join(paths))
+ except imap4.MailboxCollision:
+ if not pathspec.endswith('/'):
+ return False
+ return True
+
+ def select(self, name, readwrite=1):
+ """
+ Selects a mailbox.
+
+ :param name: the mailbox to select
+ :type name: str
+
+ :param readwrite: 1 for readwrite permissions.
+ :type readwrite: int
+
+ :rtype: bool
+ """
+ name = self._parse_mailbox_name(name)
+
+ if name not in self.mailboxes:
+ return None
+
+ self.selected = name
+
+ return SoledadMailbox(
+ name, rw=readwrite,
+ soledad=self._soledad)
+
+ def delete(self, name, force=False):
+ """
+ Deletes a mailbox.
+
+ Right now it does not purge the messages, but just removes the mailbox
+ name from the mailboxes list!!!
+
+ :param name: the mailbox to be deleted
+ :type name: str
+
+ :param force: if True, it will not check for noselect flag or inferior
+ names. use with care.
+ :type force: bool
+ """
+ name = self._parse_mailbox_name(name)
+
+ if not name in self.mailboxes:
+ raise imap4.MailboxException("No such mailbox")
+
+ mbox = self.getMailbox(name)
+
+ if force is False:
+ # See if this box is flagged \Noselect
+ # XXX use mbox.flags instead?
+ if self.NOSELECT_FLAG in mbox.getFlags():
+ # Check for hierarchically inferior mailboxes with this one
+ # as part of their root.
+ for others in self.mailboxes:
+ if others != name and others.startswith(name):
+ raise imap4.MailboxException, (
+ "Hierarchically inferior mailboxes "
+ "exist and \\Noselect is set")
+ mbox.destroy()
+
+ # XXX FIXME --- not honoring the inferior names...
+
+ # if there are no hierarchically inferior names, we will
+ # delete it from our ken.
+ #if self._inferiorNames(name) > 1:
+ # ??! -- can this be rite?
+ #self._index.removeMailbox(name)
+
+ def rename(self, oldname, newname):
+ """
+ Renames a mailbox.
+
+ :param oldname: old name of the mailbox
+ :type oldname: str
+
+ :param newname: new name of the mailbox
+ :type newname: str
+ """
+ oldname = self._parse_mailbox_name(oldname)
+ newname = self._parse_mailbox_name(newname)
+
+ if oldname not in self.mailboxes:
+ raise imap4.NoSuchMailbox, oldname
+
+ inferiors = self._inferiorNames(oldname)
+ inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
+
+ for (old, new) in inferiors:
+ if new in self.mailboxes:
+ raise imap4.MailboxCollision, new
+
+ for (old, new) in inferiors:
+ mbox = self._get_mailbox_by_name(old)
+ mbox.content[self.MBOX_KEY] = new
+ self._soledad.put_doc(mbox)
+
+ # XXX ---- FIXME!!!! ------------------------------------
+ # until here we just renamed the index...
+ # We have to rename also the occurrence of this
+ # mailbox on ALL the messages that are contained in it!!!
+ # ... we maybe could use a reference to the doc_id
+ # in each msg, instead of the "mbox" field in msgs
+ # -------------------------------------------------------
+
+ def _inferiorNames(self, name):
+ """
+ Return hierarchically inferior mailboxes.
+
+ :param name: name of the mailbox
+ :rtype: list
+ """
+ # XXX use wildcard query instead
+ inferiors = []
+ for infname in self.mailboxes:
+ if infname.startswith(name):
+ inferiors.append(infname)
+ return inferiors
+
+ def isSubscribed(self, name):
+ """
+ Returns True if user is subscribed to this mailbox.
+
+ :param name: the mailbox to be checked.
+ :type name: str
+
+ :rtype: bool
+ """
+ mbox = self._get_mailbox_by_name(name)
+ return mbox.content.get('subscribed', False)
+
+ 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
+ """
+ # maybe we should store subscriptions in another
+ # document...
+ if not name in self.mailboxes:
+ self.addMailbox(name)
+ mbox = self._get_mailbox_by_name(name)
+
+ if mbox:
+ mbox.content[self.SUBSCRIBED_KEY] = value
+ self._soledad.put_doc(mbox)
+
+ def subscribe(self, name):
+ """
+ Subscribe to this mailbox
+
+ :param name: name of the mailbox
+ :type name: str
+ """
+ name = self._parse_mailbox_name(name)
+ if name not in self.subscriptions:
+ self._set_subscription(name, True)
+
+ def unsubscribe(self, name):
+ """
+ Unsubscribe from this mailbox
+
+ :param name: name of the mailbox
+ :type name: str
+ """
+ name = self._parse_mailbox_name(name)
+ if name not in self.subscriptions:
+ raise imap4.MailboxException, "Not currently subscribed to " + name
+ self._set_subscription(name, False)
+
+ def listMailboxes(self, ref, wildcard):
+ """
+ List the mailboxes.
+
+ from rfc 3501:
+ returns a subset of names from the complete set
+ of all names available to the client. Zero or more untagged LIST
+ replies are returned, containing the name attributes, hierarchy
+ delimiter, and name.
+
+ :param ref: reference name
+ :type ref: str
+
+ :param wildcard: mailbox name with possible wildcards
+ :type wildcard: str
+ """
+ # XXX use wildcard in index query
+ ref = self._inferiorNames(
+ self._parse_mailbox_name(ref))
+ wildcard = imap4.wildcardToRegexp(wildcard, '/')
+ return [(i, self.getMailbox(i)) for i in ref if wildcard.match(i)]
+
+ ##
+ ## INamespacePresenter
+ ##
+
+ def getPersonalNamespaces(self):
+ return [["", "/"]]
+
+ def getSharedNamespaces(self):
+ return None
+
+ 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.
+ """
+ return "<SoledadBackedAccount (%s)>" % self._account_name
diff --git a/src/leap/mail/imap/fields.py b/src/leap/mail/imap/fields.py
new file mode 100644
index 0000000..96b937e
--- /dev/null
+++ b/src/leap/mail/imap/fields.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# fields.py
+# Copyright (C) 2013 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
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Fields for Mailbox and Message.
+"""
+from leap.mail.imap.parser import MBoxParser
+
+
+class WithMsgFields(object):
+ """
+ Container class for class-attributes to be shared by
+ several message-related classes.
+ """
+ # Internal representation of Message
+ DATE_KEY = "date"
+ HEADERS_KEY = "headers"
+ FLAGS_KEY = "flags"
+ MBOX_KEY = "mbox"
+ CONTENT_HASH_KEY = "chash"
+ RAW_KEY = "raw"
+ SUBJECT_KEY = "subject"
+ UID_KEY = "uid"
+ MULTIPART_KEY = "multi"
+ SIZE_KEY = "size"
+
+ # Mailbox specific keys
+ CLOSED_KEY = "closed"
+ CREATED_KEY = "created"
+ SUBSCRIBED_KEY = "subscribed"
+ RW_KEY = "rw"
+ LAST_UID_KEY = "lastuid"
+
+ # Document Type, for indexing
+ TYPE_KEY = "type"
+ TYPE_MBOX_VAL = "mbox"
+ TYPE_MESSAGE_VAL = "msg"
+ TYPE_FLAGS_VAL = "flags"
+ TYPE_HEADERS_VAL = "head"
+ TYPE_ATTACHMENT_VAL = "attach"
+ # should add also a headers val
+
+ INBOX_VAL = "inbox"
+
+ # Flags for SoledadDocument for indexing.
+ SEEN_KEY = "seen"
+ RECENT_KEY = "recent"
+
+ # Flags in Mailbox and Message
+ SEEN_FLAG = "\\Seen"
+ RECENT_FLAG = "\\Recent"
+ ANSWERED_FLAG = "\\Answered"
+ FLAGGED_FLAG = "\\Flagged" # yo dawg
+ DELETED_FLAG = "\\Deleted"
+ DRAFT_FLAG = "\\Draft"
+ NOSELECT_FLAG = "\\Noselect"
+ LIST_FLAG = "List" # is this OK? (no \. ie, no system flag)
+
+ # Fields in mail object
+ SUBJECT_FIELD = "Subject"
+ DATE_FIELD = "Date"
+
+ # Index types
+ # --------------
+
+ TYPE_IDX = 'by-type'
+ TYPE_MBOX_IDX = 'by-type-and-mbox'
+ TYPE_MBOX_UID_IDX = 'by-type-and-mbox-and-uid'
+ TYPE_SUBS_IDX = 'by-type-and-subscribed'
+ TYPE_MBOX_SEEN_IDX = 'by-type-and-mbox-and-seen'
+ TYPE_MBOX_RECT_IDX = 'by-type-and-mbox-and-recent'
+ TYPE_HASH_IDX = 'by-type-and-hash'
+
+ # Tomas created the `recent and seen index`, but the semantic is not too
+ # correct since the recent flag is volatile.
+ TYPE_MBOX_RECT_SEEN_IDX = 'by-type-and-mbox-and-recent-and-seen'
+
+ KTYPE = TYPE_KEY
+ MBOX_VAL = TYPE_MBOX_VAL
+ HASH_VAL = CONTENT_HASH_KEY
+
+ INDEXES = {
+ # generic
+ TYPE_IDX: [KTYPE],
+ TYPE_MBOX_IDX: [KTYPE, MBOX_VAL],
+ TYPE_MBOX_UID_IDX: [KTYPE, MBOX_VAL, UID_KEY],
+
+ # mailboxes
+ TYPE_SUBS_IDX: [KTYPE, 'bool(subscribed)'],
+
+ # content, headers doc
+ TYPE_HASH_IDX: [KTYPE, HASH_VAL],
+
+ # messages
+ TYPE_MBOX_SEEN_IDX: [KTYPE, MBOX_VAL, 'bool(seen)'],
+ TYPE_MBOX_RECT_IDX: [KTYPE, MBOX_VAL, 'bool(recent)'],
+ TYPE_MBOX_RECT_SEEN_IDX: [KTYPE, MBOX_VAL,
+ 'bool(recent)', 'bool(seen)'],
+ }
+
+ MBOX_KEY = MBOX_VAL
+
+ EMPTY_MBOX = {
+ TYPE_KEY: MBOX_KEY,
+ TYPE_MBOX_VAL: MBoxParser.INBOX_NAME,
+ SUBJECT_KEY: "",
+ FLAGS_KEY: [],
+ CLOSED_KEY: False,
+ SUBSCRIBED_KEY: False,
+ RW_KEY: 1,
+ LAST_UID_KEY: 0
+ }
+
+fields = WithMsgFields # alias for convenience
diff --git a/src/leap/mail/imap/index.py b/src/leap/mail/imap/index.py
new file mode 100644
index 0000000..2280d86
--- /dev/null
+++ b/src/leap/mail/imap/index.py
@@ -0,0 +1,69 @@
+# -*- coding: utf-8 -*-
+# index.py
+# Copyright (C) 2013 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
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Index for SoledadBackedAccount, Mailbox and Messages.
+"""
+import logging
+
+from leap.common.check import leap_assert, leap_assert_type
+
+from leap.mail.imap.account import SoledadBackedAccount
+
+
+logger = logging.getLogger(__name__)
+
+
+class IndexedDB(object):
+ """
+ Methods dealing with the index.
+
+ This is a MixIn that needs access to the soledad instance,
+ and also assumes that a INDEXES attribute is accessible to the instance.
+
+ INDEXES must be a dictionary of type:
+ {'index-name': ['field1', 'field2']}
+ """
+ # TODO we might want to move this to soledad itself, check
+
+ def initialize_db(self):
+ """
+ Initialize the database.
+ """
+ leap_assert(self._soledad,
+ "Need a soledad attribute accesible in the instance")
+ leap_assert_type(self.INDEXES, dict)
+
+ # Ask the database for currently existing indexes.
+ if not self._soledad:
+ logger.debug("NO SOLEDAD ON IMAP INITIALIZATION")
+ return
+ db_indexes = dict()
+ if self._soledad is not None:
+ db_indexes = dict(self._soledad.list_indexes())
+ for name, expression in SoledadBackedAccount.INDEXES.items():
+ if name not in db_indexes:
+ # The index does not yet exist.
+ self._soledad.create_index(name, *expression)
+ continue
+
+ if expression == db_indexes[name]:
+ # The index exists and is up to date.
+ continue
+ # The index exists but the definition is not what expected, so we
+ # delete it and add the proper index expression.
+ self._soledad.delete_index(name)
+ self._soledad.create_index(name, *expression)
diff --git a/src/leap/mail/imap/mailbox.py b/src/leap/mail/imap/mailbox.py
new file mode 100644
index 0000000..09c06a2
--- /dev/null
+++ b/src/leap/mail/imap/mailbox.py
@@ -0,0 +1,617 @@
+# *- coding: utf-8 -*-
+# mailbox.py
+# Copyright (C) 2013 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
+# 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 General Public License for more details.
+#
+# 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 Mailbox.
+"""
+import logging
+from collections import defaultdict
+
+from twisted.internet import defer
+from twisted.python import log
+
+from twisted.mail import imap4
+from zope.interface import implements
+
+from leap.common import events as leap_events
+from leap.common.events.events_pb2 import IMAP_UNREAD_MAIL
+from leap.common.check import leap_assert, leap_assert_type
+from leap.mail.decorators import deferred
+from leap.mail.imap.fields import WithMsgFields, fields
+from leap.mail.imap.messages import MessageCollection
+from leap.mail.imap.parser import MBoxParser
+
+logger = logging.getLogger(__name__)
+
+
+class SoledadMailbox(WithMsgFields, MBoxParser):
+ """
+ A Soledad-backed IMAP mailbox.
+
+ Implements the high-level method needed for the Mailbox interfaces.
+ The low-level database methods are contained in MessageCollection class,
+ which we instantiate and make accessible in the `messages` attribute.
+ """
+ implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox)
+ # XXX should finish the implementation of IMailboxListener
+ # XXX should implement IMessageCopier too
+
+ messages = None
+ _closed = False
+
+ INIT_FLAGS = (WithMsgFields.SEEN_FLAG, WithMsgFields.ANSWERED_FLAG,
+ WithMsgFields.FLAGGED_FLAG, WithMsgFields.DELETED_FLAG,
+ WithMsgFields.DRAFT_FLAG, WithMsgFields.RECENT_FLAG,
+ WithMsgFields.LIST_FLAG)
+ flags = None
+
+ CMD_MSG = "MESSAGES"
+ CMD_RECENT = "RECENT"
+ CMD_UIDNEXT = "UIDNEXT"
+ CMD_UIDVALIDITY = "UIDVALIDITY"
+ CMD_UNSEEN = "UNSEEN"
+
+ _listeners = defaultdict(set)
+
+ def __init__(self, mbox, soledad=None, rw=1):
+ """
+ SoledadMailbox constructor. Needs to get passed a name, plus a
+ Soledad instance.
+
+ :param mbox: the mailbox name
+ :type mbox: str
+
+ :param soledad: a Soledad instance.
+ :type soledad: Soledad
+
+ :param rw: read-and-write flags
+ :type rw: int
+ """
+ leap_assert(mbox, "Need a mailbox name to initialize")
+ leap_assert(soledad, "Need a soledad instance to initialize")
+
+ # XXX should move to wrapper
+ #leap_assert(isinstance(soledad._db, SQLCipherDatabase),
+ #"soledad._db must be an instance of SQLCipherDatabase")
+
+ self.mbox = self._parse_mailbox_name(mbox)
+ self.rw = rw
+
+ self._soledad = soledad
+
+ self.messages = MessageCollection(
+ mbox=mbox, soledad=self._soledad)
+
+ if not self.getFlags():
+ self.setFlags(self.INIT_FLAGS)
+
+ @property
+ def listeners(self):
+ """
+ Returns listeners for this mbox.
+
+ The server itself is a listener to the mailbox.
+ so we can notify it (and should!) after changes in flags
+ and number of messages.
+
+ :rtype: set
+ """
+ return self._listeners[self.mbox]
+
+ def addListener(self, listener):
+ """
+ Adds a listener to the listeners queue.
+ The server adds itself as a listener when there is a SELECT,
+ so it can send EXIST commands.
+
+ :param listener: listener to add
+ :type listener: an object that implements IMailboxListener
+ """
+ logger.debug('adding mailbox listener: %s' % listener)
+ self.listeners.add(listener)
+
+ def removeListener(self, listener):
+ """
+ Removes a listener from the listeners queue.
+
+ :param listener: listener to remove
+ :type listener: an object that implements IMailboxListener
+ """
+ self.listeners.remove(listener)
+
+ def _get_mbox(self):
+ """
+ Returns mailbox document.
+
+ :return: A SoledadDocument containing this mailbox, or None if
+ the query failed.
+ :rtype: SoledadDocument or None.
+ """
+ try:
+ query = self._soledad.get_from_index(
+ fields.TYPE_MBOX_IDX,
+ fields.TYPE_MBOX_VAL, self.mbox)
+ if query:
+ return query.pop()
+ except Exception as exc:
+ logger.error("Unhandled error %r" % exc)
+
+ def getFlags(self):
+ """
+ Returns the flags defined for this mailbox.
+
+ :returns: tuple of flags for this mailbox
+ :rtype: tuple of str
+ """
+ mbox = self._get_mbox()
+ if not mbox:
+ return None
+ flags = mbox.content.get(self.FLAGS_KEY, [])
+ return map(str, flags)
+
+ def setFlags(self, flags):
+ """
+ Sets flags for this mailbox.
+
+ :param flags: a tuple with the flags
+ :type flags: tuple of str
+ """
+ leap_assert(isinstance(flags, tuple),
+ "flags expected to be a tuple")
+ mbox = self._get_mbox()
+ if not mbox:
+ return None
+ mbox.content[self.FLAGS_KEY] = map(str, flags)
+ self._soledad.put_doc(mbox)
+
+ # XXX SHOULD BETTER IMPLEMENT ADD_FLAG, REMOVE_FLAG.
+
+ def _get_closed(self):
+ """
+ Return the closed attribute for this mailbox.
+
+ :return: True if the mailbox is closed
+ :rtype: bool
+ """
+ mbox = self._get_mbox()
+ return mbox.content.get(self.CLOSED_KEY, False)
+
+ def _set_closed(self, closed):
+ """
+ Set the closed attribute for this mailbox.
+
+ :param closed: the state to be set
+ :type closed: bool
+ """
+ leap_assert(isinstance(closed, bool), "closed needs to be boolean")
+ mbox = self._get_mbox()
+ mbox.content[self.CLOSED_KEY] = closed
+ self._soledad.put_doc(mbox)
+
+ closed = property(
+ _get_closed, _set_closed, doc="Closed attribute.")
+
+ def _get_last_uid(self):
+ """
+ Return the last uid for this mailbox.
+
+ :return: the last uid for messages in this mailbox
+ :rtype: bool
+ """
+ mbox = self._get_mbox()
+ return mbox.content.get(self.LAST_UID_KEY, 1)
+
+ def _set_last_uid(self, uid):
+ """
+ Sets the last uid for this mailbox.
+
+ :param uid: the uid to be set
+ :type uid: int
+ """
+ leap_assert(isinstance(uid, int), "uid has to be int")
+ mbox = self._get_mbox()
+ key = self.LAST_UID_KEY
+
+ count = self.getMessageCount()
+
+ # XXX safety-catch. If we do get duplicates,
+ # we want to avoid further duplication.
+
+ if uid >= count:
+ value = uid
+ else:
+ # something is wrong,
+ # just set the last uid
+ # beyond the max msg count.
+ logger.debug("WRONG uid < count. Setting last uid to %s", count)
+ value = count
+
+ mbox.content[key] = value
+ self._soledad.put_doc(mbox)
+
+ last_uid = property(
+ _get_last_uid, _set_last_uid, doc="Last_UID attribute.")
+
+ def getUIDValidity(self):
+ """
+ Return the unique validity identifier for this mailbox.
+
+ :return: unique validity identifier
+ :rtype: int
+ """
+ mbox = self._get_mbox()
+ return mbox.content.get(self.CREATED_KEY, 1)
+
+ def getUID(self, message):
+ """
+ Return the UID of a message in the mailbox
+
+ .. note:: this implementation does not make much sense RIGHT NOW,
+ but in the future will be useful to get absolute UIDs from
+ message sequence numbers.
+
+ :param message: the message uid
+ :type message: int
+
+ :rtype: int
+ """
+ msg = self.messages.get_msg_by_uid(message)
+ return msg.getUID()
+
+ def getUIDNext(self):
+ """
+ Return the likely UID for the next message added to this
+ mailbox. Currently it returns the higher UID incremented by
+ one.
+
+ We increment the next uid *each* time this function gets called.
+ In this way, there will be gaps if the message with the allocated
+ uid cannot be saved. But that is preferable to having race conditions
+ if we get to parallel message adding.
+
+ :rtype: int
+ """
+ self.last_uid += 1
+ return self.last_uid
+
+ def getMessageCount(self):
+ """
+ Returns the total count of messages in this mailbox.
+
+ :rtype: int
+ """
+ return self.messages.count()
+
+ def getUnseenCount(self):
+ """
+ Returns the number of messages with the 'Unseen' flag.
+
+ :return: count of messages flagged `unseen`
+ :rtype: int
+ """
+ return self.messages.count_unseen()
+
+ def getRecentCount(self):
+ """
+ Returns the number of messages with the 'Recent' flag.
+
+ :return: count of messages flagged `recent`
+ :rtype: int
+ """
+ return self.messages.count_recent()
+
+ def isWriteable(self):
+ """
+ Get the read/write status of the mailbox.
+
+ :return: 1 if mailbox is read-writeable, 0 otherwise.
+ :rtype: int
+ """
+ return self.rw
+
+ def getHierarchicalDelimiter(self):
+ """
+ Returns the character used to delimite hierarchies in mailboxes.
+
+ :rtype: str
+ """
+ return '/'
+
+ def requestStatus(self, names):
+ """
+ Handles a status request by gathering the output of the different
+ status commands.
+
+ :param names: a list of strings containing the status commands
+ :type names: iter
+ """
+ r = {}
+ if self.CMD_MSG in names:
+ r[self.CMD_MSG] = self.getMessageCount()
+ if self.CMD_RECENT in names:
+ r[self.CMD_RECENT] = self.getRecentCount()
+ if self.CMD_UIDNEXT in names:
+ r[self.CMD_UIDNEXT] = self.last_uid + 1
+ if self.CMD_UIDVALIDITY in names:
+ r[self.CMD_UIDVALIDITY] = self.getUID()
+ if self.CMD_UNSEEN in names:
+ r[self.CMD_UNSEEN] = self.getUnseenCount()
+ return defer.succeed(r)
+
+ def addMessage(self, message, flags, date=None):
+ """
+ Adds a message to this mailbox.
+
+ :param message: the raw message
+ :type message: str
+
+ :param flags: flag list
+ :type flags: list of str
+
+ :param date: timestamp
+ :type date: str
+
+ :return: a deferred that evals to None
+ """
+ # XXX we should treat the message as an IMessage from here
+ leap_assert_type(message, basestring)
+ uid_next = self.getUIDNext()
+ logger.debug('Adding msg with UID :%s' % uid_next)
+ if flags is None:
+ flags = tuple()
+ else:
+ flags = tuple(str(flag) for flag in flags)
+
+ d = self._do_add_messages(message, flags, date, uid_next)
+ d.addCallback(self._notify_new)
+
+ @deferred
+ def _do_add_messages(self, message, flags, date, uid_next):
+ """
+ Calls to the messageCollection add_msg method (deferred to thread).
+ Invoked from addMessage.
+ """
+ self.messages.add_msg(message, flags=flags, date=date,
+ uid=uid_next)
+
+ def _notify_new(self, *args):
+ """
+ Notify of new messages to all the listeners.
+
+ :param args: ignored.
+ """
+ exists = self.getMessageCount()
+ recent = self.getRecentCount()
+ logger.debug("NOTIFY: there are %s messages, %s recent" % (
+ exists,
+ recent))
+
+ logger.debug("listeners: %s", str(self.listeners))
+ for l in self.listeners:
+ logger.debug('notifying...')
+ l.newMessages(exists, recent)
+
+ # commands, do not rename methods
+
+ def destroy(self):
+ """
+ Called before this mailbox is permanently deleted.
+
+ Should cleanup resources, and set the \\Noselect flag
+ on the mailbox.
+ """
+ self.setFlags((self.NOSELECT_FLAG,))
+ self.deleteAllDocs()
+
+ # XXX removing the mailbox in situ for now,
+ # we should postpone the removal
+ self._soledad.delete_doc(self._get_mbox())
+
+ def expunge(self):
+ """
+ Remove all messages flagged \\Deleted
+ """
+ if not self.isWriteable():
+ raise imap4.ReadOnlyMailbox
+ delete = []
+ deleted = []
+
+ for m in self.messages.get_all_docs():
+ # XXX should operate with LeapMessages instead,
+ # so we don't expose the implementation.
+ # (so, iterate for m in self.messages)
+ if self.DELETED_FLAG in m.content[self.FLAGS_KEY]:
+ delete.append(m)
+ for m in delete:
+ deleted.append(m.content)
+ self.messages.remove(m)
+
+ # XXX should return the UIDs of the deleted messages
+ # more generically
+ return [x for x in range(len(deleted))]
+
+ @deferred
+ def fetch(self, messages, uid):
+ """
+ Retrieve one or more messages in this mailbox.
+
+ from rfc 3501: The data items to be fetched can be either a single atom
+ or a parenthesized list.
+
+ :param messages: IDs of the messages to retrieve information about
+ :type messages: MessageSet
+
+ :param uid: If true, the IDs are UIDs. They are message sequence IDs
+ otherwise.
+ :type uid: bool
+
+ :rtype: A tuple of two-tuples of message sequence numbers and
+ LeapMessage
+ """
+ result = []
+ sequence = True if uid == 0 else False
+
+ if not messages.last:
+ try:
+ iter(messages)
+ except TypeError:
+ # looks like we cannot iterate
+ messages.last = self.last_uid
+
+ # for sequence numbers (uid = 0)
+ if sequence:
+ logger.debug("Getting msg by index: INEFFICIENT call!")
+ raise NotImplementedError
+
+ else:
+ for msg_id in messages:
+ msg = self.messages.get_msg_by_uid(msg_id)
+ if msg:
+ result.append((msg_id, msg))
+ else:
+ logger.debug("fetch %s, no msg found!!!" % msg_id)
+
+ if self.isWriteable():
+ self._unset_recent_flag()
+ self._signal_unread_to_ui()
+
+ # XXX workaround for hangs in thunderbird
+ #return tuple(result[:100]) # --- doesn't show all!!
+ return tuple(result)
+
+ @deferred
+ def _unset_recent_flag(self):
+ """
+ Unsets `Recent` flag from a tuple of messages.
+ Called from fetch.
+
+ From RFC, about `Recent`:
+
+ Message is "recently" arrived in this mailbox. This session
+ is the first session to have been notified about this
+ message; if the session is read-write, subsequent sessions
+ will not see \Recent set for this message. This flag can not
+ be altered by the client.
+
+ If it is not possible to determine whether or not this
+ session is the first session to be notified about a message,
+ then that message SHOULD be considered recent.
+ """
+ log.msg('unsetting recent flags...')
+ for msg in self.messages.get_recent():
+ msg.removeFlags((fields.RECENT_FLAG,))
+ self._signal_unread_to_ui()
+
+ @deferred
+ def _signal_unread_to_ui(self):
+ """
+ Sends unread event to ui.
+ """
+ unseen = self.getUnseenCount()
+ leap_events.signal(IMAP_UNREAD_MAIL, str(unseen))
+
+ @deferred
+ def store(self, messages, flags, mode, uid):
+ """
+ Sets the flags of one or more messages.
+
+ :param messages: The identifiers of the messages to set the flags
+ :type messages: A MessageSet object with the list of messages requested
+
+ :param flags: The flags to set, unset, or add.
+ :type flags: sequence of str
+
+ :param mode: If mode is -1, these flags should be removed from the
+ specified messages. If mode is 1, these flags should be
+ added to the specified messages. If mode is 0, all
+ existing flags should be cleared and these flags should be
+ added.
+ :type mode: -1, 0, or 1
+
+ :param uid: If true, the IDs specified in the query are UIDs;
+ otherwise they are message sequence IDs.
+ :type uid: bool
+
+ :return: A dict mapping message sequence numbers to sequences of
+ str representing the flags set on the message after this
+ operation has been performed.
+ :rtype: dict
+
+ :raise ReadOnlyMailbox: Raised if this mailbox is not open for
+ read-write.
+ """
+ # XXX implement also sequence (uid = 0)
+ # XXX we should prevent cclient from setting Recent flag.
+ leap_assert(not isinstance(flags, basestring),
+ "flags cannot be a string")
+ flags = tuple(flags)
+
+ if not self.isWriteable():
+ log.msg('read only mailbox!')
+ raise imap4.ReadOnlyMailbox
+
+ if not messages.last:
+ messages.last = self.messages.count()
+
+ result = {}
+ for msg_id in messages:
+ log.msg("MSG ID = %s" % msg_id)
+ msg = self.messages.get_msg_by_uid(msg_id)
+ if mode == 1:
+ msg.addFlags(flags)
+ elif mode == -1:
+ msg.removeFlags(flags)
+ elif mode == 0:
+ msg.setFlags(flags)
+ result[msg_id] = msg.getFlags()
+
+ self._signal_unread_to_ui()
+ return result
+
+ @deferred
+ def close(self):
+ """
+ Expunge and mark as closed
+ """
+ self.expunge()
+ self.closed = True
+
+ #@deferred
+ #def copy(self, messageObject):
+ #"""
+ #Copy the given message object into this mailbox.
+ #"""
+ # XXX should just:
+ # 1. Get the message._fdoc
+ # 2. Change the UID to UIDNext for this mailbox
+ # 3. Add implements IMessageCopier
+
+ # convenience fun
+
+ def deleteAllDocs(self):
+ """
+ Deletes all docs in this mailbox
+ """
+ docs = self.messages.get_all_docs()
+ for doc in docs:
+ self.messages._soledad.delete_doc(doc)
+
+ def __repr__(self):
+ """
+ Representation string for this mailbox.
+ """
+ return u"<SoledadMailbox: mbox '%s' (%s)>" % (
+ self.mbox, self.messages.count())
diff --git a/src/leap/mail/imap/messages.py b/src/leap/mail/imap/messages.py
new file mode 100644
index 0000000..b0d5da2
--- /dev/null
+++ b/src/leap/mail/imap/messages.py
@@ -0,0 +1,735 @@
+# -*- coding: utf-8 -*-
+# messages.py
+# Copyright (C) 2013 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
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+LeapMessage and MessageCollection.
+"""
+import copy
+import logging
+import StringIO
+from collections import namedtuple
+
+from twisted.mail import imap4
+from twisted.python import log
+from u1db import errors as u1db_errors
+from zope.interface import implements
+from zope.proxy import sameProxiedObjects
+
+from leap.common.check import leap_assert, leap_assert_type
+from leap.common.mail import get_email_charset
+from leap.mail.decorators import deferred
+from leap.mail.imap.account import SoledadBackedAccount
+from leap.mail.imap.index import IndexedDB
+from leap.mail.imap.fields import fields, WithMsgFields
+from leap.mail.imap.parser import MailParser, MBoxParser
+from leap.mail.messageflow import IMessageConsumer, MessageProducer
+
+logger = logging.getLogger(__name__)
+
+
+class LeapMessage(fields, MailParser, MBoxParser):
+
+ implements(imap4.IMessage)
+
+ def __init__(self, soledad, uid, mbox):
+ """
+ Initializes a LeapMessage.
+
+ :param soledad: a Soledad instance
+ :type soledad: Soledad
+ :param uid: the UID for the message.
+ :type uid: int or basestring
+ :param mbox: the mbox this message belongs to
+ :type mbox: basestring
+ """
+ MailParser.__init__(self)
+ self._soledad = soledad
+ self._uid = int(uid)
+ self._mbox = self._parse_mailbox_name(mbox)
+ self._chash = None
+
+ self.__cdoc = None
+
+ @property
+ def _fdoc(self):
+ """
+ An accessor to the flags document.
+ """
+ return self._get_flags_doc()
+
+ @property
+ def _cdoc(self):
+ """
+ An accessor to the content document.
+ """
+ if not self.__cdoc:
+ self.__cdoc = self._get_content_doc()
+ return self.__cdoc
+
+ @property
+ def _chash(self):
+ """
+ An accessor to the content hash for this message.
+ """
+ if not self._fdoc:
+ return None
+ return self._fdoc.content.get(fields.CONTENT_HASH_KEY, None)
+
+ # IMessage implementation
+
+ def getUID(self):
+ """
+ Retrieve the unique identifier associated with this message
+
+ :return: uid for this message
+ :rtype: int
+ """
+ return self._uid
+
+ def getFlags(self):
+ """
+ Retrieve the flags associated with this message
+
+ :return: The flags, represented as strings
+ :rtype: tuple
+ """
+ if self._uid is None:
+ return []
+
+ flags = []
+ flag_doc = self._fdoc
+ if flag_doc:
+ flags = flag_doc.content.get(self.FLAGS_KEY, None)
+ if flags:
+ flags = map(str, flags)
+ return tuple(flags)
+
+ # setFlags, addFlags, removeFlags are not in the interface spec
+ # but we use them with store command.
+
+ def setFlags(self, flags):
+ """
+ Sets the flags for this message
+
+ Returns a SoledadDocument that needs to be updated by the caller.
+
+ :param flags: the flags to update in the message.
+ :type flags: tuple of str
+
+ :return: a SoledadDocument instance
+ :rtype: SoledadDocument
+ """
+ leap_assert(isinstance(flags, tuple), "flags need to be a tuple")
+ log.msg('setting flags: %s' % (self._uid))
+
+ doc = self._fdoc
+ doc.content[self.FLAGS_KEY] = flags
+ doc.content[self.SEEN_KEY] = self.SEEN_FLAG in flags
+ doc.content[self.RECENT_KEY] = self.RECENT_FLAG in flags
+ self._soledad.put_doc(doc)
+
+ def addFlags(self, flags):
+ """
+ Adds flags to this message.
+
+ Returns a SoledadDocument that needs to be updated by the caller.
+
+ :param flags: the flags to add to the message.
+ :type flags: tuple of str
+
+ :return: a SoledadDocument instance
+ :rtype: SoledadDocument
+ """
+ leap_assert(isinstance(flags, tuple), "flags need to be a tuple")
+ oldflags = self.getFlags()
+ self.setFlags(tuple(set(flags + oldflags)))
+
+ def removeFlags(self, flags):
+ """
+ Remove flags from this message.
+
+ Returns a SoledadDocument that needs to be updated by the caller.
+
+ :param flags: the flags to be removed from the message.
+ :type flags: tuple of str
+
+ :return: a SoledadDocument instance
+ :rtype: SoledadDocument
+ """
+ leap_assert(isinstance(flags, tuple), "flags need to be a tuple")
+ oldflags = self.getFlags()
+ self.setFlags(tuple(set(oldflags) - set(flags)))
+
+ def getInternalDate(self):
+ """
+ Retrieve the date internally associated with this message
+
+ :rtype: C{str}
+ :return: An RFC822-formatted date string.
+ """
+ return str(self._cdoc.content.get(self.DATE_KEY, ''))
+
+ #
+ # IMessagePart
+ #
+
+ # XXX we should implement this interface too for the subparts
+ # so we allow nested parts...
+
+ def getBodyFile(self):
+ """
+ Retrieve a file object containing only the body of this message.
+
+ :return: file-like object opened for reading
+ :rtype: StringIO
+ """
+ fd = StringIO.StringIO()
+
+ cdoc = self._cdoc
+ content = cdoc.content.get(self.RAW_KEY, '')
+ charset = get_email_charset(
+ unicode(cdoc.content.get(self.RAW_KEY, '')))
+ try:
+ content = content.encode(charset)
+ except (UnicodeEncodeError, UnicodeDecodeError) as e:
+ logger.error("Unicode error {0}".format(e))
+ content = content.encode(charset, 'replace')
+
+ raw = self._get_raw_msg()
+ msg = self._get_parsed_msg(raw)
+ body = msg.get_payload()
+ fd.write(body)
+ # XXX SHOULD use a separate BODY FIELD ...
+ fd.seek(0)
+ return fd
+
+ def getSize(self):
+ """
+ Return the total size, in octets, of this message.
+
+ :return: size of the message, in octets
+ :rtype: int
+ """
+ size = self._cdoc.content.get(self.SIZE_KEY, False)
+ if not size:
+ # XXX fallback, should remove when all migrated.
+ size = self.getBodyFile().len
+ return size
+
+ def _get_headers(self):
+ """
+ Return the headers dict stored in this message document.
+ """
+ # XXX get from the headers doc
+ return self._cdoc.content.get(self.HEADERS_KEY, {})
+
+ def getHeaders(self, negate, *names):
+ """
+ Retrieve a group of message headers.
+
+ :param names: The names of the headers to retrieve or omit.
+ :type names: tuple of str
+
+ :param negate: If True, indicates that the headers listed in names
+ should be omitted from the return value, rather
+ than included.
+ :type negate: bool
+
+ :return: A mapping of header field names to header field values
+ :rtype: dict
+ """
+ headers = self._get_headers()
+ names = map(lambda s: s.upper(), names)
+ if negate:
+ cond = lambda key: key.upper() not in names
+ else:
+ cond = lambda key: key.upper() in names
+
+ # unpack and filter original dict by negate-condition
+ filter_by_cond = [
+ map(str, (key, val)) for
+ key, val in headers.items()
+ if cond(key)]
+ return dict(filter_by_cond)
+
+ def isMultipart(self):
+ """
+ Return True if this message is multipart.
+ """
+ if self._cdoc:
+ retval = self._fdoc.content.get(self.MULTIPART_KEY, False)
+ return retval
+
+ def getSubPart(self, part):
+ """
+ Retrieve a MIME submessage
+
+ :type part: C{int}
+ :param part: The number of the part to retrieve, indexed from 0.
+ :raise IndexError: Raised if the specified part does not exist.
+ :raise TypeError: Raised if this message is not multipart.
+ :rtype: Any object implementing C{IMessagePart}.
+ :return: The specified sub-part.
+ """
+ if not self.isMultipart():
+ raise TypeError
+
+ msg = self._get_parsed_msg()
+ # XXX should wrap IMessagePart
+ return msg.get_payload()[part]
+
+ #
+ # accessors
+ #
+
+ def _get_flags_doc(self):
+ """
+ Return the document that keeps the flags for this
+ message.
+ """
+ flag_docs = self._soledad.get_from_index(
+ SoledadBackedAccount.TYPE_MBOX_UID_IDX,
+ fields.TYPE_FLAGS_VAL, self._mbox, str(self._uid))
+ flag_doc = flag_docs[0] if flag_docs else None
+ return flag_doc
+
+ def _get_content_doc(self):
+ """
+ Return the document that keeps the flags for this
+ message.
+ """
+ cont_docs = self._soledad.get_from_index(
+ SoledadBackedAccount.TYPE_HASH_IDX,
+ fields.TYPE_MESSAGE_VAL, self._content_hash, str(self._uid))
+ cont_doc = cont_docs[0] if cont_docs else None
+ return cont_doc
+
+ def _get_raw_msg(self):
+ """
+ Return the raw msg.
+ :rtype: basestring
+ """
+ return self._cdoc.content.get(self.RAW_KEY, '')
+
+ def __getitem__(self, key):
+ """
+ Return the content of the message document.
+
+ :param key: The key
+ :type key: str
+
+ :return: The content value indexed by C{key} or None
+ :rtype: str
+ """
+ return self._cdoc.content.get(key, None)
+
+ def does_exist(self):
+ """
+ Return True if there is actually a message for this
+ UID and mbox.
+ """
+ return bool(self._fdoc)
+
+
+SoledadWriterPayload = namedtuple(
+ 'SoledadWriterPayload', ['mode', 'payload'])
+
+SoledadWriterPayload.CREATE = 1
+SoledadWriterPayload.PUT = 2
+
+
+class SoledadDocWriter(object):
+ """
+ This writer will create docs serially in the local soledad database.
+ """
+
+ implements(IMessageConsumer)
+
+ def __init__(self, soledad):
+ """
+ Initialize the writer.
+
+ :param soledad: the soledad instance
+ :type soledad: Soledad
+ """
+ self._soledad = soledad
+
+ def consume(self, queue):
+ """
+ Creates a new document in soledad db.
+
+ :param queue: queue to get item from, with content of the document
+ to be inserted.
+ :type queue: Queue
+ """
+ empty = queue.empty()
+ while not empty:
+ item = queue.get()
+ if item.mode == SoledadWriterPayload.CREATE:
+ call = self._soledad.create_doc
+ elif item.mode == SoledadWriterPayload.PUT:
+ call = self._soledad.put_doc
+
+ # should handle errors
+ try:
+ call(item.payload)
+ except u1db_errors.RevisionConflict as exc:
+ logger.error("Error: %r" % (exc,))
+ raise exc
+
+ empty = queue.empty()
+
+
+class MessageCollection(WithMsgFields, IndexedDB, MailParser, MBoxParser):
+ """
+ A collection of messages, surprisingly.
+
+ It is tied to a selected mailbox name that is passed to constructor.
+ Implements a filter query over the messages contained in a soledad
+ database.
+ """
+ # XXX this should be able to produce a MessageSet methinks
+
+ EMPTY_MSG = {
+ fields.TYPE_KEY: fields.TYPE_MESSAGE_VAL,
+ fields.UID_KEY: 1,
+ fields.MBOX_KEY: fields.INBOX_VAL,
+
+ fields.SUBJECT_KEY: "",
+ fields.DATE_KEY: "",
+ fields.RAW_KEY: "",
+
+ # XXX should separate headers into another doc
+ fields.HEADERS_KEY: {},
+ }
+
+ EMPTY_FLAGS = {
+ fields.TYPE_KEY: fields.TYPE_FLAGS_VAL,
+ fields.UID_KEY: 1,
+ fields.MBOX_KEY: fields.INBOX_VAL,
+
+ fields.FLAGS_KEY: [],
+ fields.SEEN_KEY: False,
+ fields.RECENT_KEY: True,
+ fields.MULTIPART_KEY: False,
+ }
+
+ # get from SoledadBackedAccount the needed index-related constants
+ INDEXES = SoledadBackedAccount.INDEXES
+ TYPE_IDX = SoledadBackedAccount.TYPE_IDX
+
+ def __init__(self, mbox=None, soledad=None):
+ """
+ Constructor for MessageCollection.
+
+ :param mbox: the name of the mailbox. It is the name
+ with which we filter the query over the
+ messages database
+ :type mbox: str
+
+ :param soledad: Soledad database
+ :type soledad: Soledad instance
+ """
+ MailParser.__init__(self)
+ leap_assert(mbox, "Need a mailbox name to initialize")
+ leap_assert(mbox.strip() != "", "mbox cannot be blank space")
+ leap_assert(isinstance(mbox, (str, unicode)),
+ "mbox needs to be a string")
+ leap_assert(soledad, "Need a soledad instance to initialize")
+
+ # okay, all in order, keep going...
+ self.mbox = self._parse_mailbox_name(mbox)
+ self._soledad = soledad
+ self.initialize_db()
+
+ # I think of someone like nietzsche when reading this
+
+ # this will be the producer that will enqueue the content
+ # to be processed serially by the consumer (the writer). We just
+ # need to `put` the new material on its plate.
+
+ self.soledad_writer = MessageProducer(
+ SoledadDocWriter(soledad),
+ period=0.05)
+
+ def _get_empty_msg(self):
+ """
+ Returns an empty message.
+
+ :return: a dict containing a default empty message
+ :rtype: dict
+ """
+ return copy.deepcopy(self.EMPTY_MSG)
+
+ def _get_empty_flags_doc(self):
+ """
+ Returns an empty doc for storing flags.
+
+ :return:
+ :rtype:
+ """
+ return copy.deepcopy(self.EMPTY_FLAGS)
+
+ @deferred
+ def add_msg(self, raw, subject=None, flags=None, date=None, uid=1):
+ """
+ Creates a new message document.
+
+ :param raw: the raw message
+ :type raw: str
+
+ :param subject: subject of the message.
+ :type subject: str
+
+ :param flags: flags
+ :type flags: list
+
+ :param date: the received date for the message
+ :type date: str
+
+ :param uid: the message uid for this mailbox
+ :type uid: int
+ """
+ # TODO: split in smaller methods
+ logger.debug('adding message')
+ if flags is None:
+ flags = tuple()
+ leap_assert_type(flags, tuple)
+
+ content_doc = self._get_empty_msg()
+ flags_doc = self._get_empty_flags_doc()
+
+ content_doc[self.MBOX_KEY] = self.mbox
+ flags_doc[self.MBOX_KEY] = self.mbox
+ # ...should get a sanity check here.
+ content_doc[self.UID_KEY] = uid
+ flags_doc[self.UID_KEY] = uid
+
+ if flags:
+ flags_doc[self.FLAGS_KEY] = map(self._stringify, flags)
+ flags_doc[self.SEEN_KEY] = self.SEEN_FLAG in flags
+
+ msg = self._get_parsed_msg(raw)
+ headers = dict(msg)
+
+ logger.debug("adding. is multipart:%s" % msg.is_multipart())
+ flags_doc[self.MULTIPART_KEY] = msg.is_multipart()
+ # XXX get lower case for keys?
+ # XXX get headers doc
+ content_doc[self.HEADERS_KEY] = headers
+ # set subject based on message headers and eventually replace by
+ # subject given as param
+ if self.SUBJECT_FIELD in headers:
+ content_doc[self.SUBJECT_KEY] = headers[self.SUBJECT_FIELD]
+ if subject is not None:
+ content_doc[self.SUBJECT_KEY] = subject
+
+ # XXX could separate body into its own doc
+ # but should also separate multiparts
+ # that should be wrapped in MessagePart
+ content_doc[self.RAW_KEY] = self._stringify(raw)
+ content_doc[self.SIZE_KEY] = len(raw)
+
+ if not date and self.DATE_FIELD in headers:
+ content_doc[self.DATE_KEY] = headers[self.DATE_FIELD]
+ else:
+ content_doc[self.DATE_KEY] = date
+
+ logger.debug('enqueuing message for write')
+
+ ptuple = SoledadWriterPayload
+ self.soledad_writer.put(ptuple(
+ mode=ptuple.CREATE, payload=content_doc))
+ self.soledad_writer.put(ptuple(
+ mode=ptuple.CREATE, payload=flags_doc))
+
+ def remove(self, msg):
+ """
+ Removes a message.
+
+ :param msg: a Leapmessage instance
+ :type msg: LeapMessage
+ """
+ # XXX remove
+ #self._soledad.delete_doc(msg)
+ msg.remove()
+
+ # getters
+
+ def get_msg_by_uid(self, uid):
+ """
+ Retrieves a LeapMessage by UID.
+
+ :param uid: the message uid to query by
+ :type uid: int
+
+ :return: A LeapMessage instance matching the query,
+ or None if not found.
+ :rtype: LeapMessage
+ """
+ msg = LeapMessage(self._soledad, uid, self.mbox)
+ if not msg.does_exist():
+ return None
+ return msg
+
+ def get_all_docs(self, _type=fields.TYPE_FLAGS_VAL):
+ """
+ Get all documents for the selected mailbox of the
+ passed type. By default, it returns the flag docs.
+
+ If you want acess to the content, use __iter__ instead
+
+ :return: a list of u1db documents
+ :rtype: list of SoledadDocument
+ """
+ if _type not in fields.__dict__.values():
+ raise TypeError("Wrong type passed to get_all")
+
+ if sameProxiedObjects(self._soledad, None):
+ logger.warning('Tried to get messages but soledad is None!')
+ return []
+
+ all_docs = [doc for doc in self._soledad.get_from_index(
+ SoledadBackedAccount.TYPE_MBOX_IDX,
+ _type, self.mbox)]
+
+ # inneficient, but first let's grok it and then
+ # let's worry about efficiency.
+ # XXX FIXINDEX -- should implement order by in soledad
+ return sorted(all_docs, key=lambda item: item.content['uid'])
+
+ def all_msg_iter(self):
+ """
+ Return an iterator trhough the UIDs of all messages, sorted in
+ ascending order.
+ """
+ all_uids = (doc.content[self.UID_KEY] for doc in
+ self._soledad.get_from_index(
+ SoledadBackedAccount.TYPE_MBOX_IDX,
+ self.TYPE_FLAGS_VAL, self.mbox))
+ return (u for u in sorted(all_uids))
+
+ def count(self):
+ """
+ Return the count of messages for this mailbox.
+
+ :rtype: int
+ """
+ count = self._soledad.get_count_from_index(
+ SoledadBackedAccount.TYPE_MBOX_IDX,
+ fields.TYPE_FLAGS_VAL, self.mbox)
+ return count
+
+ # unseen messages
+
+ def unseen_iter(self):
+ """
+ Get an iterator for the message UIDs with no `seen` flag
+ for this mailbox.
+
+ :return: iterator through unseen message doc UIDs
+ :rtype: iterable
+ """
+ return (doc.content[self.UID_KEY] for doc in
+ self._soledad.get_from_index(
+ SoledadBackedAccount.TYPE_MBOX_SEEN_IDX,
+ self.TYPE_FLAGS_VAL, self.mbox, '0'))
+
+ def count_unseen(self):
+ """
+ Count all messages with the `Unseen` flag.
+
+ :returns: count
+ :rtype: int
+ """
+ count = self._soledad.get_count_from_index(
+ SoledadBackedAccount.TYPE_MBOX_SEEN_IDX,
+ self.TYPE_FLAGS_VAL, self.mbox, '0')
+ return count
+
+ def get_unseen(self):
+ """
+ Get all messages with the `Unseen` flag
+
+ :returns: a list of LeapMessages
+ :rtype: list
+ """
+ return [LeapMessage(self._soledad, docid, self.mbox)
+ for docid in self.unseen_iter()]
+
+ # recent messages
+
+ def recent_iter(self):
+ """
+ Get an iterator for the message docs with `recent` flag.
+
+ :return: iterator through recent message docs
+ :rtype: iterable
+ """
+ return (doc.content[self.UID_KEY] for doc in
+ self._soledad.get_from_index(
+ SoledadBackedAccount.TYPE_MBOX_RECT_IDX,
+ self.TYPE_FLAGS_VAL, self.mbox, '1'))
+
+ def get_recent(self):
+ """
+ Get all messages with the `Recent` flag.
+
+ :returns: a list of LeapMessages
+ :rtype: list
+ """
+ return [LeapMessage(self._soledad, docid, self.mbox)
+ for docid in self.recent_iter()]
+
+ def count_recent(self):
+ """
+ Count all messages with the `Recent` flag.
+
+ :returns: count
+ :rtype: int
+ """
+ count = self._soledad.get_count_from_index(
+ SoledadBackedAccount.TYPE_MBOX_RECT_IDX,
+ self.TYPE_FLAGS_VAL, self.mbox, '1')
+ return count
+
+ def __len__(self):
+ """
+ Returns the number of messages on this mailbox.
+
+ :rtype: int
+ """
+ return self.count()
+
+ def __iter__(self):
+ """
+ Returns an iterator over all messages.
+
+ :returns: iterator of dicts with content for all messages.
+ :rtype: iterable
+ """
+ return (LeapMessage(self._soledad, docuid, self.mbox)
+ for docuid in self.all_msg_iter())
+
+ def __repr__(self):
+ """
+ Representation string for this object.
+ """
+ return u"<MessageCollection: mbox '%s' (%s)>" % (
+ self.mbox, self.count())
+
+ # XXX should implement __eq__ also !!! --- use a hash
+ # of content for that, will be used for dedup.
diff --git a/src/leap/mail/imap/parser.py b/src/leap/mail/imap/parser.py
new file mode 100644
index 0000000..1ae19c0
--- /dev/null
+++ b/src/leap/mail/imap/parser.py
@@ -0,0 +1,93 @@
+# -*- coding: utf-8 -*-
+# parser.py
+# Copyright (C) 2013 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
+# 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 General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+"""
+Mail parser mixins.
+"""
+import cStringIO
+import StringIO
+import re
+
+from email.parser import Parser
+
+
+class MailParser(object):
+ """
+ Mixin with utility methods to parse raw messages.
+ """
+ def __init__(self):
+ """
+ Initializes the mail parser.
+ """
+ self._parser = Parser()
+
+ def _get_parsed_msg(self, raw):
+ """
+ Return a parsed Message.
+
+ :param raw: the raw string to parse
+ :type raw: basestring, or StringIO object
+ """
+ msg = self._get_parser_fun(raw)(raw, True)
+ return msg
+
+ def _get_parser_fun(self, o):
+ """
+ Retunn the proper parser function for an object.
+
+ :param o: object
+ :type o: object
+ :param parser: an instance of email.parser.Parser
+ :type parser: email.parser.Parser
+ """
+ if isinstance(o, (cStringIO.OutputType, StringIO.StringIO)):
+ return self._parser.parse
+ if isinstance(o, basestring):
+ return self._parser.parsestr
+ # fallback
+ return self._parser.parsestr
+
+ def _stringify(self, o):
+ """
+ Return a string object.
+
+ :param o: object
+ :type o: object
+ """
+ if isinstance(o, (cStringIO.OutputType, StringIO.StringIO)):
+ return o.getvalue()
+ else:
+ return o
+
+
+class MBoxParser(object):
+ """
+ Utility function to parse mailbox names.
+ """
+ INBOX_NAME = "INBOX"
+ INBOX_RE = re.compile(INBOX_NAME, re.IGNORECASE)
+
+ def _parse_mailbox_name(self, name):
+ """
+ :param name: the name of the mailbox
+ :type name: unicode
+
+ :rtype: unicode
+ """
+ if self.INBOX_RE.match(name):
+ # ensure inital INBOX is uppercase
+ return self.INBOX_NAME + name[len(self.INBOX_NAME):]
+ return name
diff --git a/src/leap/mail/imap/server.py b/src/leap/mail/imap/server.py
deleted file mode 100644
index 57587a5..0000000
--- a/src/leap/mail/imap/server.py
+++ /dev/null
@@ -1,1897 +0,0 @@
-# -*- coding: utf-8 -*-
-# server.py
-# Copyright (C) 2013 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
-# 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 General Public License for more details.
-#
-# 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 IMAP Server.
-"""
-import copy
-import logging
-import StringIO
-import cStringIO
-import time
-import re
-
-from collections import defaultdict, namedtuple
-from email.parser import Parser
-
-from zope.interface import implements
-from zope.proxy import sameProxiedObjects
-
-from twisted.mail import imap4
-from twisted.internet import defer
-from twisted.python import log
-
-from u1db import errors as u1db_errors
-
-from leap.common import events as leap_events
-from leap.common.events.events_pb2 import IMAP_UNREAD_MAIL
-from leap.common.check import leap_assert, leap_assert_type
-from leap.common.mail import get_email_charset
-from leap.mail.messageflow import IMessageConsumer, MessageProducer
-from leap.mail.decorators import deferred
-from leap.soledad.client import Soledad
-
-logger = logging.getLogger(__name__)
-
-
-class MissingIndexError(Exception):
- """
- Raises when tried to access a non existent index document.
- """
-
-
-class BadIndexError(Exception):
- """
- Raises when index is malformed or has the wrong cardinality.
- """
-
-
-class WithMsgFields(object):
- """
- Container class for class-attributes to be shared by
- several message-related classes.
- """
- # Internal representation of Message
- DATE_KEY = "date"
- HEADERS_KEY = "headers"
- FLAGS_KEY = "flags"
- MBOX_KEY = "mbox"
- RAW_KEY = "raw"
- SUBJECT_KEY = "subject"
- UID_KEY = "uid"
- MULTIPART_KEY = "multi"
- SIZE_KEY = "size"
-
- # Mailbox specific keys
- CLOSED_KEY = "closed"
- CREATED_KEY = "created"
- SUBSCRIBED_KEY = "subscribed"
- RW_KEY = "rw"
- LAST_UID_KEY = "lastuid"
-
- # Document Type, for indexing
- TYPE_KEY = "type"
- TYPE_MESSAGE_VAL = "msg"
- TYPE_MBOX_VAL = "mbox"
- TYPE_FLAGS_VAL = "flags"
- # should add also a headers val
-
- INBOX_VAL = "inbox"
-
- # Flags for SoledadDocument for indexing.
- SEEN_KEY = "seen"
- RECENT_KEY = "recent"
-
- # Flags in Mailbox and Message
- SEEN_FLAG = "\\Seen"
- RECENT_FLAG = "\\Recent"
- ANSWERED_FLAG = "\\Answered"
- FLAGGED_FLAG = "\\Flagged" # yo dawg
- DELETED_FLAG = "\\Deleted"
- DRAFT_FLAG = "\\Draft"
- NOSELECT_FLAG = "\\Noselect"
- LIST_FLAG = "List" # is this OK? (no \. ie, no system flag)
-
- # Fields in mail object
- SUBJECT_FIELD = "Subject"
- DATE_FIELD = "Date"
-
-fields = WithMsgFields # alias for convenience
-
-
-class IndexedDB(object):
- """
- Methods dealing with the index.
-
- This is a MixIn that needs access to the soledad instance,
- and also assumes that a INDEXES attribute is accessible to the instance.
-
- INDEXES must be a dictionary of type:
- {'index-name': ['field1', 'field2']}
- """
- # TODO we might want to move this to soledad itself, check
-
- def initialize_db(self):
- """
- Initialize the database.
- """
- leap_assert(self._soledad,
- "Need a soledad attribute accesible in the instance")
- leap_assert_type(self.INDEXES, dict)
-
- # Ask the database for currently existing indexes.
- if not self._soledad:
- logger.debug("NO SOLEDAD ON IMAP INITIALIZATION")
- return
- db_indexes = dict()
- if self._soledad is not None:
- db_indexes = dict(self._soledad.list_indexes())
- for name, expression in SoledadBackedAccount.INDEXES.items():
- if name not in db_indexes:
- # The index does not yet exist.
- self._soledad.create_index(name, *expression)
- continue
-
- if expression == db_indexes[name]:
- # The index exists and is up to date.
- continue
- # The index exists but the definition is not what expected, so we
- # delete it and add the proper index expression.
- self._soledad.delete_index(name)
- self._soledad.create_index(name, *expression)
-
-
-class MailParser(object):
- """
- Mixin with utility methods to parse raw messages.
- """
- def __init__(self):
- """
- Initializes the mail parser.
- """
- self._parser = Parser()
-
- def _get_parsed_msg(self, raw):
- """
- Return a parsed Message.
-
- :param raw: the raw string to parse
- :type raw: basestring, or StringIO object
- """
- msg = self._get_parser_fun(raw)(raw, True)
- return msg
-
- def _get_parser_fun(self, o):
- """
- Retunn the proper parser function for an object.
-
- :param o: object
- :type o: object
- :param parser: an instance of email.parser.Parser
- :type parser: email.parser.Parser
- """
- if isinstance(o, (cStringIO.OutputType, StringIO.StringIO)):
- return self._parser.parse
- if isinstance(o, basestring):
- return self._parser.parsestr
- # fallback
- return self._parser.parsestr
-
- def _stringify(self, o):
- """
- Return a string object.
-
- :param o: object
- :type o: object
- """
- if isinstance(o, (cStringIO.OutputType, StringIO.StringIO)):
- return o.getvalue()
- else:
- return o
-
-
-class MBoxParser(object):
- """
- Utility function to parse mailbox names.
- """
- INBOX_NAME = "INBOX"
- INBOX_RE = re.compile(INBOX_NAME, re.IGNORECASE)
-
- def _parse_mailbox_name(self, name):
- """
- :param name: the name of the mailbox
- :type name: unicode
-
- :rtype: unicode
- """
- if self.INBOX_RE.match(name):
- # ensure inital INBOX is uppercase
- return self.INBOX_NAME + name[len(self.INBOX_NAME):]
- return name
-
-
-#######################################
-# Soledad Account
-#######################################
-
-
-class SoledadBackedAccount(WithMsgFields, IndexedDB, MBoxParser):
- """
- An implementation of IAccount and INamespacePresenteer
- that is backed by Soledad Encrypted Documents.
- """
-
- implements(imap4.IAccount, imap4.INamespacePresenter)
-
- _soledad = None
- selected = None
-
- TYPE_IDX = 'by-type'
- TYPE_MBOX_IDX = 'by-type-and-mbox'
- TYPE_MBOX_UID_IDX = 'by-type-and-mbox-and-uid'
- TYPE_SUBS_IDX = 'by-type-and-subscribed'
- TYPE_MBOX_SEEN_IDX = 'by-type-and-mbox-and-seen'
- TYPE_MBOX_RECT_IDX = 'by-type-and-mbox-and-recent'
- # Tomas created the `recent and seen index`, but the semantic is not too
- # correct since the recent flag is volatile.
- TYPE_MBOX_RECT_SEEN_IDX = 'by-type-and-mbox-and-recent-and-seen'
-
- KTYPE = WithMsgFields.TYPE_KEY
- MBOX_VAL = WithMsgFields.TYPE_MBOX_VAL
-
- INDEXES = {
- # generic
- TYPE_IDX: [KTYPE],
- TYPE_MBOX_IDX: [KTYPE, MBOX_VAL],
- TYPE_MBOX_UID_IDX: [KTYPE, MBOX_VAL, WithMsgFields.UID_KEY],
-
- # mailboxes
- TYPE_SUBS_IDX: [KTYPE, 'bool(subscribed)'],
-
- # messages
- TYPE_MBOX_SEEN_IDX: [KTYPE, MBOX_VAL, 'bool(seen)'],
- TYPE_MBOX_RECT_IDX: [KTYPE, MBOX_VAL, 'bool(recent)'],
- TYPE_MBOX_RECT_SEEN_IDX: [KTYPE, MBOX_VAL,
- 'bool(recent)', 'bool(seen)'],
- }
-
- MBOX_KEY = MBOX_VAL
-
- EMPTY_MBOX = {
- WithMsgFields.TYPE_KEY: MBOX_KEY,
- WithMsgFields.TYPE_MBOX_VAL: MBoxParser.INBOX_NAME,
- WithMsgFields.SUBJECT_KEY: "",
- WithMsgFields.FLAGS_KEY: [],
- WithMsgFields.CLOSED_KEY: False,
- WithMsgFields.SUBSCRIBED_KEY: False,
- WithMsgFields.RW_KEY: 1,
- WithMsgFields.LAST_UID_KEY: 0
- }
-
- def __init__(self, account_name, soledad=None):
- """
- Creates a SoledadAccountIndex that keeps track of the mailboxes
- and subscriptions handled by this account.
-
- :param acct_name: The name of the account (user id).
- :type acct_name: str
-
- :param soledad: a Soledad instance.
- :param soledad: Soledad
- """
- leap_assert(soledad, "Need a soledad instance to initialize")
- leap_assert_type(soledad, Soledad)
-
- # XXX SHOULD assert too that the name matches the user/uuid with which
- # soledad has been initialized.
-
- self._account_name = self._parse_mailbox_name(account_name)
- self._soledad = soledad
-
- self.initialize_db()
-
- # every user should have the right to an inbox folder
- # at least, so let's make one!
-
- if not self.mailboxes:
- self.addMailbox(self.INBOX_NAME)
-
- def _get_empty_mailbox(self):
- """
- Returns an empty mailbox.
-
- :rtype: dict
- """
- return copy.deepcopy(self.EMPTY_MBOX)
-
- 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
- """
- doc = self._soledad.get_from_index(
- self.TYPE_MBOX_IDX, self.MBOX_KEY,
- self._parse_mailbox_name(name))
- return doc[0] if doc else None
-
- @property
- def mailboxes(self):
- """
- A list of the current mailboxes for this account.
- """
- return [doc.content[self.MBOX_KEY]
- for doc in self._soledad.get_from_index(
- self.TYPE_IDX, self.MBOX_KEY)]
-
- @property
- def subscriptions(self):
- """
- A list of the current subscriptions for this account.
- """
- return [doc.content[self.MBOX_KEY]
- for doc in self._soledad.get_from_index(
- self.TYPE_SUBS_IDX, self.MBOX_KEY, '1')]
-
- def getMailbox(self, name):
- """
- Returns a Mailbox with that name, without selecting it.
-
- :param name: name of the mailbox
- :type name: str
-
- :returns: a a SoledadMailbox instance
- :rtype: SoledadMailbox
- """
- name = self._parse_mailbox_name(name)
-
- if name not in self.mailboxes:
- raise imap4.MailboxException("No such mailbox")
-
- return SoledadMailbox(name, soledad=self._soledad)
-
- ##
- ## IAccount
- ##
-
- def addMailbox(self, name, creation_ts=None):
- """
- Add a mailbox to the account.
-
- :param name: the name of the mailbox
- :type name: str
-
- :param creation_ts: an optional creation timestamp to be used as
- mailbox id. A timestamp will be used if no
- one is provided.
- :type creation_ts: int
-
- :returns: True if successful
- :rtype: bool
- """
- name = self._parse_mailbox_name(name)
-
- if name in self.mailboxes:
- raise imap4.MailboxCollision, name
-
- if not creation_ts:
- # by default, we pass an int value
- # taken from the current time
- # we make sure to take enough decimals to get a unique
- # mailbox-uidvalidity.
- creation_ts = int(time.time() * 10E2)
-
- mbox = self._get_empty_mailbox()
- mbox[self.MBOX_KEY] = name
- mbox[self.CREATED_KEY] = creation_ts
-
- doc = self._soledad.create_doc(mbox)
- return bool(doc)
-
- def create(self, pathspec):
- """
- Create a new mailbox from the given hierarchical name.
-
- :param pathspec: The full hierarchical name of a new mailbox to create.
- If any of the inferior hierarchical names to this one
- do not exist, they are created as well.
- :type pathspec: str
-
- :return: A true value if the creation succeeds.
- :rtype: bool
-
- :raise MailboxException: Raised if this mailbox cannot be added.
- """
- # TODO raise MailboxException
- paths = filter(
- None,
- self._parse_mailbox_name(pathspec).split('/'))
- for accum in range(1, len(paths)):
- try:
- self.addMailbox('/'.join(paths[:accum]))
- except imap4.MailboxCollision:
- pass
- try:
- self.addMailbox('/'.join(paths))
- except imap4.MailboxCollision:
- if not pathspec.endswith('/'):
- return False
- return True
-
- def select(self, name, readwrite=1):
- """
- Selects a mailbox.
-
- :param name: the mailbox to select
- :type name: str
-
- :param readwrite: 1 for readwrite permissions.
- :type readwrite: int
-
- :rtype: bool
- """
- name = self._parse_mailbox_name(name)
-
- if name not in self.mailboxes:
- return None
-
- self.selected = name
-
- return SoledadMailbox(
- name, rw=readwrite,
- soledad=self._soledad)
-
- def delete(self, name, force=False):
- """
- Deletes a mailbox.
-
- Right now it does not purge the messages, but just removes the mailbox
- name from the mailboxes list!!!
-
- :param name: the mailbox to be deleted
- :type name: str
-
- :param force: if True, it will not check for noselect flag or inferior
- names. use with care.
- :type force: bool
- """
- name = self._parse_mailbox_name(name)
-
- if not name in self.mailboxes:
- raise imap4.MailboxException("No such mailbox")
-
- mbox = self.getMailbox(name)
-
- if force is False:
- # See if this box is flagged \Noselect
- # XXX use mbox.flags instead?
- if self.NOSELECT_FLAG in mbox.getFlags():
- # Check for hierarchically inferior mailboxes with this one
- # as part of their root.
- for others in self.mailboxes:
- if others != name and others.startswith(name):
- raise imap4.MailboxException, (
- "Hierarchically inferior mailboxes "
- "exist and \\Noselect is set")
- mbox.destroy()
-
- # XXX FIXME --- not honoring the inferior names...
-
- # if there are no hierarchically inferior names, we will
- # delete it from our ken.
- #if self._inferiorNames(name) > 1:
- # ??! -- can this be rite?
- #self._index.removeMailbox(name)
-
- def rename(self, oldname, newname):
- """
- Renames a mailbox.
-
- :param oldname: old name of the mailbox
- :type oldname: str
-
- :param newname: new name of the mailbox
- :type newname: str
- """
- oldname = self._parse_mailbox_name(oldname)
- newname = self._parse_mailbox_name(newname)
-
- if oldname not in self.mailboxes:
- raise imap4.NoSuchMailbox, oldname
-
- inferiors = self._inferiorNames(oldname)
- inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
-
- for (old, new) in inferiors:
- if new in self.mailboxes:
- raise imap4.MailboxCollision, new
-
- for (old, new) in inferiors:
- mbox = self._get_mailbox_by_name(old)
- mbox.content[self.MBOX_KEY] = new
- self._soledad.put_doc(mbox)
-
- # XXX ---- FIXME!!!! ------------------------------------
- # until here we just renamed the index...
- # We have to rename also the occurrence of this
- # mailbox on ALL the messages that are contained in it!!!
- # ... we maybe could use a reference to the doc_id
- # in each msg, instead of the "mbox" field in msgs
- # -------------------------------------------------------
-
- def _inferiorNames(self, name):
- """
- Return hierarchically inferior mailboxes.
-
- :param name: name of the mailbox
- :rtype: list
- """
- # XXX use wildcard query instead
- inferiors = []
- for infname in self.mailboxes:
- if infname.startswith(name):
- inferiors.append(infname)
- return inferiors
-
- def isSubscribed(self, name):
- """
- Returns True if user is subscribed to this mailbox.
-
- :param name: the mailbox to be checked.
- :type name: str
-
- :rtype: bool
- """
- mbox = self._get_mailbox_by_name(name)
- return mbox.content.get('subscribed', False)
-
- 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
- """
- # maybe we should store subscriptions in another
- # document...
- if not name in self.mailboxes:
- self.addMailbox(name)
- mbox = self._get_mailbox_by_name(name)
-
- if mbox:
- mbox.content[self.SUBSCRIBED_KEY] = value
- self._soledad.put_doc(mbox)
-
- def subscribe(self, name):
- """
- Subscribe to this mailbox
-
- :param name: name of the mailbox
- :type name: str
- """
- name = self._parse_mailbox_name(name)
- if name not in self.subscriptions:
- self._set_subscription(name, True)
-
- def unsubscribe(self, name):
- """
- Unsubscribe from this mailbox
-
- :param name: name of the mailbox
- :type name: str
- """
- name = self._parse_mailbox_name(name)
- if name not in self.subscriptions:
- raise imap4.MailboxException, "Not currently subscribed to " + name
- self._set_subscription(name, False)
-
- def listMailboxes(self, ref, wildcard):
- """
- List the mailboxes.
-
- from rfc 3501:
- returns a subset of names from the complete set
- of all names available to the client. Zero or more untagged LIST
- replies are returned, containing the name attributes, hierarchy
- delimiter, and name.
-
- :param ref: reference name
- :type ref: str
-
- :param wildcard: mailbox name with possible wildcards
- :type wildcard: str
- """
- # XXX use wildcard in index query
- ref = self._inferiorNames(
- self._parse_mailbox_name(ref))
- wildcard = imap4.wildcardToRegexp(wildcard, '/')
- return [(i, self.getMailbox(i)) for i in ref if wildcard.match(i)]
-
- ##
- ## INamespacePresenter
- ##
-
- def getPersonalNamespaces(self):
- return [["", "/"]]
-
- def getSharedNamespaces(self):
- return None
-
- 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.
- """
- return "<SoledadBackedAccount (%s)>" % self._account_name
-
-#######################################
-# LeapMessage, MessageCollection
-# and Mailbox
-#######################################
-
-
-class LeapMessage(fields, MailParser, MBoxParser):
-
- implements(imap4.IMessage)
-
- def __init__(self, soledad, uid, mbox):
- """
- Initializes a LeapMessage.
-
- :param soledad: a Soledad instance
- :type soledad: Soledad
- :param uid: the UID for the message.
- :type uid: int or basestring
- :param mbox: the mbox this message belongs to
- :type mbox: basestring
- """
- MailParser.__init__(self)
- self._soledad = soledad
- self._uid = int(uid)
- self._mbox = self._parse_mailbox_name(mbox)
-
- self.__cdoc = None
-
- @property
- def _fdoc(self):
- """
- An accessor to the flags docuemnt
- """
- return self._get_flags_doc()
-
- @property
- def _cdoc(self):
- """
- An accessor to the content docuemnt
- """
- if not self.__cdoc:
- self.__cdoc = self._get_content_doc()
- return self.__cdoc
-
- def getUID(self):
- """
- Retrieve the unique identifier associated with this message
-
- :return: uid for this message
- :rtype: int
- """
- return self._uid
-
- def getFlags(self):
- """
- Retrieve the flags associated with this message
-
- :return: The flags, represented as strings
- :rtype: tuple
- """
- if self._uid is None:
- return []
-
- flags = []
- flag_doc = self._fdoc
- if flag_doc:
- flags = flag_doc.content.get(self.FLAGS_KEY, None)
- if flags:
- flags = map(str, flags)
- return tuple(flags)
-
- # setFlags, addFlags, removeFlags are not in the interface spec
- # but we use them with store command.
-
- def setFlags(self, flags):
- """
- Sets the flags for this message
-
- Returns a SoledadDocument that needs to be updated by the caller.
-
- :param flags: the flags to update in the message.
- :type flags: tuple of str
-
- :return: a SoledadDocument instance
- :rtype: SoledadDocument
- """
- leap_assert(isinstance(flags, tuple), "flags need to be a tuple")
- log.msg('setting flags: %s' % (self._uid))
-
- doc = self._fdoc
- doc.content[self.FLAGS_KEY] = flags
- doc.content[self.SEEN_KEY] = self.SEEN_FLAG in flags
- doc.content[self.RECENT_KEY] = self.RECENT_FLAG in flags
- self._soledad.put_doc(doc)
-
- def addFlags(self, flags):
- """
- Adds flags to this message.
-
- Returns a SoledadDocument that needs to be updated by the caller.
-
- :param flags: the flags to add to the message.
- :type flags: tuple of str
-
- :return: a SoledadDocument instance
- :rtype: SoledadDocument
- """
- leap_assert(isinstance(flags, tuple), "flags need to be a tuple")
- oldflags = self.getFlags()
- self.setFlags(tuple(set(flags + oldflags)))
-
- def removeFlags(self, flags):
- """
- Remove flags from this message.
-
- Returns a SoledadDocument that needs to be updated by the caller.
-
- :param flags: the flags to be removed from the message.
- :type flags: tuple of str
-
- :return: a SoledadDocument instance
- :rtype: SoledadDocument
- """
- leap_assert(isinstance(flags, tuple), "flags need to be a tuple")
- oldflags = self.getFlags()
- self.setFlags(tuple(set(oldflags) - set(flags)))
-
- def getInternalDate(self):
- """
- Retrieve the date internally associated with this message
-
- :rtype: C{str}
- :return: An RFC822-formatted date string.
- """
- return str(self._cdoc.content.get(self.DATE_KEY, ''))
-
- #
- # IMessagePart
- #
-
- # XXX we should implement this interface too for the subparts
- # so we allow nested parts...
-
- def getBodyFile(self):
- """
- Retrieve a file object containing only the body of this message.
-
- :return: file-like object opened for reading
- :rtype: StringIO
- """
- fd = StringIO.StringIO()
-
- cdoc = self._cdoc
- content = cdoc.content.get(self.RAW_KEY, '')
- charset = get_email_charset(
- unicode(cdoc.content.get(self.RAW_KEY, '')))
- try:
- content = content.encode(charset)
- except (UnicodeEncodeError, UnicodeDecodeError) as e:
- logger.error("Unicode error {0}".format(e))
- content = content.encode(charset, 'replace')
-
- raw = self._get_raw_msg()
- msg = self._get_parsed_msg(raw)
- body = msg.get_payload()
- fd.write(body)
- # XXX SHOULD use a separate BODY FIELD ...
- fd.seek(0)
- return fd
-
- def getSize(self):
- """
- Return the total size, in octets, of this message.
-
- :return: size of the message, in octets
- :rtype: int
- """
- size = self._cdoc.content.get(self.SIZE_KEY, False)
- if not size:
- # XXX fallback, should remove when all migrated.
- size = self.getBodyFile().len
- return size
-
- def _get_headers(self):
- """
- Return the headers dict stored in this message document.
- """
- # XXX get from the headers doc
- return self._cdoc.content.get(self.HEADERS_KEY, {})
-
- def getHeaders(self, negate, *names):
- """
- Retrieve a group of message headers.
-
- :param names: The names of the headers to retrieve or omit.
- :type names: tuple of str
-
- :param negate: If True, indicates that the headers listed in names
- should be omitted from the return value, rather
- than included.
- :type negate: bool
-
- :return: A mapping of header field names to header field values
- :rtype: dict
- """
- headers = self._get_headers()
- names = map(lambda s: s.upper(), names)
- if negate:
- cond = lambda key: key.upper() not in names
- else:
- cond = lambda key: key.upper() in names
-
- # unpack and filter original dict by negate-condition
- filter_by_cond = [
- map(str, (key, val)) for
- key, val in headers.items()
- if cond(key)]
- return dict(filter_by_cond)
-
- def isMultipart(self):
- """
- Return True if this message is multipart.
- """
- if self._cdoc:
- retval = self._fdoc.content.get(self.MULTIPART_KEY, False)
- return retval
-
- def getSubPart(self, part):
- """
- Retrieve a MIME submessage
-
- :type part: C{int}
- :param part: The number of the part to retrieve, indexed from 0.
- :raise IndexError: Raised if the specified part does not exist.
- :raise TypeError: Raised if this message is not multipart.
- :rtype: Any object implementing C{IMessagePart}.
- :return: The specified sub-part.
- """
- if not self.isMultipart():
- raise TypeError
-
- msg = self._get_parsed_msg()
- # XXX should wrap IMessagePart
- return msg.get_payload()[part]
-
- #
- # accessors
- #
-
- def _get_flags_doc(self):
- """
- Return the document that keeps the flags for this
- message.
- """
- flag_docs = self._soledad.get_from_index(
- SoledadBackedAccount.TYPE_MBOX_UID_IDX,
- fields.TYPE_FLAGS_VAL, self._mbox, str(self._uid))
- flag_doc = flag_docs[0] if flag_docs else None
- return flag_doc
-
- def _get_content_doc(self):
- """
- Return the document that keeps the flags for this
- message.
- """
- cont_docs = self._soledad.get_from_index(
- SoledadBackedAccount.TYPE_MBOX_UID_IDX,
- fields.TYPE_MESSAGE_VAL, self._mbox, str(self._uid))
- cont_doc = cont_docs[0] if cont_docs else None
- return cont_doc
-
- def _get_raw_msg(self):
- """
- Return the raw msg.
- :rtype: basestring
- """
- return self._cdoc.content.get(self.RAW_KEY, '')
-
- def __getitem__(self, key):
- """
- Return the content of the message document.
-
- :param key: The key
- :type key: str
-
- :return: The content value indexed by C{key} or None
- :rtype: str
- """
- return self._cdoc.content.get(key, None)
-
- def does_exist(self):
- """
- Return True if there is actually a message for this
- UID and mbox.
- """
- return bool(self._fdoc)
-
-
-SoledadWriterPayload = namedtuple(
- 'SoledadWriterPayload', ['mode', 'payload'])
-
-SoledadWriterPayload.CREATE = 1
-SoledadWriterPayload.PUT = 2
-
-
-class SoledadDocWriter(object):
- """
- This writer will create docs serially in the local soledad database.
- """
-
- implements(IMessageConsumer)
-
- def __init__(self, soledad):
- """
- Initialize the writer.
-
- :param soledad: the soledad instance
- :type soledad: Soledad
- """
- self._soledad = soledad
-
- def consume(self, queue):
- """
- Creates a new document in soledad db.
-
- :param queue: queue to get item from, with content of the document
- to be inserted.
- :type queue: Queue
- """
- empty = queue.empty()
- while not empty:
- item = queue.get()
- if item.mode == SoledadWriterPayload.CREATE:
- call = self._soledad.create_doc
- elif item.mode == SoledadWriterPayload.PUT:
- call = self._soledad.put_doc
-
- # should handle errors
- try:
- call(item.payload)
- except u1db_errors.RevisionConflict as exc:
- logger.error("Error: %r" % (exc,))
- raise exc
-
- empty = queue.empty()
-
-
-class MessageCollection(WithMsgFields, IndexedDB, MailParser, MBoxParser):
- """
- A collection of messages, surprisingly.
-
- It is tied to a selected mailbox name that is passed to constructor.
- Implements a filter query over the messages contained in a soledad
- database.
- """
- # XXX this should be able to produce a MessageSet methinks
-
- EMPTY_MSG = {
- fields.TYPE_KEY: fields.TYPE_MESSAGE_VAL,
- fields.UID_KEY: 1,
- fields.MBOX_KEY: fields.INBOX_VAL,
-
- fields.SUBJECT_KEY: "",
- fields.DATE_KEY: "",
- fields.RAW_KEY: "",
-
- # XXX should separate headers into another doc
- fields.HEADERS_KEY: {},
- }
-
- EMPTY_FLAGS = {
- fields.TYPE_KEY: fields.TYPE_FLAGS_VAL,
- fields.UID_KEY: 1,
- fields.MBOX_KEY: fields.INBOX_VAL,
-
- fields.FLAGS_KEY: [],
- fields.SEEN_KEY: False,
- fields.RECENT_KEY: True,
- fields.MULTIPART_KEY: False,
- }
-
- # get from SoledadBackedAccount the needed index-related constants
- INDEXES = SoledadBackedAccount.INDEXES
- TYPE_IDX = SoledadBackedAccount.TYPE_IDX
-
- def __init__(self, mbox=None, soledad=None):
- """
- Constructor for MessageCollection.
-
- :param mbox: the name of the mailbox. It is the name
- with which we filter the query over the
- messages database
- :type mbox: str
-
- :param soledad: Soledad database
- :type soledad: Soledad instance
- """
- MailParser.__init__(self)
- leap_assert(mbox, "Need a mailbox name to initialize")
- leap_assert(mbox.strip() != "", "mbox cannot be blank space")
- leap_assert(isinstance(mbox, (str, unicode)),
- "mbox needs to be a string")
- leap_assert(soledad, "Need a soledad instance to initialize")
-
- # okay, all in order, keep going...
- self.mbox = self._parse_mailbox_name(mbox)
- self._soledad = soledad
- self.initialize_db()
-
- # I think of someone like nietzsche when reading this
-
- # this will be the producer that will enqueue the content
- # to be processed serially by the consumer (the writer). We just
- # need to `put` the new material on its plate.
-
- self.soledad_writer = MessageProducer(
- SoledadDocWriter(soledad),
- period=0.05)
-
- def _get_empty_msg(self):
- """
- Returns an empty message.
-
- :return: a dict containing a default empty message
- :rtype: dict
- """
- return copy.deepcopy(self.EMPTY_MSG)
-
- def _get_empty_flags_doc(self):
- """
- Returns an empty doc for storing flags.
-
- :return:
- :rtype:
- """
- return copy.deepcopy(self.EMPTY_FLAGS)
-
- @deferred
- def add_msg(self, raw, subject=None, flags=None, date=None, uid=1):
- """
- Creates a new message document.
-
- :param raw: the raw message
- :type raw: str
-
- :param subject: subject of the message.
- :type subject: str
-
- :param flags: flags
- :type flags: list
-
- :param date: the received date for the message
- :type date: str
-
- :param uid: the message uid for this mailbox
- :type uid: int
- """
- # TODO: split in smaller methods
- logger.debug('adding message')
- if flags is None:
- flags = tuple()
- leap_assert_type(flags, tuple)
-
- content_doc = self._get_empty_msg()
- flags_doc = self._get_empty_flags_doc()
-
- content_doc[self.MBOX_KEY] = self.mbox
- flags_doc[self.MBOX_KEY] = self.mbox
- # ...should get a sanity check here.
- content_doc[self.UID_KEY] = uid
- flags_doc[self.UID_KEY] = uid
-
- if flags:
- flags_doc[self.FLAGS_KEY] = map(self._stringify, flags)
- flags_doc[self.SEEN_KEY] = self.SEEN_FLAG in flags
-
- msg = self._get_parsed_msg(raw)
- headers = dict(msg)
-
- logger.debug("adding. is multipart:%s" % msg.is_multipart())
- flags_doc[self.MULTIPART_KEY] = msg.is_multipart()
- # XXX get lower case for keys?
- # XXX get headers doc
- content_doc[self.HEADERS_KEY] = headers
- # set subject based on message headers and eventually replace by
- # subject given as param
- if self.SUBJECT_FIELD in headers:
- content_doc[self.SUBJECT_KEY] = headers[self.SUBJECT_FIELD]
- if subject is not None:
- content_doc[self.SUBJECT_KEY] = subject
-
- # XXX could separate body into its own doc
- # but should also separate multiparts
- # that should be wrapped in MessagePart
- content_doc[self.RAW_KEY] = self._stringify(raw)
- content_doc[self.SIZE_KEY] = len(raw)
-
- if not date and self.DATE_FIELD in headers:
- content_doc[self.DATE_KEY] = headers[self.DATE_FIELD]
- else:
- content_doc[self.DATE_KEY] = date
-
- logger.debug('enqueuing message for write')
-
- ptuple = SoledadWriterPayload
- self.soledad_writer.put(ptuple(
- mode=ptuple.CREATE, payload=content_doc))
- self.soledad_writer.put(ptuple(
- mode=ptuple.CREATE, payload=flags_doc))
-
- def remove(self, msg):
- """
- Removes a message.
-
- :param msg: a u1db doc containing the message
- :type msg: SoledadDocument
- """
- self._soledad.delete_doc(msg)
-
- # getters
-
- def get_msg_by_uid(self, uid):
- """
- Retrieves a LeapMessage by UID.
-
- :param uid: the message uid to query by
- :type uid: int
-
- :return: A LeapMessage instance matching the query,
- or None if not found.
- :rtype: LeapMessage
- """
- msg = LeapMessage(self._soledad, uid, self.mbox)
- if not msg.does_exist():
- return None
- return msg
-
- def get_all(self):
- """
- Get all message documents for the selected mailbox.
- If you want acess to the content, use __iter__ instead
-
- :return: a list of u1db documents
- :rtype: list of SoledadDocument
- """
- # TODO change to get_all_docs and turn this
- # into returning messages
- if sameProxiedObjects(self._soledad, None):
- logger.warning('Tried to get messages but soledad is None!')
- return []
-
- all_docs = [doc for doc in self._soledad.get_from_index(
- SoledadBackedAccount.TYPE_MBOX_IDX,
- fields.TYPE_FLAGS_VAL, self.mbox)]
-
- # inneficient, but first let's grok it and then
- # let's worry about efficiency.
- # XXX FIXINDEX -- should implement order by in soledad
- return sorted(all_docs, key=lambda item: item.content['uid'])
-
- def count(self):
- """
- Return the count of messages for this mailbox.
-
- :rtype: int
- """
- count = self._soledad.get_count_from_index(
- SoledadBackedAccount.TYPE_MBOX_IDX,
- fields.TYPE_FLAGS_VAL, self.mbox)
- return count
-
- # unseen messages
-
- def unseen_iter(self):
- """
- Get an iterator for the message docs with no `seen` flag
-
- :return: iterator through unseen message doc UIDs
- :rtype: iterable
- """
- return (doc.content[self.UID_KEY] for doc in
- self._soledad.get_from_index(
- SoledadBackedAccount.TYPE_MBOX_SEEN_IDX,
- self.TYPE_FLAGS_VAL, self.mbox, '0'))
-
- def count_unseen(self):
- """
- Count all messages with the `Unseen` flag.
-
- :returns: count
- :rtype: int
- """
- count = self._soledad.get_count_from_index(
- SoledadBackedAccount.TYPE_MBOX_SEEN_IDX,
- self.TYPE_FLAGS_VAL, self.mbox, '0')
- return count
-
- def get_unseen(self):
- """
- Get all messages with the `Unseen` flag
-
- :returns: a list of LeapMessages
- :rtype: list
- """
- return [LeapMessage(self._soledad, docid, self.mbox)
- for docid in self.unseen_iter()]
-
- # recent messages
-
- def recent_iter(self):
- """
- Get an iterator for the message docs with `recent` flag.
-
- :return: iterator through recent message docs
- :rtype: iterable
- """
- return (doc.content[self.UID_KEY] for doc in
- self._soledad.get_from_index(
- SoledadBackedAccount.TYPE_MBOX_RECT_IDX,
- self.TYPE_FLAGS_VAL, self.mbox, '1'))
-
- def get_recent(self):
- """
- Get all messages with the `Recent` flag.
-
- :returns: a list of LeapMessages
- :rtype: list
- """
- return [LeapMessage(self._soledad, docid, self.mbox)
- for docid in self.recent_iter()]
-
- def count_recent(self):
- """
- Count all messages with the `Recent` flag.
-
- :returns: count
- :rtype: int
- """
- count = self._soledad.get_count_from_index(
- SoledadBackedAccount.TYPE_MBOX_RECT_IDX,
- self.TYPE_FLAGS_VAL, self.mbox, '1')
- return count
-
- def __len__(self):
- """
- Returns the number of messages on this mailbox.
-
- :rtype: int
- """
- return self.count()
-
- def __iter__(self):
- """
- Returns an iterator over all messages.
-
- :returns: iterator of dicts with content for all messages.
- :rtype: iterable
- """
- # XXX return LeapMessage instead?! (change accordingly)
- return (m.content for m in self.get_all())
-
- def __repr__(self):
- """
- Representation string for this object.
- """
- return u"<MessageCollection: mbox '%s' (%s)>" % (
- self.mbox, self.count())
-
- # XXX should implement __eq__ also !!! --- use a hash
- # of content for that, will be used for dedup.
-
-
-class SoledadMailbox(WithMsgFields, MBoxParser):
- """
- A Soledad-backed IMAP mailbox.
-
- Implements the high-level method needed for the Mailbox interfaces.
- The low-level database methods are contained in MessageCollection class,
- which we instantiate and make accessible in the `messages` attribute.
- """
- implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox)
- # XXX should finish the implementation of IMailboxListener
-
- messages = None
- _closed = False
-
- INIT_FLAGS = (WithMsgFields.SEEN_FLAG, WithMsgFields.ANSWERED_FLAG,
- WithMsgFields.FLAGGED_FLAG, WithMsgFields.DELETED_FLAG,
- WithMsgFields.DRAFT_FLAG, WithMsgFields.RECENT_FLAG,
- WithMsgFields.LIST_FLAG)
- flags = None
-
- CMD_MSG = "MESSAGES"
- CMD_RECENT = "RECENT"
- CMD_UIDNEXT = "UIDNEXT"
- CMD_UIDVALIDITY = "UIDVALIDITY"
- CMD_UNSEEN = "UNSEEN"
-
- _listeners = defaultdict(set)
-
- def __init__(self, mbox, soledad=None, rw=1):
- """
- SoledadMailbox constructor. Needs to get passed a name, plus a
- Soledad instance.
-
- :param mbox: the mailbox name
- :type mbox: str
-
- :param soledad: a Soledad instance.
- :type soledad: Soledad
-
- :param rw: read-and-write flags
- :type rw: int
- """
- leap_assert(mbox, "Need a mailbox name to initialize")
- leap_assert(soledad, "Need a soledad instance to initialize")
-
- # XXX should move to wrapper
- #leap_assert(isinstance(soledad._db, SQLCipherDatabase),
- #"soledad._db must be an instance of SQLCipherDatabase")
-
- self.mbox = self._parse_mailbox_name(mbox)
- self.rw = rw
-
- self._soledad = soledad
-
- self.messages = MessageCollection(
- mbox=mbox, soledad=self._soledad)
-
- if not self.getFlags():
- self.setFlags(self.INIT_FLAGS)
-
- @property
- def listeners(self):
- """
- Returns listeners for this mbox.
-
- The server itself is a listener to the mailbox.
- so we can notify it (and should!) after changes in flags
- and number of messages.
-
- :rtype: set
- """
- return self._listeners[self.mbox]
-
- def addListener(self, listener):
- """
- Adds a listener to the listeners queue.
- The server adds itself as a listener when there is a SELECT,
- so it can send EXIST commands.
-
- :param listener: listener to add
- :type listener: an object that implements IMailboxListener
- """
- logger.debug('adding mailbox listener: %s' % listener)
- self.listeners.add(listener)
-
- def removeListener(self, listener):
- """
- Removes a listener from the listeners queue.
-
- :param listener: listener to remove
- :type listener: an object that implements IMailboxListener
- """
- self.listeners.remove(listener)
-
- def _get_mbox(self):
- """
- Returns mailbox document.
-
- :return: A SoledadDocument containing this mailbox, or None if
- the query failed.
- :rtype: SoledadDocument or None.
- """
- try:
- query = self._soledad.get_from_index(
- SoledadBackedAccount.TYPE_MBOX_IDX,
- self.TYPE_MBOX_VAL, self.mbox)
- if query:
- return query.pop()
- except Exception as exc:
- logger.error("Unhandled error %r" % exc)
-
- def getFlags(self):
- """
- Returns the flags defined for this mailbox.
-
- :returns: tuple of flags for this mailbox
- :rtype: tuple of str
- """
- mbox = self._get_mbox()
- if not mbox:
- return None
- flags = mbox.content.get(self.FLAGS_KEY, [])
- return map(str, flags)
-
- def setFlags(self, flags):
- """
- Sets flags for this mailbox.
-
- :param flags: a tuple with the flags
- :type flags: tuple of str
- """
- leap_assert(isinstance(flags, tuple),
- "flags expected to be a tuple")
- mbox = self._get_mbox()
- if not mbox:
- return None
- mbox.content[self.FLAGS_KEY] = map(str, flags)
- self._soledad.put_doc(mbox)
-
- # XXX SHOULD BETTER IMPLEMENT ADD_FLAG, REMOVE_FLAG.
-
- def _get_closed(self):
- """
- Return the closed attribute for this mailbox.
-
- :return: True if the mailbox is closed
- :rtype: bool
- """
- mbox = self._get_mbox()
- return mbox.content.get(self.CLOSED_KEY, False)
-
- def _set_closed(self, closed):
- """
- Set the closed attribute for this mailbox.
-
- :param closed: the state to be set
- :type closed: bool
- """
- leap_assert(isinstance(closed, bool), "closed needs to be boolean")
- mbox = self._get_mbox()
- mbox.content[self.CLOSED_KEY] = closed
- self._soledad.put_doc(mbox)
-
- closed = property(
- _get_closed, _set_closed, doc="Closed attribute.")
-
- def _get_last_uid(self):
- """
- Return the last uid for this mailbox.
-
- :return: the last uid for messages in this mailbox
- :rtype: bool
- """
- mbox = self._get_mbox()
- return mbox.content.get(self.LAST_UID_KEY, 1)
-
- def _set_last_uid(self, uid):
- """
- Sets the last uid for this mailbox.
-
- :param uid: the uid to be set
- :type uid: int
- """
- leap_assert(isinstance(uid, int), "uid has to be int")
- mbox = self._get_mbox()
- key = self.LAST_UID_KEY
-
- count = self.getMessageCount()
-
- # XXX safety-catch. If we do get duplicates,
- # we want to avoid further duplication.
-
- if uid >= count:
- value = uid
- else:
- # something is wrong,
- # just set the last uid
- # beyond the max msg count.
- logger.debug("WRONG uid < count. Setting last uid to %s", count)
- value = count
-
- mbox.content[key] = value
- self._soledad.put_doc(mbox)
-
- last_uid = property(
- _get_last_uid, _set_last_uid, doc="Last_UID attribute.")
-
- def getUIDValidity(self):
- """
- Return the unique validity identifier for this mailbox.
-
- :return: unique validity identifier
- :rtype: int
- """
- mbox = self._get_mbox()
- return mbox.content.get(self.CREATED_KEY, 1)
-
- def getUID(self, message):
- """
- Return the UID of a message in the mailbox
-
- .. note:: this implementation does not make much sense RIGHT NOW,
- but in the future will be useful to get absolute UIDs from
- message sequence numbers.
-
- :param message: the message uid
- :type message: int
-
- :rtype: int
- """
- msg = self.messages.get_msg_by_uid(message)
- return msg.getUID()
-
- def getUIDNext(self):
- """
- Return the likely UID for the next message added to this
- mailbox. Currently it returns the higher UID incremented by
- one.
-
- We increment the next uid *each* time this function gets called.
- In this way, there will be gaps if the message with the allocated
- uid cannot be saved. But that is preferable to having race conditions
- if we get to parallel message adding.
-
- :rtype: int
- """
- self.last_uid += 1
- return self.last_uid
-
- def getMessageCount(self):
- """
- Returns the total count of messages in this mailbox.
-
- :rtype: int
- """
- return self.messages.count()
-
- def getUnseenCount(self):
- """
- Returns the number of messages with the 'Unseen' flag.
-
- :return: count of messages flagged `unseen`
- :rtype: int
- """
- return self.messages.count_unseen()
-
- def getRecentCount(self):
- """
- Returns the number of messages with the 'Recent' flag.
-
- :return: count of messages flagged `recent`
- :rtype: int
- """
- return self.messages.count_recent()
-
- def isWriteable(self):
- """
- Get the read/write status of the mailbox.
-
- :return: 1 if mailbox is read-writeable, 0 otherwise.
- :rtype: int
- """
- return self.rw
-
- def getHierarchicalDelimiter(self):
- """
- Returns the character used to delimite hierarchies in mailboxes.
-
- :rtype: str
- """
- return '/'
-
- def requestStatus(self, names):
- """
- Handles a status request by gathering the output of the different
- status commands.
-
- :param names: a list of strings containing the status commands
- :type names: iter
- """
- r = {}
- if self.CMD_MSG in names:
- r[self.CMD_MSG] = self.getMessageCount()
- if self.CMD_RECENT in names:
- r[self.CMD_RECENT] = self.getRecentCount()
- if self.CMD_UIDNEXT in names:
- r[self.CMD_UIDNEXT] = self.last_uid + 1
- if self.CMD_UIDVALIDITY in names:
- r[self.CMD_UIDVALIDITY] = self.getUID()
- if self.CMD_UNSEEN in names:
- r[self.CMD_UNSEEN] = self.getUnseenCount()
- return defer.succeed(r)
-
- def addMessage(self, message, flags, date=None):
- """
- Adds a message to this mailbox.
-
- :param message: the raw message
- :type message: str
-
- :param flags: flag list
- :type flags: list of str
-
- :param date: timestamp
- :type date: str
-
- :return: a deferred that evals to None
- """
- # XXX we should treat the message as an IMessage from here
- leap_assert_type(message, basestring)
- uid_next = self.getUIDNext()
- logger.debug('Adding msg with UID :%s' % uid_next)
- if flags is None:
- flags = tuple()
- else:
- flags = tuple(str(flag) for flag in flags)
-
- d = self._do_add_messages(message, flags, date, uid_next)
- d.addCallback(self._notify_new)
-
- @deferred
- def _do_add_messages(self, message, flags, date, uid_next):
- """
- Calls to the messageCollection add_msg method (deferred to thread).
- Invoked from addMessage.
- """
- self.messages.add_msg(message, flags=flags, date=date,
- uid=uid_next)
-
- def _notify_new(self, *args):
- """
- Notify of new messages to all the listeners.
-
- :param args: ignored.
- """
- exists = self.getMessageCount()
- recent = self.getRecentCount()
- logger.debug("NOTIFY: there are %s messages, %s recent" % (
- exists,
- recent))
-
- logger.debug("listeners: %s", str(self.listeners))
- for l in self.listeners:
- logger.debug('notifying...')
- l.newMessages(exists, recent)
-
- # commands, do not rename methods
-
- def destroy(self):
- """
- Called before this mailbox is permanently deleted.
-
- Should cleanup resources, and set the \\Noselect flag
- on the mailbox.
- """
- self.setFlags((self.NOSELECT_FLAG,))
- self.deleteAllDocs()
-
- # XXX removing the mailbox in situ for now,
- # we should postpone the removal
- self._soledad.delete_doc(self._get_mbox())
-
- def expunge(self):
- """
- Remove all messages flagged \\Deleted
- """
- if not self.isWriteable():
- raise imap4.ReadOnlyMailbox
- delete = []
- deleted = []
-
- for m in self.messages.get_all():
- if self.DELETED_FLAG in m.content[self.FLAGS_KEY]:
- delete.append(m)
- for m in delete:
- deleted.append(m.content)
- self.messages.remove(m)
-
- # XXX should return the UIDs of the deleted messages
- # more generically
- return [x for x in range(len(deleted))]
-
- @deferred
- def fetch(self, messages, uid):
- """
- Retrieve one or more messages in this mailbox.
-
- from rfc 3501: The data items to be fetched can be either a single atom
- or a parenthesized list.
-
- :param messages: IDs of the messages to retrieve information about
- :type messages: MessageSet
-
- :param uid: If true, the IDs are UIDs. They are message sequence IDs
- otherwise.
- :type uid: bool
-
- :rtype: A tuple of two-tuples of message sequence numbers and
- LeapMessage
- """
- result = []
- sequence = True if uid == 0 else False
-
- if not messages.last:
- try:
- iter(messages)
- except TypeError:
- # looks like we cannot iterate
- messages.last = self.last_uid
-
- # for sequence numbers (uid = 0)
- if sequence:
- logger.debug("Getting msg by index: INEFFICIENT call!")
- raise NotImplementedError
-
- else:
- for msg_id in messages:
- msg = self.messages.get_msg_by_uid(msg_id)
- if msg:
- result.append((msg_id, msg))
- else:
- logger.debug("fetch %s, no msg found!!!" % msg_id)
-
- if self.isWriteable():
- self._unset_recent_flag()
- self._signal_unread_to_ui()
-
- # XXX workaround for hangs in thunderbird
- #return tuple(result[:100]) # --- doesn't show all!!
- return tuple(result)
-
- @deferred
- def _unset_recent_flag(self):
- """
- Unsets `Recent` flag from a tuple of messages.
- Called from fetch.
-
- From RFC, about `Recent`:
-
- Message is "recently" arrived in this mailbox. This session
- is the first session to have been notified about this
- message; if the session is read-write, subsequent sessions
- will not see \Recent set for this message. This flag can not
- be altered by the client.
-
- If it is not possible to determine whether or not this
- session is the first session to be notified about a message,
- then that message SHOULD be considered recent.
- """
- log.msg('unsetting recent flags...')
- for msg in self.messages.get_recent():
- msg.removeFlags((fields.RECENT_FLAG,))
- self._signal_unread_to_ui()
-
- @deferred
- def _signal_unread_to_ui(self):
- """
- Sends unread event to ui.
- """
- unseen = self.getUnseenCount()
- leap_events.signal(IMAP_UNREAD_MAIL, str(unseen))
-
- @deferred
- def store(self, messages, flags, mode, uid):
- """
- Sets the flags of one or more messages.
-
- :param messages: The identifiers of the messages to set the flags
- :type messages: A MessageSet object with the list of messages requested
-
- :param flags: The flags to set, unset, or add.
- :type flags: sequence of str
-
- :param mode: If mode is -1, these flags should be removed from the
- specified messages. If mode is 1, these flags should be
- added to the specified messages. If mode is 0, all
- existing flags should be cleared and these flags should be
- added.
- :type mode: -1, 0, or 1
-
- :param uid: If true, the IDs specified in the query are UIDs;
- otherwise they are message sequence IDs.
- :type uid: bool
-
- :return: A dict mapping message sequence numbers to sequences of
- str representing the flags set on the message after this
- operation has been performed.
- :rtype: dict
-
- :raise ReadOnlyMailbox: Raised if this mailbox is not open for
- read-write.
- """
- # XXX implement also sequence (uid = 0)
- # XXX we should prevent cclient from setting Recent flag.
- leap_assert(not isinstance(flags, basestring),
- "flags cannot be a string")
- flags = tuple(flags)
-
- if not self.isWriteable():
- log.msg('read only mailbox!')
- raise imap4.ReadOnlyMailbox
-
- if not messages.last:
- messages.last = self.messages.count()
-
- result = {}
- for msg_id in messages:
- log.msg("MSG ID = %s" % msg_id)
- msg = self.messages.get_msg_by_uid(msg_id)
- if mode == 1:
- msg.addFlags(flags)
- elif mode == -1:
- msg.removeFlags(flags)
- elif mode == 0:
- msg.setFlags(flags)
- result[msg_id] = msg.getFlags()
-
- self._signal_unread_to_ui()
- return result
-
- @deferred
- def close(self):
- """
- Expunge and mark as closed
- """
- self.expunge()
- self.closed = True
-
- # convenience fun
-
- def deleteAllDocs(self):
- """
- Deletes all docs in this mailbox
- """
- docs = self.messages.get_all()
- for doc in docs:
- self.messages._soledad.delete_doc(doc)
-
- def __repr__(self):
- """
- Representation string for this mailbox.
- """
- return u"<SoledadMailbox: mbox '%s' (%s)>" % (
- self.mbox, self.messages.count())
diff --git a/src/leap/mail/imap/service/imap.py b/src/leap/mail/imap/service/imap.py
index 8756ddc..26e14c3 100644
--- a/src/leap/mail/imap/service/imap.py
+++ b/src/leap/mail/imap/service/imap.py
@@ -32,7 +32,7 @@ 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.keymanager import KeyManager
-from leap.mail.imap.server import SoledadBackedAccount
+from leap.mail.imap.account import SoledadBackedAccount
from leap.mail.imap.fetch import LeapIncomingMail
from leap.soledad.client import Soledad