diff options
Diffstat (limited to 'src/leap/mx/mail_receiver.py')
-rw-r--r-- | src/leap/mx/mail_receiver.py | 213 |
1 files changed, 121 insertions, 92 deletions
diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 5875034..8fcadce 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -25,14 +25,20 @@ import uuid as pyuuid import json import email.utils +import socket + +try: + import cchardet as chardet +except ImportError: + import chardet from email import message_from_string from twisted.application.service import Service from twisted.internet import inotify -from twisted.internet.defer import DeferredList from twisted.python import filepath, log +from leap.common.mail import get_email_charset from leap.soledad.common.document import SoledadDocument from leap.soledad.common.crypto import ( EncryptionSchemes, @@ -54,19 +60,20 @@ class MailReceiver(Service): """ Constructor - @param mail_couch_url: URL prefix for the couchdb where mail + :param mail_couch_url: URL prefix for the couchdb where mail should be stored - @type mail_couch_url: str - @param users_cdb: CouchDB instance from where to get the uuid + :type mail_couch_url: str + :param users_cdb: CouchDB instance from where to get the uuid and pubkey for a user - @type users_cdb: ConnectedCouchDB - @param directories: list of directories to monitor - @type directories: list of tuples (path: str, recursive: bool) + :type users_cdb: ConnectedCouchDB + :param directories: list of directories to monitor + :type directories: list of tuples (path: str, recursive: bool) """ # Service doesn't define an __init__ self._mail_couch_url = mail_couch_url self._users_cdb = users_cdb self._directories = directories + self._domain = socket.gethostbyaddr(socket.gethostname())[0] def startService(self): """ @@ -79,56 +86,49 @@ class MailReceiver(Service): mask = inotify.IN_CREATE for directory, recursive in self._directories: - log.msg("Watching %s --- Recursive: %s" % (directory, recursive)) + log.msg("Watching %r --- Recursive: %r" % (directory, recursive)) self.wm.watch(filepath.FilePath(directory), mask, callbacks=[self._process_incoming_email], recursive=recursive) - def _gather_uuid_pubkey(self, results): - if len(results) < 2: - return None, None - - # DeferredList results are structured like this: - # [ (succeeded, pubkey), (succeeded, uuid) ] - # succeeded is a bool value that specifies if the - # corresponding callback succeeded - pubkey_res, uuid_res = results - - pubkey = pubkey_res[1] if pubkey_res[0] else None - uuid = uuid_res[1] if uuid_res[0] else None - - return uuid, pubkey - - def _encrypt_message(self, uuid_pubkey, address, message): + def _encrypt_message(self, pubkey, uuid, message): """ - Given a UUID, a public key, address and a message, it encrypts - the message to that public key. + Given a UUID, a public key and a message, it encrypts the + message to that public key. The address is needed in order to build the OpenPGPKey object. - @param uuid_pubkey: tuple that holds the uuid and the public - key as it is returned by the previous call in the chain - @type uuid_pubkey: tuple (str, str) - @param address: mail address for this message - @type address: str - @param message: message contents - @type message: str + :param uuid_pubkey: tuple that holds the uuid and the public + key as it is returned by the previous call in the + chain + :type uuid_pubkey: tuple (str, str) + :param message: message contents + :type message: str - @return: uuid, doc to sync with Soledad - @rtype: tuple(str, SoledadDocument) + :return: uuid, doc to sync with Soledad or None, None if + something went wrong. + :rtype: tuple(str, SoledadDocument) """ - uuid, pubkey = uuid_pubkey + if uuid is None or pubkey is None or len(pubkey) == 0: + log.msg("_encrypt_message: Something went wrong, here's all " + "I know: %r | %r" % (uuid, pubkey)) + return None, None + log.msg("Encrypting message to %s's pubkey" % (uuid,)) - log.msg("Pubkey: %s" % (pubkey,)) doc = SoledadDocument(doc_id=str(pyuuid.uuid4())) + encoding = get_email_charset(message, default=None) + if encoding is None: + result = chardet.detect(message) + encoding = result["encoding"] + data = {'incoming': True, 'content': message} if pubkey is None or len(pubkey) == 0: doc.content = { self.INCOMING_KEY: True, ENC_SCHEME_KEY: EncryptionSchemes.NONE, - ENC_JSON_KEY: json.dumps(data) + ENC_JSON_KEY: json.dumps(data, encoding=encoding) } return uuid, doc @@ -136,13 +136,15 @@ class MailReceiver(Service): with openpgp.TempGPGWrapper(gpgbinary='/usr/bin/gpg') as gpg: gpg.import_keys(pubkey) key = gpg.list_keys().pop() - openpgp_key = openpgp._build_key_from_gpg(address, key, pubkey) + # We don't care about the actual address, so we use a + # dummy one, we just care about the import of the pubkey + openpgp_key = openpgp._build_key_from_gpg("dummy@mail.com", key, pubkey) doc.content = { self.INCOMING_KEY: True, ENC_SCHEME_KEY: EncryptionSchemes.PUBKEY, ENC_JSON_KEY: str(gpg.encrypt( - json.dumps(data), + json.dumps(data, encoding=encoding), openpgp_key.fingerprint, symmetric=False)) } @@ -153,20 +155,22 @@ class MailReceiver(Service): """ Given a UUID and a SoledadDocument, it saves it directly in the couchdb that serves as a backend for Soledad, in a db - accessible to the recipient of the mail + accessible to the recipient of the mail. - @param uuid_doc: tuple that holds the UUID and SoledadDocument - @type uuid_doc: tuple(str, SoledadDocument) + :param uuid_doc: tuple that holds the UUID and SoledadDocument + :type uuid_doc: tuple(str, SoledadDocument) - @return: True if it's ok to remove the message, False - otherwise - @rtype: bool + :return: True if it's ok to remove the message, False + otherwise + :rtype: bool """ uuid, doc = uuid_doc - log.msg("Exporting message for %s" % (uuid,)) + if uuid is None or doc is None: + log.msg("_export_message: Something went wrong, here's all " + "I know: %r | %r" % (uuid, doc)) + return False - if uuid is None: - uuid = 0 + log.msg("Exporting message for %s" % (uuid,)) db = CouchDatabase(self._mail_couch_url, "user-%s" % (uuid,)) db.put_doc(doc) @@ -177,58 +181,83 @@ class MailReceiver(Service): def _conditional_remove(self, do_remove, filepath): """ - Removes the message if do_remove is True + Removes the message if do_remove is True. - @param do_remove: True if the message should be removed, False - otherwise - @type do_remove: bool - @param filepath: path to the mail - @type filepath: twisted.python.filepath.FilePath + :param do_remove: True if the message should be removed, False + otherwise + :type do_remove: bool + :param filepath: path to the mail + :type filepath: twisted.python.filepath.FilePath """ if do_remove: # remove the original mail try: - log.msg("Removing %s" % (filepath.path,)) + log.msg("Removing %r" % (filepath.path,)) filepath.remove() log.msg("Done removing") - except: + except Exception: log.err() + else: + log.msg("Not removing %r" % (filepath.path,)) + + def _get_owner(self, mail): + """ + Given an email, returns the uuid of the owner. + + :param mail: mail to analyze + :type mail: email.message.Message + + :returns: uuid + :rtype: str or None + """ + uuid = None + + delivereds = mail.get_all("Delivered-To") + for to in delivereds: + name, addr = email.utils.parseaddr(to) + parts = addr.split("@") + if len(parts) > 1 and parts[1] == self._domain: + uuid = parts[0] + break + + return uuid def _process_incoming_email(self, otherself, filepath, mask): """ - Callback that processes incoming email - - @param otherself: Watch object for the current callback from - inotify - @type otherself: twisted.internet.inotify._Watch - @param filepath: Path of the file that changed - @type filepath: twisted.python.filepath.FilePath - @param mask: identifier for the type of change that triggered - this callback - @type mask: int + Callback that processes incoming email. + + :param otherself: Watch object for the current callback from + inotify. + :type otherself: twisted.internet.inotify._Watch + :param filepath: Path of the file that changed + :type filepath: twisted.python.filepath.FilePath + :param mask: identifier for the type of change that triggered + this callback + :type mask: int """ - if os.path.split(filepath.dirname())[-1] == "new": - log.msg("Processing new mail at %s" % (filepath.path,)) - with filepath.open("r") as f: - mail_data = f.read() - mail = message_from_string(mail_data) - owner = mail["To"] - if owner is None: # default to Delivered-To - owner = mail["Delivered-To"] - if owner is None: - log.err("Malformed mail, neither To: nor " - "Delivered-To: field") - log.msg("Mail owner: %s" % (owner,)) - - owner = email.utils.parseaddr(owner)[1] - log.msg("%s received a new mail" % (owner,)) - dpubk = self._users_cdb.getPubKey(owner) - duuid = self._users_cdb.queryByAddress(owner) - d = DeferredList([dpubk, duuid]) - d.addCallbacks(self._gather_uuid_pubkey, log.err) - d.addCallbacks(self._encrypt_message, log.err, - (owner, mail_data)) - d.addCallbacks(self._export_message, log.err) - d.addCallbacks(self._conditional_remove, log.err, - (filepath,)) - d.addErrback(log.err) + try: + if os.path.split(filepath.dirname())[-1] == "new": + log.msg("Processing new mail at %r" % (filepath.path,)) + with filepath.open("r") as f: + mail_data = f.read() + mail = message_from_string(mail_data) + uuid = self._get_owner(mail) + if uuid is None: + log.msg("Don't know how to deliver mail %r, skipping..." % + filepath.path) + return + log.msg("Mail owner: %s" % (uuid,)) + + if uuid is None: + log.msg("BUG: There was no uuid!") + return + + d = self._users_cdb.getPubKey(uuid) + d.addCallbacks(self._encrypt_message, log.err, + (uuid, mail_data)) + d.addCallbacks(self._export_message, log.err) + d.addCallbacks(self._conditional_remove, log.err, + (filepath,)) + d.addErrback(log.err) + except Exception: + log.err() |