diff options
| author | Kali Kaneko <kali@leap.se> | 2013-05-20 23:10:07 +0900 | 
|---|---|---|
| committer | Kali Kaneko <kali@leap.se> | 2013-05-21 04:06:54 +0900 | 
| commit | 7787329d32f4ae4df2eaec283e000dd730e1ea7f (patch) | |
| tree | 03dae7f9f304f33bd194ae8c1ebeb8d4309e0111 /mail/src | |
| parent | 0a97738430e6c487a4b76bc0b2f726be8d4942fe (diff) | |
cleanup and complete docs
Diffstat (limited to 'mail/src')
| -rw-r--r-- | mail/src/leap/mail/imap/fetch.py | 11 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/server.py | 838 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/service/imap-server.tac | 11 | 
3 files changed, 541 insertions, 319 deletions
| diff --git a/mail/src/leap/mail/imap/fetch.py b/mail/src/leap/mail/imap/fetch.py index adf5787..bcd8901 100644 --- a/mail/src/leap/mail/imap/fetch.py +++ b/mail/src/leap/mail/imap/fetch.py @@ -66,8 +66,7 @@ class LeapIncomingMail(object):              soledad_path,              server_url,              server_pemfile, -            token, -            bootstrap=True) +            token)          self._pkey = self._keymanager.get_all_keys_in_local_db(              private=True).pop() @@ -109,7 +108,7 @@ class LeapIncomingMail(object):          """          Process a successfully decrypted message          """ -        log.msg("processing message!") +        log.msg("processing incoming message!")          msg = json.loads(data)          if not isinstance(msg, dict):              return False @@ -119,10 +118,10 @@ class LeapIncomingMail(object):          rawmsg = msg.get('content', None)          if not rawmsg:              return False -        log.msg("we got raw message") +        #log.msg("we got raw message")          # add to inbox and delete from soledad          inbox.addMessage(rawmsg, ("\\Recent",)) -        log.msg("added msg") +        doc_id = doc.doc_id          self._soledad.delete_doc(doc) -        log.msg("deleted doc") +        log.msg("deleted doc %s from incoming" % doc_id) diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py index c8eac71..30938db 100644 --- a/mail/src/leap/mail/imap/server.py +++ b/mail/src/leap/mail/imap/server.py @@ -36,34 +36,96 @@ from twisted.python import log  #import u1db  from leap.common.check import leap_assert, leap_assert_type +from leap.soledad import Soledad  from leap.soledad.backends.sqlcipher import SQLCipherDatabase  logger = logging.getLogger(__name__)  class MissingIndexError(Exception): -    """raises when tried to access a non existent index document""" +    """ +    Raises when tried to access a non existent index document. +    """  class BadIndexError(Exception): -    """raises when index is malformed or has the wrong cardinality""" +    """ +    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" + +    # Mailbox specific keys +    CLOSED_KEY = "closed" +    CREATED_KEY = "created" +    SUBSCRIBED_KEY = "subscribed" +    RW_KEY = "rw" + +    # Document Type, for indexing +    TYPE_KEY = "type" +    TYPE_MESSAGE_VAL = "msg" +    TYPE_MBOX_VAL = "mbox" + +    INBOX_VAL = "inbox" + +    # Flags for LeapDocument 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"  class IndexedDB(object):      """ -    Methods dealing with the index +    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. -        db_indexes = dict(self._db.list_indexes()) -        for name, expression in self.INDEXES.items(): +        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._db.create_index(name, *expression) +                self._soledad.create_index(name, *expression)                  continue              if expression == db_indexes[name]: @@ -71,8 +133,8 @@ class IndexedDB(object):                  continue              # The index exists but the definition is not what expected, so we              # delete it and add the proper index expression. -            self._db.delete_index(name) -            self._db.create_index(name, *expression) +            self._soledad.delete_index(name) +            self._soledad.create_index(name, *expression)  ####################################### @@ -80,7 +142,7 @@ class IndexedDB(object):  ####################################### -class SoledadBackedAccount(IndexedDB): +class SoledadBackedAccount(WithMsgFields, IndexedDB):      """      An implementation of IAccount and INamespacePresenteer      that is backed by Soledad Encrypted Documents. @@ -89,7 +151,6 @@ class SoledadBackedAccount(IndexedDB):      implements(imap4.IAccount, imap4.INamespacePresenter)      _soledad = None -    _db = None      selected = None      TYPE_IDX = 'by-type' @@ -99,75 +160,84 @@ class SoledadBackedAccount(IndexedDB):      TYPE_MBOX_SEEN_IDX = 'by-type-and-mbox-and-seen'      TYPE_MBOX_RECT_IDX = 'by-type-and-mbox-and-recent' +    KTYPE = WithMsgFields.TYPE_KEY +    MBOX_VAL = WithMsgFields.TYPE_MBOX_VAL +      INDEXES = {          # generic -        TYPE_IDX: ['type'], -        TYPE_MBOX_IDX: ['type', 'mbox'], -        TYPE_MBOX_UID_IDX: ['type', 'mbox', 'uid'], +        TYPE_IDX: [KTYPE], +        TYPE_MBOX_IDX: [KTYPE, MBOX_VAL], +        TYPE_MBOX_UID_IDX: [KTYPE, MBOX_VAL, WithMsgFields.UID_KEY],          # mailboxes -        TYPE_SUBS_IDX: ['type', 'bool(subscribed)'], +        TYPE_SUBS_IDX: [KTYPE, 'bool(subscribed)'],          # messages -        TYPE_MBOX_SEEN_IDX: ['type', 'mbox', 'bool(seen)'], -        TYPE_MBOX_RECT_IDX: ['type', 'mbox', 'bool(recent)'], +        TYPE_MBOX_SEEN_IDX: [KTYPE, MBOX_VAL, 'bool(seen)'], +        TYPE_MBOX_RECT_IDX: [KTYPE, MBOX_VAL, 'bool(recent)'],      } +    INBOX_NAME = "INBOX" +    MBOX_KEY = MBOX_VAL +      EMPTY_MBOX = { -        "type": "mbox", -        "mbox": "INBOX", -        "subject": "", -        "flags": [], -        "closed": False, -        "subscribed": False, -        "rw": 1, +        WithMsgFields.TYPE_KEY: MBOX_KEY, +        WithMsgFields.TYPE_MBOX_VAL: INBOX_NAME, +        WithMsgFields.SUBJECT_KEY: "", +        WithMsgFields.FLAGS_KEY: [], +        WithMsgFields.CLOSED_KEY: False, +        WithMsgFields.SUBSCRIBED_KEY: False, +        WithMsgFields.RW_KEY: 1,      } -    def __init__(self, name, soledad=None): +    def __init__(self, account_name, soledad=None):          """ -        SoledadBackedAccount constructor -        creates a SoledadAccountIndex that keeps track of the -        mailboxes and subscriptions handled by this account. +        Creates a SoledadAccountIndex that keeps track of the mailboxes +        and subscriptions handled by this account. -        @param name: the name of the account (user id) -        @type name: C{str} +        :param acct_name: The name of the account (user id). +        :type acct_name: str -        @param soledad: a Soledad instance -        @param soledad: C{Soledad} +        :param soledad: a Soledad instance. +        :param soledad: Soledad          """          leap_assert(soledad, "Need a soledad instance to initialize") -        # XXX check isinstance ... -        # XXX SHOULD assert too that the name matches the user with which -        # soledad has been intialized. +        leap_assert_type(soledad, Soledad) + +        # XXX SHOULD assert too that the name matches the user/uuid with which +        # soledad has been initialized. -        self.name = name.upper() +        self._account_name = account_name.upper()          self._soledad = soledad -        self._db = soledad._db          self.initialize_db() -        # every user should see an inbox folder -        # at least +        # every user should have the right to an inbox folder +        # at least, so let's make one!          if not self.mailboxes: -            self.addMailbox('inbox') +            self.addMailbox(self.INBOX_NAME)      def _get_empty_mailbox(self):          """          Returns an empty mailbox. -        @rtype: dict +        :rtype: dict          """          return copy.deepcopy(self.EMPTY_MBOX)      def _get_mailbox_by_name(self, name):          """ -        Returns an mbox by name. +        Returns an mbox document by name. + +        :param name: the name of the mailbox +        :type name: str -        @rtype: C{LeapDocument} +        :rtype: LeapDocument          """          name = name.upper() -        doc = self._db.get_from_index(self.TYPE_MBOX_IDX, 'mbox', name) +        doc = self._soledad.get_from_index( +            self.TYPE_MBOX_IDX, self.MBOX_KEY, name)          return doc[0] if doc else None      @property @@ -175,26 +245,28 @@ class SoledadBackedAccount(IndexedDB):          """          A list of the current mailboxes for this account.          """ -        return [str(doc.content['mbox']) -                for doc in self._db.get_from_index(self.TYPE_IDX, 'mbox')] +        return [str(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 [str(doc.content['mbox']) -                for doc in self._db.get_from_index( -                    self.TYPE_SUBS_IDX, 'mbox', '1')] +        return [str(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 Mailbox with that name, without selecting it. +        Returns a Mailbox with that name, without selecting it. -        @param name: name of the mailbox -        @type name: C{str} +        :param name: name of the mailbox +        :type name: str -        @returns: a a SoledadMailbox instance +        :returns: a a SoledadMailbox instance +        :rtype: SoledadMailbox          """          name = name.upper()          if name not in self.mailboxes: @@ -210,15 +282,16 @@ class SoledadBackedAccount(IndexedDB):          """          Adds a mailbox to the account. -        @param name: the name of the mailbox -        @type name: str +        :param name: the name of the mailbox +        :type name: str -        @param creation_ts: a optional creation timestamp to be used as -            mailbox id. A timestamp will be used if no one is provided. -        @type creation_ts: C{int} +        :param creation_ts: a 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 +        :returns: True if successful +        :rtype: bool          """          name = name.upper()          # XXX should check mailbox name for RFC-compliant form @@ -229,27 +302,33 @@ class SoledadBackedAccount(IndexedDB):          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 +            # maibox-uidvalidity.              creation_ts = int(time.time() * 10E2)          mbox = self._get_empty_mailbox() -        mbox['mbox'] = name -        mbox['created'] = creation_ts +        mbox[self.MBOX_KEY] = name +        mbox[self.CREATED_KEY] = creation_ts -        doc = self._db.create_doc(mbox) +        doc = self._soledad.create_doc(mbox)          return bool(doc)      def create(self, pathspec): -        # XXX What _exactly_ is the difference with addMailbox? -        # We accept here a path specification, which can contain -        # many levels, but look for the appropriate documentation -        # pointer.          """ -        Create a mailbox -        Return True if successfully created +        Create a new mailbox from the given hierarchical name. -        @param pathspec: XXX ??? ----------------- -        @rtype: bool +        :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, pathspec.split('/'))          for accum in range(1, len(paths)):              try: @@ -265,10 +344,15 @@ class SoledadBackedAccount(IndexedDB):      def select(self, name, readwrite=1):          """ -        Select a mailbox. -        @param name: the mailbox to select -        @param readwrite: 1 for readwrite permissions. -        @rtype: bool +        Selects a mailbox. + +        :param name: the mailbox to select +        :type name: str + +        :param readwrite: 1 for readwrite permissions. +        :type readwrite: int + +        :rtype: bool          """          name = name.upper() @@ -284,10 +368,16 @@ class SoledadBackedAccount(IndexedDB):      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 +        :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 = name.upper()          if not name in self.mailboxes: @@ -298,7 +388,7 @@ class SoledadBackedAccount(IndexedDB):          if force is False:              # See if this box is flagged \Noselect              # XXX use mbox.flags instead? -            if r'\Noselect' in mbox.getFlags(): +            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: @@ -318,9 +408,13 @@ class SoledadBackedAccount(IndexedDB):      def rename(self, oldname, newname):          """ -        Renames a mailbox -        @param oldname: old name of the mailbox -        @param newname: new name of the mailbox +        Renames a mailbox. + +        :param oldname: old name of the mailbox +        :type oldname: str + +        :param newname: new name of the mailbox +        :type newname: str          """          oldname = oldname.upper()          newname = newname.upper() @@ -337,8 +431,8 @@ class SoledadBackedAccount(IndexedDB):          for (old, new) in inferiors:              mbox = self._get_mailbox_by_name(old) -            mbox.content['mbox'] = new -            self._db.put_doc(mbox) +            mbox.content[self.MBOX_KEY] = new +            self._soledad.put_doc(mbox)          # XXX ---- FIXME!!!! ------------------------------------          # until here we just renamed the index... @@ -350,9 +444,10 @@ class SoledadBackedAccount(IndexedDB):      def _inferiorNames(self, name):          """ -        Return hierarchically inferior mailboxes -        @param name: the mailbox -        @rtype: list +        Return hierarchically inferior mailboxes. + +        :param name: name of the mailbox +        :rtype: list          """          # XXX use wildcard query instead          inferiors = [] @@ -365,8 +460,10 @@ class SoledadBackedAccount(IndexedDB):          """          Returns True if user is subscribed to this mailbox. -        @param name: the mailbox to be checked. -        @rtype: bool +        :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) @@ -375,11 +472,11 @@ class SoledadBackedAccount(IndexedDB):          """          Sets the subscription value for a given mailbox -        @param name: the mailbox -        @type name: C{str} +        :param name: the mailbox +        :type name: str -        @param value: the boolean value -        @type value: C{bool} +        :param value: the boolean value +        :type value: bool          """          # maybe we should store subscriptions in another          # document... @@ -389,15 +486,15 @@ class SoledadBackedAccount(IndexedDB):          mbox = self._get_mailbox_by_name(name)          if mbox: -            mbox.content['subscribed'] = value -            self._db.put_doc(mbox) +            mbox.content[self.SUBSCRIBED_KEY] = value +            self._soledad.put_doc(mbox)      def subscribe(self, name):          """          Subscribe to this mailbox -        @param name: the mailbox -        @type name: C{str} +        :param name: name of the mailbox +        :type name: str          """          name = name.upper()          if name not in self.subscriptions: @@ -407,8 +504,8 @@ class SoledadBackedAccount(IndexedDB):          """          Unsubscribe from this mailbox -        @param name: the mailbox -        @type name: C{str} +        :param name: name of the mailbox +        :type name: str          """          name = name.upper()          if name not in self.subscriptions: @@ -425,8 +522,11 @@ class SoledadBackedAccount(IndexedDB):          replies are returned, containing the name attributes, hierarchy          delimiter, and name. -        @param ref: reference name -        @param wildcard: mailbox name with possible wildcards +        :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(ref.upper()) @@ -453,13 +553,18 @@ class SoledadBackedAccount(IndexedDB):          Deletes all messages from all mailboxes.          Danger! high voltage! -        @param iknowhatiamdoing: confirmation parameter, needs to be True -            to proceed. +        :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  #######################################  # Soledad Message, MessageCollection @@ -467,7 +572,7 @@ class SoledadBackedAccount(IndexedDB):  ####################################### -class LeapMessage(object): +class LeapMessage(WithMsgFields):      implements(imap4.IMessage, imap4.IMessageFile) @@ -475,9 +580,9 @@ class LeapMessage(object):          """          Initializes a LeapMessage. -        @type doc: C{LeapDocument} -        @param doc: A LeapDocument containing the internal -        representation of the message +        :param doc: A LeapDocument containing the internal +                    representation of the message +        :type doc: LeapDocument          """          self._doc = doc @@ -485,23 +590,25 @@ class LeapMessage(object):          """          Retrieve the unique identifier associated with this message -        @rtype: C{int} +        :return: uid for this message +        :rtype: int          """ +        # XXX debug, to remove after a while...          if not self._doc:              log.msg('BUG!!! ---- message has no doc!')              return -        return self._doc.content['uid'] +        return self._doc.content[self.UID_KEY]      def getFlags(self):          """          Retrieve the flags associated with this message -        @rtype: C{iterable} -        @return: The flags, represented as strings +        :return: The flags, represented as strings +        :rtype: iterable          """          if self._doc is None:              return [] -        flags = self._doc.content.get('flags', None) +        flags = self._doc.content.get(self.FLAGS_KEY, None)          if flags:              flags = map(str, flags)          return flags @@ -515,24 +622,30 @@ class LeapMessage(object):          Returns a LeapDocument that needs to be updated by the caller. -        @type flags: sequence of C{str} -        @rtype: LeapDocument +        :param flags: the flags to update in the message. +        :type flags: sequence of str + +        :return: a LeapDocument instance +        :rtype: LeapDocument          """          log.msg('setting flags')          doc = self._doc -        doc.content['flags'] = flags -        doc.content['seen'] = "\\Seen" in flags -        doc.content['recent'] = "\\Recent" in flags -        return self._doc +        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 +        return doc      def addFlags(self, flags):          """ -        Adds flags to this message +        Adds flags to this message. -        Returns a document that needs to be updated by the caller. +        Returns a LeapDocument that needs to be updated by the caller. + +        :param flags: the flags to add to the message. +        :type flags: sequence of str -        @type flags: sequence of C{str} -        @rtype: LeapDocument +        :return: a LeapDocument instance +        :rtype: LeapDocument          """          oldflags = self.getFlags()          return self.setFlags(list(set(flags + oldflags))) @@ -541,10 +654,13 @@ class LeapMessage(object):          """          Remove flags from this message. -        Returns a document that needs to be updated by the caller. +        Returns a LeapDocument that needs to be updated by the caller. -        @type flags: sequence of C{str} -        @rtype: LeapDocument +        :param flags: the flags to be removed from the message. +        :type flags: sequence of str + +        :return: a LeapDocument instance +        :rtype: LeapDocument          """          oldflags = self.getFlags()          return self.setFlags(list(set(oldflags) - set(flags))) @@ -556,7 +672,7 @@ class LeapMessage(object):          @rtype: C{str}          @retur: An RFC822-formatted date string.          """ -        return str(self._doc.content.get('date', '')) +        return str(self._doc.content.get(self.DATE_KEY, ''))      #      # IMessageFile @@ -575,9 +691,12 @@ class LeapMessage(object):          Reading from the returned file will return all the bytes          of which this message consists. + +        :return: file-like object opened fore reading. +        :rtype: StringIO          """          fd = cStringIO.StringIO() -        fd.write(str(self._doc.content.get('raw', ''))) +        fd.write(str(self._doc.content.get(self.RAW_KEY, '')))          fd.seek(0)          return fd @@ -592,50 +711,57 @@ class LeapMessage(object):          """          Retrieve a file object containing only the body of this message. -        @rtype: C{StringIO} +        :return: file-like object opened for reading +        :rtype: StringIO          """          fd = StringIO.StringIO() -        fd.write(str(self._doc.content.get('raw', ''))) +        fd.write(str(self._doc.content.get(self.RAW_KEY, '')))          # SHOULD use a separate BODY FIELD ...          fd.seek(0)          return fd      def getSize(self):          """ -        Return the total size, in octets, of this message +        Return the total size, in octets, of this message. -        @rtype: C{int} +        :return: size of the message, in octets +        :rtype: int          """          return self.getBodyFile().len      def _get_headers(self):          """ -        Return the headers dict stored in this message document +        Return the headers dict stored in this message document.          """ -        return self._doc.content['headers'] +        return self._doc.content.get(self.HEADERS_KEY, {})      def getHeaders(self, negate, *names):          """          Retrieve a group of message headers. -        @type names: C{tuple} of C{str} -        @param names: The names of the headers to retrieve or omit. +        :param names: The names of the headers to retrieve or omit. +        :type names: tuple of str -        @type negate: C{bool} -        @param negate: If True, indicates that the headers listed in C{names} -        should be omitted from the return value, rather than included. +        :param negate: If True, indicates that the headers listed in names +                       should be omitted from the return value, rather +                       than included. +        :type negate: bool -        @rtype: C{dict} -        @return: A mapping of header field names to header field values +        :return: A mapping of header field names to header field values +        :rtype: dict          """          headers = self._get_headers()          if negate:              cond = lambda key: key.upper() not in names          else:              cond = lambda key: key.upper() in names -        return dict( -            [map(str, (key, val)) for key, val in headers.items() -             if cond(key)]) + +        # 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)      # --- no multipart for now @@ -646,7 +772,7 @@ class LeapMessage(object):          return None -class MessageCollection(object): +class MessageCollection(WithMsgFields):      """      A collection of messages, surprisingly. @@ -657,49 +783,53 @@ class MessageCollection(object):      # XXX this should be able to produce a MessageSet methinks      EMPTY_MSG = { -        "type": "msg", -        "uid": 1, -        "mbox": "inbox", -        "subject": "", -        "date": "", -        "seen": False, -        "recent": True, -        "flags": [], -        "headers": {}, -        "raw": "", +        WithMsgFields.TYPE_KEY: WithMsgFields.TYPE_MESSAGE_VAL, +        WithMsgFields.UID_KEY: 1, +        WithMsgFields.MBOX_KEY: WithMsgFields.INBOX_VAL, +        WithMsgFields.SUBJECT_KEY: "", +        WithMsgFields.DATE_KEY: "", +        WithMsgFields.SEEN_KEY: False, +        WithMsgFields.RECENT_KEY: True, +        WithMsgFields.FLAGS_KEY: [], +        WithMsgFields.HEADERS_KEY: {}, +        WithMsgFields.RAW_KEY: "",      } -    def __init__(self, mbox=None, db=None): +    def __init__(self, mbox=None, soledad=None):          """          Constructor for MessageCollection. -        @param mbox: the name of the mailbox. It is the name +        :param mbox: the name of the mailbox. It is the name                       with which we filter the query over the                       messages database -        @type mbox: C{str} +        :type mbox: str -        @param db: SQLCipher database (contained in soledad) -        @type db: SQLCipher instance +        :param soledad: Soledad database +        :type soledad: Soledad instance          """ +        # XXX pass soledad directly +          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(db, "Need a db instance to initialize") -        leap_assert(isinstance(db, SQLCipherDatabase), -                    "db must be an instance of SQLCipherDatabase") +        leap_assert(soledad, "Need a soledad instance to initialize") +        leap_assert(isinstance(soledad._db, SQLCipherDatabase), +                    "soledad._db must be an instance of SQLCipherDatabase")          # okay, all in order, keep going...          self.mbox = mbox.upper() -        self.db = db +        self._soledad = soledad +        #self.db = db          self._parser = Parser()      def _get_empty_msg(self):          """          Returns an empty message. -        @rtype: dict +        :return: a dict containing a default empty message +        :rtype: dict          """          return copy.deepcopy(self.EMPTY_MSG) @@ -707,20 +837,20 @@ class MessageCollection(object):          """          Creates a new message document. -        @param raw: the raw message -        @type raw: C{str} +        :param raw: the raw message +        :type raw: str -        @param subject: subject of the message. -        @type subject: C{str} +        :param subject: subject of the message. +        :type subject: str -        @param flags: flags -        @type flags: C{list} +        :param flags: flags +        :type flags: list -        @param date: the received date for the message -        @type date: C{str} +        :param date: the received date for the message +        :type date: str -        @param uid: the message uid for this mailbox -        @type uid: C{int} +        :param uid: the message uid for this mailbox +        :type uid: int          """          if flags is None:              flags = tuple() @@ -733,11 +863,11 @@ class MessageCollection(object):                  return o          content = self._get_empty_msg() -        content['mbox'] = self.mbox +        content[self.MBOX_KEY] = self.mbox          if flags: -            content['flags'] = map(stringify, flags) -            content['seen'] = "\\Seen" in flags +            content[self.FLAGS_KEY] = map(stringify, flags) +            content[self.SEEN_KEY] = self.SEEN_FLAG in flags          def _get_parser_fun(o):              if isinstance(o, (cStringIO.OutputType, StringIO.StringIO)): @@ -749,39 +879,55 @@ class MessageCollection(object):          headers = dict(msg)          # XXX get lower case for keys? -        content['headers'] = headers -        content['subject'] = headers['Subject'] -        content['raw'] = stringify(raw) +        content[self.HEADERS_KEY] = headers +        content[self.SUBJECT_KEY] = headers[self.SUBJECT_FIELD] +        content[self.RAW_KEY] = stringify(raw)          if not date: -            content['date'] = headers['Date'] +            content[self.DATE_KEY] = headers[self.DATE_FIELD]          # ...should get a sanity check here. -        content['uid'] = uid +        content[self.UID_KEY] = uid -        return self.db.create_doc(content) +        return self._soledad.create_doc(content)      def remove(self, msg):          """          Removes a message. -        @param msg: a u1db doc containing the message +        :param msg: a u1db doc containing the message +        :type msg: LeapDocument          """ -        self.db.delete_doc(msg) +        self._soledad.delete_doc(msg)      # getters      def get_by_uid(self, uid):          """ -        Retrieves a message document by UID +        Retrieves a message document by UID. + +        :param uid: the message uid to query by +        :type uid: int + +        :return: A LeapDocument instance matching the query, +                 or None if not found. +        :rtype: LeapDocument          """ -        docs = self.db.get_from_index( -            SoledadBackedAccount.TYPE_MBOX_UID_IDX, 'msg', self.mbox, str(uid)) +        docs = self._soledad.get_from_index( +            SoledadBackedAccount.TYPE_MBOX_UID_IDX, +            self.TYPE_MESSAGE_VAL, self.mbox, str(uid))          return docs[0] if docs else None      def get_msg_by_uid(self, uid):          """ -        Retrieves a LeapMessage by 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          """          doc = self.get_by_uid(uid)          if doc: @@ -789,53 +935,56 @@ class MessageCollection(object):      def get_all(self):          """ -        Get all messages for the selected mailbox -        Returns a list of u1db documents. +        Get all message documents for the selected mailbox.          If you want acess to the content, use __iter__ instead -        @rtype: list +        :return: a list of u1db documents +        :rtype: list of LeapDocument          """          # XXX this should return LeapMessage instances -        return self.db.get_from_index( -            SoledadBackedAccount.TYPE_MBOX_IDX, 'msg', self.mbox) +        return self._soledad.get_from_index( +            SoledadBackedAccount.TYPE_MBOX_IDX, +            self.TYPE_MESSAGE_VAL, self.mbox)      def unseen_iter(self):          """          Get an iterator for the message docs with no `seen` flag -        @rtype: C{iterable} +        :return: iterator through unseen message docs +        :rtype: iterable          """          return (doc for doc in -                self.db.get_from_index( +                self._soledad.get_from_index(                      SoledadBackedAccount.TYPE_MBOX_RECT_IDX, -                    'msg', self.mbox, '1')) +                    self.TYPE_MESSAGE_VAL, self.mbox, '1'))      def get_unseen(self):          """          Get all messages with the `Unseen` flag -        @rtype: C{list} -        @returns: a list of LeapMessages +        :returns: a list of LeapMessages +        :rtype: list          """          return [LeapMessage(doc) for doc in self.unseen_iter()]      def recent_iter(self):          """ -        Get an iterator for the message docs with recent flag. +        Get an iterator for the message docs with `recent` flag. -        @rtype: C{iterable} +        :return: iterator through recent message docs +        :rtype: iterable          """          return (doc for doc in -                self.db.get_from_index( +                self._soledad.get_from_index(                      SoledadBackedAccount.TYPE_MBOX_RECT_IDX, -                    'msg', self.mbox, '1')) +                    self.TYPE_MESSAGE_VAL, self.mbox, '1'))      def get_recent(self):          """          Get all messages with the `Recent` flag. -        @type: C{list} -        @returns: a list of LeapMessages +        :returns: a list of LeapMessages +        :rtype: list          """          return [LeapMessage(doc) for doc in self.recent_iter()] @@ -843,15 +992,15 @@ class MessageCollection(object):          """          Return the count of messages for this mailbox. -        @rtype: C{int} +        :rtype: int          """          return len(self.get_all())      def __len__(self):          """ -        Returns the number of messages on this mailbox +        Returns the number of messages on this mailbox. -        @rtype: C{int} +        :rtype: int          """          return self.count() @@ -859,17 +1008,21 @@ class MessageCollection(object):          """          Returns an iterator over all messages. -        @rtype: C{iterable} -        @returns: iterator of dicts with content for 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 __getitem__(self, uid):          """          Allows indexing as a list, with msg uid as the index. -        @type key: C{int} -        @param key: an integer index +        :param uid: an integer index +        :type uid: int + +        :return: LeapMessage or None if not found. +        :rtype: LeapMessage          """          try:              return self.get_msg_by_uid(uid) @@ -877,13 +1030,16 @@ class MessageCollection(object):              return None      def __repr__(self): +        """ +        Representation string for this object. +        """          return u"<MessageCollection: mbox '%s' (%s)>" % (              self.mbox, self.count())      # XXX should implement __eq__ also -class SoledadMailbox(object): +class SoledadMailbox(WithMsgFields):      """      A Soledad-backed IMAP mailbox. @@ -896,26 +1052,31 @@ class SoledadMailbox(object):      messages = None      _closed = False -    INIT_FLAGS = ('\\Seen', '\\Answered', '\\Flagged', -                  '\\Deleted', '\\Draft', '\\Recent', 'List') -    DELETED_FLAG = '\\Deleted' +    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" +      def __init__(self, mbox, soledad=None, rw=1):          """ -        SoledadMailbox constructor -        Needs to get passed a name, plus a soledad instance and -        the soledad account index, where it stores the flags for this -        mailbox. +        SoledadMailbox constructor. Needs to get passed a name, plus a +        Soledad instance. -        @param mbox: the mailbox name -        @type mbox: C{str} +        :param mbox: the mailbox name +        :type mbox: str -        @param soledad: a Soledad instance. -        @type soledad: C{Soledad} +        :param soledad: a Soledad instance. +        :type soledad: Soledad -        @param rw: read-and-write flags -        @type rw: C{int} +        :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") @@ -926,10 +1087,9 @@ class SoledadMailbox(object):          self.rw = rw          self._soledad = soledad -        self._db = soledad._db          self.messages = MessageCollection( -            mbox=mbox, db=soledad._db) +            mbox=mbox, soledad=soledad)          if not self.getFlags():              self.setFlags(self.INIT_FLAGS) @@ -945,41 +1105,75 @@ class SoledadMailbox(object):          #------------------------------------------      def _get_mbox(self): -        """Returns mailbox document""" -        return self._db.get_from_index( -            SoledadBackedAccount.TYPE_MBOX_IDX, 'mbox', self.mbox)[0] +        """ +        Returns mailbox document. + +        :return: A LeapDocument containing this mailbox. +        :rtype: LeapDocument +        """ +        query = self._soledad.get_from_index( +            SoledadBackedAccount.TYPE_MBOX_IDX, +            self.TYPE_MBOX_VAL, self.mbox) +        if query: +            return query.pop()      def getFlags(self):          """ -        Returns the possible flags of this mailbox -        @rtype: tuple +        Returns the flags defined for this mailbox. + +        :returns: tuple of flags for this mailbox +        :rtype: tuple of str          """ -        mbox = self._get_mbox() -        flags = mbox.content.get('flags', []) -        return map(str, flags) +        return map(str, self.INIT_FLAGS) + +        # TODO -- returning hardcoded flags for now, +        # no need of setting flags. + +        #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 +        Sets flags for this mailbox. + +        :param flags: a tuple with the flags +        :type flags: tuple of str          """ +        # TODO -- fix also getFlags          leap_assert(isinstance(flags, tuple),                      "flags expected to be a tuple")          mbox = self._get_mbox() -        mbox.content['flags'] = map(str, flags) -        self._db.put_doc(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('closed', False) +        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['closed'] = closed -        self._db.put_doc(mbox) +        mbox.content[self.CLOSED_KEY] = closed +        self._soledad.put_doc(mbox)      closed = property(          _get_closed, _set_closed, doc="Closed attribute.") @@ -988,92 +1182,117 @@ class SoledadMailbox(object):          """          Return the unique validity identifier for this mailbox. -        @rtype: C{int} +        :return: unique validity identifier +        :rtype: int          """          mbox = self._get_mbox() -        return mbox.content.get('created', 1) +        return mbox.content.get(self.CREATED_KEY, 1)      def getUID(self, message):          """          Return the UID of a message in the mailbox -        @rtype: C{int} -        """ -        msg = self.messages.get_msg_by_uid(message) -        return msg.getUID() +        .. 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. -    def getRecentCount(self): -        """ -        Returns the number of messages with the 'Recent' flag +        :param message: the message uid +        :type message: int -        @rtype: C{int} +        :rtype: int          """ -        return len(self.messages.get_recent()) +        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 +        mailbox. Currently it returns the current length incremented +        by one. -        @rtype: C{int} +        :rtype: int          """          # XXX reimplement with proper index          return self.messages.count() + 1      def getMessageCount(self):          """ -        Returns the total count of messages in this mailbox +        Returns the total count of messages in this mailbox. + +        :rtype: int          """          return self.messages.count()      def getUnseenCount(self):          """ -        Returns the total count of unseen messages in this mailbox +        Returns the number of messages with the 'Unseen' flag. + +        :return: count of messages flagged `unseen` +        :rtype: int          """          return len(self.messages.get_unseen()) +    def getRecentCount(self): +        """ +        Returns the number of messages with the 'Recent' flag. + +        :return: count of messages flagged `recent` +        :rtype: int +        """ +        return len(self.messages.get_recent()) +      def isWriteable(self):          """ -        Get the read/write status of the mailbox -        @rtype: C{int} +        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 +        Returns the character used to delimite hierarchies in mailboxes. -        @rtype: C{str} +        :rtype: str          """          return '/'      def requestStatus(self, names):          """          Handles a status request by gathering the output of the different -        status commands +        status commands. -        @param names: a list of strings containing the status commands -        @type names: iter +        :param names: a list of strings containing the status commands +        :type names: iter          """          r = {} -        if 'MESSAGES' in names: -            r['MESSAGES'] = self.getMessageCount() -        if 'RECENT' in names: -            r['RECENT'] = self.getRecentCount() -        if 'UIDNEXT' in names: -            r['UIDNEXT'] = self.getMessageCount() + 1 -        if 'UIDVALIDITY' in names: -            r['UIDVALIDITY'] = self.getUID() -        if 'UNSEEN' in names: -            r['UNSEEN'] = self.getUnseenCount() +        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.getMessageCount() + 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 -        @flags: flag list -        @date: timestamp +        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          uid_next = self.getUIDNext() @@ -1092,12 +1311,12 @@ class SoledadMailbox(object):          Should cleanup resources, and set the \\Noselect flag          on the mailbox.          """ -        self.setFlags(('\\Noselect',)) +        self.setFlags((self.NOSELECT_FLAG,))          self.deleteAllDocs()          # XXX removing the mailbox in situ for now,          # we should postpone the removal -        self._db.delete_doc(self._get_mbox()) +        self._soledad.delete_doc(self._get_mbox())      def expunge(self):          """ @@ -1109,7 +1328,7 @@ class SoledadMailbox(object):          delete = []          deleted = []          for m in self.messages.get_all(): -            if self.DELETED_FLAG in m.content['flags']: +            if self.DELETED_FLAG in m.content[self.FLAGS_KEY]:                  delete.append(m)          for m in delete:              deleted.append(m.content) @@ -1126,15 +1345,15 @@ class SoledadMailbox(object):          from rfc 3501: The data items to be fetched can be either a single atom          or a parenthesized list. -        @type messages: C{MessageSet} -        @param messages: IDs of the messages to retrieve information about +        :param messages: IDs of the messages to retrieve information about +        :type messages: MessageSet -        @type uid: C{bool} -        @param uid: If true, the IDs are UIDs. They are message sequence IDs -        otherwise. +        :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 -        C{LeapMessage} +        :rtype: A tuple of two-tuples of message sequence numbers and +                LeapMessage          """          # XXX implement sequence numbers (uid = 0)          result = [] @@ -1152,34 +1371,35 @@ class SoledadMailbox(object):          """          Sets the flags of one or more messages. -        @type messages: A MessageSet object with the list of messages requested -        @param messages: The identifiers of the messages to set the flags +        :param messages: The identifiers of the messages to set the flags +        :type messages: A MessageSet object with the list of messages requested -        @type flags: sequence of {str} -        @param flags: The flags to set, unset, or  add. +        :param flags: The flags to set, unset, or add. +        :type flags: sequence of str -        @type mode: -1, 0, or 1 -        @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. +        :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 -        @type uid: C{bool} -        @param uid: If true, the IDs specified in the query are UIDs; -        otherwise they are message sequence IDs. +        :param uid: If true, the IDs specified in the query are UIDs; +                    otherwise they are message sequence IDs. +        :type uid: bool -        @rtype: C{dict} -        @return: A C{dict} mapping message sequence numbers to sequences of -        C{str} -        representing the flags set on the message after this operation has -        been performed, or a C{Deferred} whose callback will be invoked with -        such a dict +        :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. +        :raise ReadOnlyMailbox: Raised if this mailbox is not open for +                                read-write.          """          # XXX implement also sequence (uid = 0) +          if not self.isWriteable(): +            log.msg('read only mailbox!')              raise imap4.ReadOnlyMailbox          if not messages.last: @@ -1187,6 +1407,7 @@ class SoledadMailbox(object):          result = {}          for msg_id in messages: +            print "MSG ID = %s" % msg_id              msg = self.messages.get_msg_by_uid(msg_id)              if mode == 1:                  self._update(msg.addFlags(flags)) @@ -1220,8 +1441,11 @@ class SoledadMailbox(object):          Updates document in u1db database          """          #log.msg('updating doc... %s ' % doc) -        self._db.put_doc(doc) +        self._soledad.put_doc(doc)      def __repr__(self): +        """ +        Representation string for this mailbox. +        """          return u"<SoledadMailbox: mbox '%s' (%s)>" % (              self.mbox, self.messages.count()) diff --git a/mail/src/leap/mail/imap/service/imap-server.tac b/mail/src/leap/mail/imap/service/imap-server.tac index e491e06..362b536 100644 --- a/mail/src/leap/mail/imap/service/imap-server.tac +++ b/mail/src/leap/mail/imap/service/imap-server.tac @@ -117,7 +117,7 @@ class LeapIMAPFactory(ServerFactory):  def initialize_mailbox_soledad(user_uuid, soledad_pass, server_url, -                       	       server_pemfile, token): +                               server_pemfile, token):      """      Initializes soledad by hand @@ -137,16 +137,14 @@ def initialize_mailbox_soledad(user_uuid, soledad_pass, server_url,      soledad_path = os.path.join(          base_config, "leap", "soledad", "%s-mailbox.db" % user_uuid) -      _soledad = Soledad(          user_uuid, -	soledad_pass, +        soledad_pass,          secret_path,          soledad_path, -	server_url, +        server_url,          server_pemfile, -        token, -        bootstrap=True) +        token)      #_soledad._init_dirs()      #_soledad._crypto = SoledadCrypto(_soledad)      #_soledad._shared_db = None @@ -166,6 +164,7 @@ def populate_test_account(acct):      inbox.addMessage(mail_sample, ("\\Foo", "\\Recent",), date="Right now2")  ''' +  def incoming_check(fetcher):      """      Check incoming queue. To be called periodically. | 
