summaryrefslogtreecommitdiff
path: root/mail/src/leap/mail/imap/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'mail/src/leap/mail/imap/server.py')
-rw-r--r--mail/src/leap/mail/imap/server.py693
1 files changed, 0 insertions, 693 deletions
diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py
deleted file mode 100644
index 5a63af0..0000000
--- a/mail/src/leap/mail/imap/server.py
+++ /dev/null
@@ -1,693 +0,0 @@
-# -*- coding: utf-8 -*-
-# server.py
-# Copyright (C) 2014 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/>.
-"""
-LEAP IMAP4 Server Implementation.
-"""
-import StringIO
-from copy import copy
-
-from twisted.internet.defer import maybeDeferred
-from twisted.mail import imap4
-from twisted.python import log
-
-# imports for LITERAL+ patch
-from twisted.internet import defer, interfaces
-from twisted.mail.imap4 import IllegalClientResponse
-from twisted.mail.imap4 import LiteralString, LiteralFile
-
-from leap.common.events import emit_async, catalog
-
-
-def _getContentType(msg):
- """
- Return a two-tuple of the main and subtype of the given message.
- """
- attrs = None
- mm = msg.getHeaders(False, 'content-type').get('content-type', None)
- if mm:
- mm = ''.join(mm.splitlines())
- mimetype = mm.split(';')
- if mimetype:
- type = mimetype[0].split('/', 1)
- if len(type) == 1:
- major = type[0]
- minor = None
- elif len(type) == 2:
- major, minor = type
- else:
- major = minor = None
- # XXX patched ---------------------------------------------
- attrs = dict(x.strip().split('=', 1) for x in mimetype[1:])
- # XXX patched ---------------------------------------------
- else:
- major = minor = None
- else:
- major = minor = None
- return major, minor, attrs
-
-# Monkey-patch _getContentType to avoid bug that passes lower-case boundary in
-# BODYSTRUCTURE response.
-imap4._getContentType = _getContentType
-
-
-class LEAPIMAPServer(imap4.IMAP4Server):
- """
- An IMAP4 Server with a LEAP Storage Backend.
- """
-
- #############################################################
- #
- # Twisted imap4 patch to workaround bad mime rendering in TB.
- # See https://leap.se/code/issues/6773
- # and https://bugzilla.mozilla.org/show_bug.cgi?id=149771
- # Still unclear if this is a thunderbird bug.
- # TODO send this patch upstream
- #
- #############################################################
-
- def spew_body(self, part, id, msg, _w=None, _f=None):
- if _w is None:
- _w = self.transport.write
- for p in part.part:
- if msg.isMultipart():
- msg = msg.getSubPart(p)
- elif p > 0:
- # Non-multipart messages have an implicit first part but no
- # other parts - reject any request for any other part.
- raise TypeError("Requested subpart of non-multipart message")
-
- if part.header:
- hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
- hdrs = imap4._formatHeaders(hdrs)
- # PATCHED ##########################################
- _w(str(part) + ' ' + imap4._literal(hdrs + "\r\n"))
- # PATCHED ##########################################
- elif part.text:
- _w(str(part) + ' ')
- _f()
- return imap4.FileProducer(
- msg.getBodyFile()
- ).beginProducing(self.transport)
- elif part.mime:
- hdrs = imap4._formatHeaders(msg.getHeaders(True))
-
- # PATCHED ##########################################
- _w(str(part) + ' ' + imap4._literal(hdrs + "\r\n"))
- # END PATCHED ######################################
-
- elif part.empty:
- _w(str(part) + ' ')
- _f()
- if part.part:
- # PATCHED #############################################
- # implement partial FETCH
- # TODO implement boundary checks
- # TODO see if there's a more efficient way, without
- # copying the original content into a new buffer.
- fd = msg.getBodyFile()
- begin = getattr(part, "partialBegin", None)
- _len = getattr(part, "partialLength", None)
- if begin is not None and _len is not None:
- _fd = StringIO.StringIO()
- fd.seek(part.partialBegin)
- _fd.write(fd.read(part.partialLength))
- _fd.seek(0)
- else:
- _fd = fd
- return imap4.FileProducer(
- _fd
- # END PATCHED #########################3
- ).beginProducing(self.transport)
- else:
- mf = imap4.IMessageFile(msg, None)
- if mf is not None:
- return imap4.FileProducer(
- mf.open()).beginProducing(self.transport)
- return imap4.MessageProducer(
- msg, None, self._scheduler).beginProducing(self.transport)
-
- else:
- _w('BODY ' +
- imap4.collapseNestedLists([imap4.getBodyStructure(msg)]))
-
- ##################################################################
- #
- # END Twisted imap4 patch to workaround bad mime rendering in TB.
- # #6773
- #
- ##################################################################
-
- 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.
- msg = line[:7] + " [...]"
- else:
- msg = copy(line)
- log.msg('rcv (%s): %s' % (self.state, msg))
- imap4.IMAP4Server.lineReceived(self, line)
-
- def close_server_connection(self):
- """
- Send a BYE command so that the MUA at least knows that we're closing
- the connection.
- """
- self.sendLine(
- '* BYE LEAP IMAP Proxy is shutting down; '
- 'so long and thanks for all the fish')
- self.transport.loseConnection()
- if self.mbox:
- self.mbox.removeListener(self)
- self.mbox = None
- self.state = 'unauth'
-
- def do_FETCH(self, tag, messages, query, uid=0):
- """
- Overwritten fetch dispatcher to use the fast fetch_flags
- method
- """
- if not query:
- self.sendPositiveResponse(tag, 'FETCH complete')
- return
-
- cbFetch = self._IMAP4Server__cbFetch
- ebFetch = self._IMAP4Server__ebFetch
-
- if len(query) == 1 and str(query[0]) == "flags":
- self._oldTimeout = self.setTimeout(None)
- # no need to call iter, we get a generator
- maybeDeferred(
- self.mbox.fetch_flags, messages, uid=uid
- ).addCallback(
- cbFetch, tag, query, uid
- ).addErrback(ebFetch, tag)
-
- elif len(query) == 1 and str(query[0]) == "rfc822.header":
- self._oldTimeout = self.setTimeout(None)
- # no need to call iter, we get a generator
- maybeDeferred(
- self.mbox.fetch_headers, messages, uid=uid
- ).addCallback(
- cbFetch, tag, query, uid
- ).addErrback(ebFetch, tag)
- else:
- self._oldTimeout = self.setTimeout(None)
- # no need to call iter, we get a generator
- maybeDeferred(
- self.mbox.fetch, messages, uid=uid
- ).addCallback(
- cbFetch, tag, query, uid
- ).addErrback(
- ebFetch, tag)
-
- select_FETCH = (do_FETCH, imap4.IMAP4Server.arg_seqset,
- imap4.IMAP4Server.arg_fetchatt)
-
- def _cbSelectWork(self, mbox, cmdName, tag):
- """
- Callback for selectWork
-
- * patched to avoid conformance errors due to incomplete UIDVALIDITY
- line.
- * patched to accept deferreds for messagecount and recent count
- """
- if mbox is None:
- self.sendNegativeResponse(tag, 'No such mailbox')
- return
- if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
- self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
- return
-
- d1 = defer.maybeDeferred(mbox.getMessageCount)
- d2 = defer.maybeDeferred(mbox.getRecentCount)
- return defer.gatherResults([d1, d2]).addCallback(
- self.__cbSelectWork, mbox, cmdName, tag)
-
- def __cbSelectWork(self, ((msg_count, recent_count)), mbox, cmdName, tag):
- flags = mbox.getFlags()
- self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
-
- # Patched -------------------------------------------------------
- # accept deferreds for the count
- self.sendUntaggedResponse(str(msg_count) + ' EXISTS')
- self.sendUntaggedResponse(str(recent_count) + ' RECENT')
- # ----------------------------------------------------------------
-
- # Patched -------------------------------------------------------
- # imaptest was complaining about the incomplete line, we're adding
- # "UIDs valid" here.
- self.sendPositiveResponse(
- None, '[UIDVALIDITY %d] UIDs valid' % mbox.getUIDValidity())
- # ----------------------------------------------------------------
-
- s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
- mbox.addListener(self)
- self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
- self.state = 'select'
- self.mbox = mbox
-
- def checkpoint(self):
- """
- Called when the client issues a CHECK command.
-
- This should perform any checkpoint operations required by the server.
- It may be a long running operation, but may not block. If it returns
- a deferred, the client will only be informed of success (or failure)
- when the deferred's callback (or errback) is invoked.
- """
- # TODO implement a collection of ongoing deferreds?
- return None
-
- #############################################################
- #
- # Twisted imap4 patch to support LITERAL+ extension
- # TODO send this patch upstream asap!
- #
- #############################################################
-
- def capabilities(self):
- cap = {'AUTH': self.challengers.keys()}
- if self.ctx and self.canStartTLS:
- t = self.transport
- ti = interfaces.ISSLTransport
- if not self.startedTLS and ti(t, None) is None:
- cap['LOGINDISABLED'] = None
- cap['STARTTLS'] = None
- cap['NAMESPACE'] = None
- cap['IDLE'] = None
- # patched ############
- cap['LITERAL+'] = None
- ######################
- return cap
-
- def _stringLiteral(self, size, literal_plus=False):
- if size > self._literalStringLimit:
- raise IllegalClientResponse(
- "Literal too long! I accept at most %d octets" %
- (self._literalStringLimit,))
- d = defer.Deferred()
- self.parseState = 'pending'
- self._pendingLiteral = LiteralString(size, d)
- # Patched ###########################################################
- if not literal_plus:
- self.sendContinuationRequest('Ready for %d octets of text' % size)
- #####################################################################
- self.setRawMode()
- return d
-
- def _fileLiteral(self, size, literal_plus=False):
- d = defer.Deferred()
- self.parseState = 'pending'
- self._pendingLiteral = LiteralFile(size, d)
- if not literal_plus:
- self.sendContinuationRequest('Ready for %d octets of data' % size)
- self.setRawMode()
- return d
-
- def arg_astring(self, line):
- """
- Parse an astring from the line, return (arg, rest), possibly
- via a deferred (to handle literals)
- """
- line = line.strip()
- if not line:
- raise IllegalClientResponse("Missing argument")
- d = None
- arg, rest = None, None
- if line[0] == '"':
- try:
- spam, arg, rest = line.split('"', 2)
- rest = rest[1:] # Strip space
- except ValueError:
- raise IllegalClientResponse("Unmatched quotes")
- elif line[0] == '{':
- # literal
- if line[-1] != '}':
- raise IllegalClientResponse("Malformed literal")
-
- # Patched ################
- if line[-2] == "+":
- literalPlus = True
- size_end = -2
- else:
- literalPlus = False
- size_end = -1
-
- try:
- size = int(line[1:size_end])
- except ValueError:
- raise IllegalClientResponse(
- "Bad literal size: " + line[1:size_end])
- d = self._stringLiteral(size, literalPlus)
- ##########################
- else:
- arg = line.split(' ', 1)
- if len(arg) == 1:
- arg.append('')
- arg, rest = arg
- return d or (arg, rest)
-
- def arg_literal(self, line):
- """
- Parse a literal from the line
- """
- if not line:
- raise IllegalClientResponse("Missing argument")
-
- if line[0] != '{':
- raise IllegalClientResponse("Missing literal")
-
- if line[-1] != '}':
- raise IllegalClientResponse("Malformed literal")
-
- # Patched ##################
- if line[-2] == "+":
- literalPlus = True
- size_end = -2
- else:
- literalPlus = False
- size_end = -1
-
- try:
- size = int(line[1:size_end])
- except ValueError:
- raise IllegalClientResponse(
- "Bad literal size: " + line[1:size_end])
-
- return self._fileLiteral(size, literalPlus)
- #############################
-
- # --------------------------------- isSubscribed patch
- # TODO -- send patch upstream.
- # There is a bug in twisted implementation:
- # in cbListWork, it's assumed that account.isSubscribed IS a callable,
- # although in the interface documentation it's stated that it can be
- # a deferred.
-
- def _listWork(self, tag, ref, mbox, sub, cmdName):
- mbox = self._parseMbox(mbox)
- mailboxes = maybeDeferred(self.account.listMailboxes, ref, mbox)
- mailboxes.addCallback(self._cbSubscribed)
- mailboxes.addCallback(
- self._cbListWork, tag, sub, cmdName,
- ).addErrback(self._ebListWork, tag)
-
- def _cbSubscribed(self, mailboxes):
- subscribed = [
- maybeDeferred(self.account.isSubscribed, name)
- for (name, box) in mailboxes]
-
- def get_mailboxes_and_subs(result):
- subscribed = [i[0] for i, yes in zip(mailboxes, result) if yes]
- return mailboxes, subscribed
-
- d = defer.gatherResults(subscribed)
- d.addCallback(get_mailboxes_and_subs)
- return d
-
- def _cbListWork(self, mailboxes_subscribed, tag, sub, cmdName):
- mailboxes, subscribed = mailboxes_subscribed
-
- for (name, box) in mailboxes:
- if not sub or name in subscribed:
- flags = box.getFlags()
- delim = box.getHierarchicalDelimiter()
- resp = (imap4.DontQuoteMe(cmdName),
- map(imap4.DontQuoteMe, flags),
- delim, name.encode('imap4-utf-7'))
- self.sendUntaggedResponse(
- imap4.collapseNestedLists(resp))
- self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
- # -------------------- end isSubscribed patch -----------
-
- # TODO subscribe method had also to be changed to accomodate deferred
- def do_SUBSCRIBE(self, tag, name):
- name = self._parseMbox(name)
-
- def _subscribeCb(_):
- self.sendPositiveResponse(tag, 'Subscribed')
-
- def _subscribeEb(failure):
- m = failure.value
- log.err()
- if failure.check(imap4.MailboxException):
- self.sendNegativeResponse(tag, str(m))
- else:
- self.sendBadResponse(
- tag,
- "Server error encountered while subscribing to mailbox")
-
- d = self.account.subscribe(name)
- d.addCallbacks(_subscribeCb, _subscribeEb)
- return d
-
- auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
- select_SUBSCRIBE = auth_SUBSCRIBE
-
- def do_UNSUBSCRIBE(self, tag, name):
- # unsubscribe method had also to be changed to accomodate
- # deferred
- name = self._parseMbox(name)
-
- def _unsubscribeCb(_):
- self.sendPositiveResponse(tag, 'Unsubscribed')
-
- def _unsubscribeEb(failure):
- m = failure.value
- log.err()
- if failure.check(imap4.MailboxException):
- self.sendNegativeResponse(tag, str(m))
- else:
- self.sendBadResponse(
- tag,
- "Server error encountered while unsubscribing "
- "from mailbox")
-
- d = self.account.unsubscribe(name)
- d.addCallbacks(_unsubscribeCb, _unsubscribeEb)
- return d
-
- auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
- select_UNSUBSCRIBE = auth_UNSUBSCRIBE
-
- def do_RENAME(self, tag, oldname, newname):
- oldname, newname = [self._parseMbox(n) for n in oldname, newname]
- if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
- self.sendNegativeResponse(
- tag,
- 'You cannot rename the inbox, or '
- 'rename another mailbox to inbox.')
- return
-
- def _renameCb(_):
- self.sendPositiveResponse(tag, 'Mailbox renamed')
-
- def _renameEb(failure):
- m = failure.value
- if failure.check(TypeError):
- self.sendBadResponse(tag, 'Invalid command syntax')
- elif failure.check(imap4.MailboxException):
- self.sendNegativeResponse(tag, str(m))
- else:
- log.err()
- self.sendBadResponse(
- tag,
- "Server error encountered while "
- "renaming mailbox")
-
- d = self.account.rename(oldname, newname)
- d.addCallbacks(_renameCb, _renameEb)
- return d
-
- auth_RENAME = (do_RENAME, arg_astring, arg_astring)
- select_RENAME = auth_RENAME
-
- def do_CREATE(self, tag, name):
- name = self._parseMbox(name)
-
- def _createCb(result):
- if result:
- self.sendPositiveResponse(tag, 'Mailbox created')
- else:
- self.sendNegativeResponse(tag, 'Mailbox not created')
-
- def _createEb(failure):
- c = failure.value
- if failure.check(imap4.MailboxException):
- self.sendNegativeResponse(tag, str(c))
- else:
- log.err()
- self.sendBadResponse(
- tag, "Server error encountered while creating mailbox")
-
- d = self.account.create(name)
- d.addCallbacks(_createCb, _createEb)
- return d
-
- auth_CREATE = (do_CREATE, arg_astring)
- select_CREATE = auth_CREATE
-
- def do_DELETE(self, tag, name):
- name = self._parseMbox(name)
- if name.lower() == 'inbox':
- self.sendNegativeResponse(tag, 'You cannot delete the inbox')
- return
-
- def _deleteCb(result):
- self.sendPositiveResponse(tag, 'Mailbox deleted')
-
- def _deleteEb(failure):
- m = failure.value
- if failure.check(imap4.MailboxException):
- self.sendNegativeResponse(tag, str(m))
- else:
- print "SERVER: other error"
- log.err()
- self.sendBadResponse(
- tag,
- "Server error encountered while deleting mailbox")
-
- d = self.account.delete(name)
- d.addCallbacks(_deleteCb, _deleteEb)
- return d
-
- auth_DELETE = (do_DELETE, arg_astring)
- select_DELETE = auth_DELETE
-
- # -----------------------------------------------------------------------
- # Patched just to allow __cbAppend to receive a deferred from messageCount
- # TODO format and send upstream.
- def do_APPEND(self, tag, mailbox, flags, date, message):
- mailbox = self._parseMbox(mailbox)
- maybeDeferred(self.account.select, mailbox).addCallback(
- self._cbAppendGotMailbox, tag, flags, date, message).addErrback(
- self._ebAppendGotMailbox, tag)
-
- def __ebAppend(self, failure, tag):
- self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
-
- def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
- if not mbox:
- self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
- return
-
- d = mbox.addMessage(message, flags, date)
- d.addCallback(self.__cbAppend, tag, mbox)
- d.addErrback(self.__ebAppend, tag)
-
- def _ebAppendGotMailbox(self, failure, tag):
- self.sendBadResponse(
- tag, "Server error encountered while opening mailbox.")
- log.err(failure)
-
- def __cbAppend(self, result, tag, mbox):
-
- # XXX patched ---------------------------------
- def send_response(count):
- self.sendUntaggedResponse('%d EXISTS' % count)
- self.sendPositiveResponse(tag, 'APPEND complete')
-
- d = mbox.getMessageCount()
- d.addCallback(send_response)
- return d
- # XXX patched ---------------------------------
- # -----------------------------------------------------------------------
-
- auth_APPEND = (do_APPEND, arg_astring, imap4.IMAP4Server.opt_plist,
- imap4.IMAP4Server.opt_datetime, arg_literal)
- select_APPEND = auth_APPEND
-
- # Need to override the command table after patching
- # arg_astring and arg_literal, except on the methods that we are already
- # overriding.
-
- # TODO --------------------------------------------
- # Check if we really need to override these
- # methods, or we can monkeypatch.
- # do_DELETE = imap4.IMAP4Server.do_DELETE
- # do_CREATE = imap4.IMAP4Server.do_CREATE
- # do_RENAME = imap4.IMAP4Server.do_RENAME
- # do_SUBSCRIBE = imap4.IMAP4Server.do_SUBSCRIBE
- # do_UNSUBSCRIBE = imap4.IMAP4Server.do_UNSUBSCRIBE
- # do_APPEND = imap4.IMAP4Server.do_APPEND
- # -------------------------------------------------
- do_LOGIN = imap4.IMAP4Server.do_LOGIN
- do_STATUS = imap4.IMAP4Server.do_STATUS
- do_COPY = imap4.IMAP4Server.do_COPY
-
- _selectWork = imap4.IMAP4Server._selectWork
-
- arg_plist = imap4.IMAP4Server.arg_plist
- arg_seqset = imap4.IMAP4Server.arg_seqset
- opt_plist = imap4.IMAP4Server.opt_plist
- opt_datetime = imap4.IMAP4Server.opt_datetime
-
- unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
-
- auth_SELECT = (_selectWork, arg_astring, 1, 'SELECT')
- select_SELECT = auth_SELECT
-
- auth_CREATE = (do_CREATE, arg_astring)
- select_CREATE = auth_CREATE
-
- auth_EXAMINE = (_selectWork, arg_astring, 0, 'EXAMINE')
- select_EXAMINE = auth_EXAMINE
-
- # TODO -----------------------------------------------
- # re-add if we stop overriding DELETE
- # auth_DELETE = (do_DELETE, arg_astring)
- # select_DELETE = auth_DELETE
- # auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
- # arg_literal)
- # select_APPEND = auth_APPEND
-
- # ----------------------------------------------------
-
- auth_RENAME = (do_RENAME, arg_astring, arg_astring)
- select_RENAME = auth_RENAME
-
- auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
- select_SUBSCRIBE = auth_SUBSCRIBE
-
- auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
- select_UNSUBSCRIBE = auth_UNSUBSCRIBE
-
- auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
- select_LIST = auth_LIST
-
- auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
- select_LSUB = auth_LSUB
-
- auth_STATUS = (do_STATUS, arg_astring, arg_plist)
- select_STATUS = auth_STATUS
-
- select_COPY = (do_COPY, arg_seqset, arg_astring)
-
- #############################################################
- # END of Twisted imap4 patch to support LITERAL+ extension
- #############################################################
-
- def authenticateLogin(self, user, passwd):
- result = imap4.IMAP4Server.authenticateLogin(self, user, passwd)
- emit_async(catalog.IMAP_CLIENT_LOGIN, str(user))
- return result