diff options
Diffstat (limited to 'mail')
-rw-r--r-- | mail/changes/bug_4454_remove-multipart-encrypted-header-after-decrypting | 2 | ||||
-rw-r--r-- | mail/changes/bug_4461_fix-uid-indexing | 4 | ||||
-rw-r--r-- | mail/changes/bug_imap-user-check | 1 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/fetch.py | 13 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/server.py | 150 | ||||
-rw-r--r-- | mail/src/leap/mail/imap/service/imap.py | 50 |
6 files changed, 174 insertions, 46 deletions
diff --git a/mail/changes/bug_4454_remove-multipart-encrypted-header-after-decrypting b/mail/changes/bug_4454_remove-multipart-encrypted-header-after-decrypting new file mode 100644 index 0000000..8aa0aaa --- /dev/null +++ b/mail/changes/bug_4454_remove-multipart-encrypted-header-after-decrypting @@ -0,0 +1,2 @@ + o Remove 'multipart/encrypted' header after decrypting incoming mail. Closes + #4454. diff --git a/mail/changes/bug_4461_fix-uid-indexing b/mail/changes/bug_4461_fix-uid-indexing new file mode 100644 index 0000000..881bb24 --- /dev/null +++ b/mail/changes/bug_4461_fix-uid-indexing @@ -0,0 +1,4 @@ + o Fix several bugs with imap mailbox getUIDNext and notifiers that were breaking + the mail indexing after message deletion. This solves also the perceived + mismatch between the number of unread mails reported by bitmask_client and + the number reported by MUAs. Closes: #4461 diff --git a/mail/changes/bug_imap-user-check b/mail/changes/bug_imap-user-check new file mode 100644 index 0000000..678871d --- /dev/null +++ b/mail/changes/bug_imap-user-check @@ -0,0 +1 @@ + o Check username in authentications. Closes: #4299 diff --git a/mail/src/leap/mail/imap/fetch.py b/mail/src/leap/mail/imap/fetch.py index 4d47408..3422ed5 100644 --- a/mail/src/leap/mail/imap/fetch.py +++ b/mail/src/leap/mail/imap/fetch.py @@ -141,16 +141,19 @@ class LeapIncomingMail(object): """ Starts a loop to fetch mail. """ - self._loop = LoopingCall(self.fetch) - self._loop.start(self._check_period) + if self._loop is None: + self._loop = LoopingCall(self.fetch) + self._loop.start(self._check_period) + else: + logger.warning("Tried to start an already running fetching loop.") def stop(self): """ Stops the loop that fetches mail. """ - # XXX should cancel ongoing fetches too. if self._loop and self._loop.running is True: self._loop.stop() + self._loop = None # # Private methods. @@ -214,7 +217,7 @@ class LeapIncomingMail(object): Generic errback """ err = failure.value - logger.error("error!: %r" % (err,)) + logger.exception("error!: %r" % (err,)) def _decryption_error(self, failure): """ @@ -387,6 +390,8 @@ class LeapIncomingMail(object): decrdata = decrdata.encode(encoding, 'replace') decrmsg = parser.parsestr(decrdata) + # remove original message's multipart/encrypted content-type + del(origmsg['content-type']) # replace headers back in original message for hkey, hval in decrmsg.items(): try: diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py index 11f3ccf..bb2830d 100644 --- a/mail/src/leap/mail/imap/server.py +++ b/mail/src/leap/mail/imap/server.py @@ -23,6 +23,7 @@ import StringIO import cStringIO import time +from collections import defaultdict from email.parser import Parser from zope.interface import implements @@ -241,6 +242,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): :rtype: SoledadDocument """ + # XXX only upper for INBOX --- name = name.upper() doc = self._soledad.get_from_index( self.TYPE_MBOX_IDX, self.MBOX_KEY, name) @@ -274,6 +276,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): :returns: a a SoledadMailbox instance :rtype: SoledadMailbox """ + # XXX only upper for INBOX name = name.upper() if name not in self.mailboxes: raise imap4.MailboxException("No such mailbox") @@ -299,6 +302,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): :returns: True if successful :rtype: bool """ + # XXX only upper for INBOX name = name.upper() # XXX should check mailbox name for RFC-compliant form @@ -360,6 +364,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): :rtype: bool """ + # XXX only upper for INBOX name = name.upper() if name not in self.mailboxes: @@ -385,6 +390,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): names. use with care. :type force: bool """ + # XXX only upper for INBOX name = name.upper() if not name in self.mailboxes: raise imap4.MailboxException("No such mailbox") @@ -422,6 +428,7 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): :param newname: new name of the mailbox :type newname: str """ + # XXX only upper for INBOX oldname = oldname.upper() newname = newname.upper() @@ -487,7 +494,6 @@ class SoledadBackedAccount(WithMsgFields, IndexedDB): # maybe we should store subscriptions in another # document... if not name in self.mailboxes: - print "not this mbox" self.addMailbox(name) mbox = self._get_mailbox_by_name(name) @@ -785,6 +791,7 @@ class LeapMessage(WithMsgFields): return dict(filter_by_cond) # --- no multipart for now + # XXX Fix MULTIPART SUPPORT! def isMultipart(self): return False @@ -967,6 +974,7 @@ class MessageCollection(WithMsgFields, IndexedDB): 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): @@ -984,6 +992,47 @@ class MessageCollection(WithMsgFields, IndexedDB): if doc: return LeapMessage(doc) + def get_by_index(self, index): + """ + Retrieves a mesage document by mailbox index. + + :param index: the index of the sequence (zero-indexed) + :type index: int + """ + try: + return self.get_all()[index] + except IndexError: + return None + + def get_msg_by_index(self, index): + """ + Retrieves a LeapMessage by sequence index. + + :param index: the index of the sequence (zero-indexed) + :type index: int + """ + doc = self.get_by_index(index) + if doc: + return LeapMessage(doc) + + def is_deleted(self, doc): + """ + Returns whether a given doc is deleted or not. + + :param doc: the document to check + :rtype: bool + """ + return self.DELETED_FLAG in doc.content[self.FLAGS_KEY] + + def get_last(self): + """ + Gets the last LeapMessage + """ + _all = self.get_all() + if not _all: + return None + return LeapMessage(_all[-1]) + def get_all(self): """ Get all message documents for the selected mailbox. @@ -993,9 +1042,13 @@ class MessageCollection(WithMsgFields, IndexedDB): :rtype: list of SoledadDocument """ # XXX this should return LeapMessage instances - return self._soledad.get_from_index( + all_docs = [doc for doc in self._soledad.get_from_index( SoledadBackedAccount.TYPE_MBOX_IDX, - self.TYPE_MESSAGE_VAL, self.mbox) + self.TYPE_MESSAGE_VAL, self.mbox)] + #if not self.is_deleted(doc)] + # highly inneficient, but first let's grok it and then + # let's worry about efficiency. + return sorted(all_docs, key=lambda item: item.content['uid']) def unseen_iter(self): """ @@ -1075,8 +1128,11 @@ class MessageCollection(WithMsgFields, IndexedDB): :return: LeapMessage or None if not found. :rtype: LeapMessage """ + #try: + #return self.get_msg_by_uid(uid) try: - return self.get_msg_by_uid(uid) + return [doc + for doc in self.get_all()][uid - 1] except IndexError: return None @@ -1116,7 +1172,7 @@ class SoledadMailbox(WithMsgFields): CMD_UIDVALIDITY = "UIDVALIDITY" CMD_UNSEEN = "UNSEEN" - listeners = [] + _listeners = defaultdict(set) def __init__(self, mbox, soledad=None, rw=1): """ @@ -1150,10 +1206,18 @@ class SoledadMailbox(WithMsgFields): if not self.getFlags(): self.setFlags(self.INIT_FLAGS) - # the server itself is a listener to the mailbox. - # so we can notify it (and should!) after chanes in flags - # and number of messages. - map(lambda i: self.listeners.remove(i), self.listeners) + @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): """ @@ -1163,7 +1227,7 @@ class SoledadMailbox(WithMsgFields): :type listener: an object that implements IMailboxListener """ logger.debug('adding mailbox listener: %s' % listener) - self.listeners.append(listener) + self.listeners.add(listener) def removeListener(self, listener): """ @@ -1172,25 +1236,24 @@ class SoledadMailbox(WithMsgFields): :param listener: listener to remove :type listener: an object that implements IMailboxListener """ - logger.debug('removing mailbox listener: %s' % listener) - try: - self.listeners.remove(listener) - except ValueError: - logger.error( - "removeListener: cannot remove listener %s" % listener) + self.listeners.remove(listener) def _get_mbox(self): """ Returns mailbox document. - :return: A SoledadDocument containing this mailbox. - :rtype: SoledadDocument + :return: A SoledadDocument containing this mailbox, or None if + the query failed. + :rtype: SoledadDocument or None. """ - query = self._soledad.get_from_index( - SoledadBackedAccount.TYPE_MBOX_IDX, - self.TYPE_MBOX_VAL, self.mbox) - if query: - return query.pop() + 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): """ @@ -1287,8 +1350,12 @@ class SoledadMailbox(WithMsgFields): :rtype: int """ - # XXX reimplement with proper index - return self.messages.count() + 1 + last = self.messages.get_last() + if last: + nextuid = last.getUID() + 1 + else: + nextuid = 1 + return nextuid def getMessageCount(self): """ @@ -1375,6 +1442,8 @@ class SoledadMailbox(WithMsgFields): self.messages.add_msg(message, flags=flags, date=date, uid=uid_next) + + # XXX recent should not include deleted...?? exists = len(self.messages) recent = len(self.messages.get_recent()) for listener in self.listeners: @@ -1434,16 +1503,35 @@ class SoledadMailbox(WithMsgFields): :rtype: A tuple of two-tuples of message sequence numbers and LeapMessage """ - # XXX implement sequence numbers (uid = 0) result = [] + sequence = True if uid == 0 else False if not messages.last: - messages.last = self.messages.count() + try: + iter(messages) + except TypeError: + # looks like we cannot iterate + last = self.messages.get_last() + uid_last = last.getUID() + messages.last = uid_last + + # for sequence numbers (uid = 0) + if sequence: + for msg_id in messages: + msg = self.messages.get_msg_by_index(msg_id - 1) + if msg: + result.append((msg.getUID(), msg)) + else: + print "fetch %s, no msg found!!!" % msg_id + + else: + for msg_id in messages: + msg = self.messages.get_msg_by_uid(msg_id) + if msg: + result.append((msg_id, msg)) + else: + print "fetch %s, no msg found!!!" % msg_id - for msg_id in messages: - msg = self.messages.get_msg_by_uid(msg_id) - if msg: - result.append((msg_id, msg)) return tuple(result) def _signal_unread_to_ui(self): diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py index 984ad04..feb2593 100644 --- a/mail/src/leap/mail/imap/service/imap.py +++ b/mail/src/leap/mail/imap/service/imap.py @@ -25,6 +25,7 @@ from twisted.internet.protocol import ServerFactory from twisted.internet.error import CannotListenError from twisted.mail import imap4 from twisted.python import log +from twisted import cred logger = logging.getLogger(__name__) @@ -54,10 +55,13 @@ class LeapIMAPServer(imap4.IMAP4Server): def __init__(self, *args, **kwargs): # pop extraneous arguments soledad = kwargs.pop('soledad', None) - user = kwargs.pop('user', None) + uuid = kwargs.pop('uuid', None) + userid = kwargs.pop('userid', None) leap_assert(soledad, "need a soledad instance") leap_assert_type(soledad, Soledad) - leap_assert(user, "need a user in the initialization") + leap_assert(uuid, "need a user in the initialization") + + self._userid = userid # initialize imap server! imap4.IMAP4Server.__init__(self, *args, **kwargs) @@ -77,6 +81,12 @@ class LeapIMAPServer(imap4.IMAP4Server): #self.theAccount = theAccount def lineReceived(self, line): + """ + Attempt to parse a single line from the server. + + :param line: the line from the server, without the line delimiter. + :type line: str + """ if "login" in line.lower(): # avoid to log the pass, even though we are using a dummy auth # by now. @@ -87,7 +97,21 @@ class LeapIMAPServer(imap4.IMAP4Server): imap4.IMAP4Server.lineReceived(self, line) def authenticateLogin(self, username, password): - # all is allowed so far. use realm instead + """ + Lookup the account with the given parameters, and deny + the improper combinations. + + :param username: the username that is attempting authentication. + :type username: str + :param password: the password to authenticate with. + :type password: str + """ + # XXX this should use portal: + # return portal.login(cred.credentials.UsernamePassword(user, pass) + if username != self._userid: + # bad username, reject. + raise cred.error.UnauthorizedLogin() + # any dummy password is allowed so far. use realm instead! leap_events.signal(IMAP_CLIENT_LOGIN, "1") return imap4.IAccount, self.theAccount, lambda: None @@ -108,28 +132,32 @@ class LeapIMAPFactory(ServerFactory): capabilities. """ - def __init__(self, user, soledad): + def __init__(self, uuid, userid, soledad): """ Initializes the server factory. - :param user: user ID. **right now it's uuid** - this might change! - :type user: str + :param uuid: user uuid + :type uuid: str + + :param userid: user id (user@provider.org) + :type userid: str :param soledad: soledad instance :type soledad: Soledad """ - self._user = user + self._uuid = uuid + self._userid = userid self._soledad = soledad theAccount = SoledadBackedAccount( - user, soledad=soledad) + uuid, soledad=soledad) self.theAccount = theAccount def buildProtocol(self, addr): "Return a protocol suitable for the job." imapProtocol = LeapIMAPServer( - user=self._user, + uuid=self._uuid, + userid=self._userid, soledad=self._soledad) imapProtocol.theAccount = self.theAccount imapProtocol.factory = self @@ -156,7 +184,7 @@ def run_service(*args, **kwargs): leap_check(userid is not None, "need an user id") uuid = soledad._get_uuid() - factory = LeapIMAPFactory(uuid, soledad) + factory = LeapIMAPFactory(uuid, userid, soledad) from twisted.internet import reactor |