From 82a2b6d45751d671c593c15144c908b665e955bb Mon Sep 17 00:00:00 2001 From: Ruben Pollan Date: Mon, 22 Dec 2014 09:04:29 -0600 Subject: Use gpg key directly _build_key_from_gpg in keymanager has changed --- src/leap/mx/mail_receiver.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index dd76f08..d3f3c68 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -224,10 +224,6 @@ class MailReceiver(Service): with openpgp.TempGPGWrapper(gpgbinary='/usr/bin/gpg') as gpg: gpg.import_keys(pubkey) key = gpg.list_keys().pop() - # 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) # add X-Leap-Provenance header if message is not encrypted if message.get_content_type() != 'multipart/encrypted' and \ @@ -236,7 +232,7 @@ class MailReceiver(Service): message.add_header( 'X-Leap-Provenance', email.utils.formatdate(), - pubkey=openpgp_key.key_id) + pubkey=key["keyid"]) data = {'incoming': True, 'content': message.as_string()} doc.content = { self.INCOMING_KEY: True, @@ -244,7 +240,7 @@ class MailReceiver(Service): ENC_SCHEME_KEY: EncryptionSchemes.PUBKEY, ENC_JSON_KEY: str(gpg.encrypt( json.dumps(data, ensure_ascii=False), - openpgp_key.fingerprint, + key["fingerprint"], symmetric=False)) } -- cgit v1.2.3 From e660453542889fc8285217f008803c9701f1b778 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 5 Feb 2015 10:04:12 -0200 Subject: Process unprocessed mail when MX starts (closes #2591). --- src/leap/mx/mail_receiver.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index d3f3c68..2d91fda 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -134,6 +134,7 @@ class MailReceiver(Service): INCOMING_KEY = 'incoming' ERROR_DECRYPTING_KEY = "errdecr" + PROCESS_SKIPPED_INTERVAL = 60 * 30 # every half an hour def __init__(self, mail_couch_url, users_cdb, directories, bounce_from, bounce_subject): @@ -171,17 +172,18 @@ class MailReceiver(Service): self.wm = inotify.INotify() self.wm.startReading() + # watch the mail directory for new files and process incoming mail mask = inotify.IN_CREATE - for directory, recursive in self._directories: log.msg("Watching %r --- Recursive: %r" % (directory, recursive)) self.wm.watch(filepath.FilePath(directory), mask, callbacks=[self._process_incoming_email], recursive=recursive) + # schedule a periodic task to process skipped mail, but also run it + # immediatelly self._lcall = task.LoopingCall(self._process_skipped) - # Run once every half an hour, but don't start right now - self._lcall.start(interval=60*30, now=False) + self._lcall.start(interval=self.PROCESS_SKIPPED_INTERVAL, now=True) def _encrypt_message(self, pubkey, message): """ @@ -380,7 +382,7 @@ class MailReceiver(Service): self._processing_skipped = True try: log.msg("Starting processing skipped mail...") - log.msg("-"*50) + log.msg("-" * 50) for directory, recursive in self._directories: for root, dirs, files in os.walk(directory): @@ -390,7 +392,7 @@ class MailReceiver(Service): fpath = filepath.FilePath(fullpath) yield self._step_process_mail_backend(fpath) except Exception: - log.msg("Error processing skipped mail: %r" % \ + log.msg("Error processing skipped mail: %r" % (fullpath,)) log.err() if not recursive: @@ -401,7 +403,7 @@ class MailReceiver(Service): finally: self._processing_skipped = False - log.msg("+"*50) + log.msg("+" * 50) log.msg("Done processing skipped mail") @defer.inlineCallbacks @@ -463,9 +465,10 @@ class MailReceiver(Service): """ try: while self._processing_skipped: - log.msg("Waiting for the process of skipped mail to be done...") + log.msg("Waiting for the process of skipped mail to be " + "done...") yield self.sleep(10) # NO-OP - if os.path.split(filepath.dirname())[-1] == "new": + if os.path.split(filepath.dirname())[-1] == "new": yield self._step_process_mail_backend(filepath) except Exception as e: log.msg("Something went wrong while processing {0!r}: {1!r}" -- cgit v1.2.3 From 91404f56d200ba01130ff208169d45f615f8a094 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Thu, 5 Feb 2015 10:46:57 -0400 Subject: Add ability to trigger processing on SIGUSR1 (Related: #2591) --- src/leap/mx/mail_receiver.py | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 2d91fda..5fa3863 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -15,9 +15,13 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - """ -MailReceiver service definition +MailReceiver service definition. This service monitors the incoming mail and +process it. + +The MailReceiver service is configured to process incoming any skipped mail +every half an hour, and also on service start. It can be forced to start +processing by sending SIGUSR1 to the process. If there's a user facing problem when processing an email, it will be bounced back to the sender. @@ -29,9 +33,9 @@ User facing problems could be: Any other problem is a bug, which will be logged. Until the bug is fixed, the email will stay in there waiting. """ - import os import uuid as pyuuid +import signal import json import email.utils @@ -43,10 +47,12 @@ from email.MIMEText import MIMEText from email.Utils import formatdate from email.header import decode_header -from twisted.application.service import Service +from twisted.application.service import Service, IService from twisted.internet import inotify, defer, task, reactor from twisted.python import filepath, log +from zope.interface import implements + from leap.soledad.common.crypto import ( EncryptionSchemes, ENC_JSON_KEY, @@ -129,8 +135,9 @@ def async_check_output(args, msg): class MailReceiver(Service): """ - Service that monitors incoming email and processes it + Service that monitors incoming email and processes it. """ + implements(IService) INCOMING_KEY = 'incoming' ERROR_DECRYPTING_KEY = "errdecr" @@ -144,13 +151,17 @@ class MailReceiver(Service): :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 - and pubkey for a user + 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) + :param bounce_from: Email address of the bouncer :type bounce_from: str + :param bounce_subject: Subject line used in the bounced mail :type bounce_subject: str """ @@ -164,6 +175,11 @@ class MailReceiver(Service): self._bounce_from = bounce_from self._bounce_subject = bounce_subject + def _signal_handler(sig_num, stack_frame): + self._process_skipped() + + signal.signal(signal.SIGUSR1, _signal_handler) + def startService(self): """ Starts the MailReceiver service -- cgit v1.2.3 From de95cc2fc21756c1b021723f49e846298e3b38a2 Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 6 Feb 2015 16:48:13 -0200 Subject: Retry watching dirs if failed for some reason (closes #6687). --- src/leap/mx/mail_receiver.py | 68 +++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 16 deletions(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 5fa3863..630c982 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -143,6 +143,12 @@ class MailReceiver(Service): ERROR_DECRYPTING_KEY = "errdecr" PROCESS_SKIPPED_INTERVAL = 60 * 30 # every half an hour + """ + If there's a failure when trying to watch a directory for file creation, + the service will schedule a retry delayed by the following amount of time. + """ + RETRY_DIR_WATCH_DELAY = 60 * 5 # 5 minutes + def __init__(self, mail_couch_url, users_cdb, directories, bounce_from, bounce_subject): """ @@ -165,20 +171,15 @@ class MailReceiver(Service): :param bounce_subject: Subject line used in the bounced mail :type bounce_subject: str """ - # Service doesn't define an __init__ + # IService 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] - self._processing_skipped = False - self._bounce_from = bounce_from self._bounce_subject = bounce_subject - def _signal_handler(sig_num, stack_frame): - self._process_skipped() - - signal.signal(signal.SIGUSR1, _signal_handler) + self._domain = socket.gethostbyaddr(socket.gethostname())[0] + self._processing_skipped = False def startService(self): """ @@ -188,19 +189,55 @@ class MailReceiver(Service): self.wm = inotify.INotify() self.wm.startReading() - # watch the mail directory for new files and process incoming mail - mask = inotify.IN_CREATE + # watch mail directories for new files to trigger processing of + # incoming mail for directory, recursive in self._directories: - log.msg("Watching %r --- Recursive: %r" % (directory, recursive)) - self.wm.watch(filepath.FilePath(directory), mask, - callbacks=[self._process_incoming_email], - recursive=recursive) + self._start_watching_dir(directory, recursive) - # schedule a periodic task to process skipped mail, but also run it + # schedule a periodic task to process skipped mail, and also run it # immediatelly self._lcall = task.LoopingCall(self._process_skipped) self._lcall.start(interval=self.PROCESS_SKIPPED_INTERVAL, now=True) + # catch SIGUSR1 to trigger processing of skipped mail + signal.signal( + signal.SIGUSR1, + lambda *_: self._process_skipped()) + + def stopService(self): + """ + Stop the MailReceiver service + """ + self.wm.stopReading() + self._lcall.stop() + + def _start_watching_dir(self, dirname, recursive): + """ + Start watching a directory to trigger processing of newly created + files. + + Will also add a delayed call to retry when failed for some reason. + """ + directory = filepath.FilePath(dirname) + try: + if not directory.isdir(): + raise OSError("Not a directory: '%s'" % directory.path) + self.wm.watch( + directory, + inotify.IN_CREATE, + callbacks=[self._process_incoming_email], + recursive=recursive) + log.msg("Watching %r --- Recursive: %r" % (directory, recursive)) + except Exception as e: + log.msg( + "Failed adding watch to %s, will try again in %s seconds: %s" + % (directory, self.RETRY_DIR_WATCH_DELAY, e)) + reactor.callLater( + self.RETRY_DIR_WATCH_DELAY, + self._start_watching_dir, + dirname, + recursive) + def _encrypt_message(self, pubkey, message): """ Given a public key and a message, it encrypts the message to @@ -380,7 +417,6 @@ class MailReceiver(Service): :rtype: twisted.internet.defer.Deferred """ - from twisted.internet import reactor d = defer.Deferred() reactor.callLater(secs, d.callback, None) return d -- cgit v1.2.3 From 938cada00298243f0cf51c9bfd460ecb16938b57 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 24 Mar 2015 15:07:51 -0300 Subject: [bug] correctly return async bouncer deferred --- src/leap/mx/mail_receiver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 630c982..7856594 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -96,6 +96,10 @@ class BouncerSubprocessProtocol(protocol.ProcessProtocol): self._errBuffer = "" self._d = None + @property + def deferred(self): + return self._d + def connectionMade(self): self._d = defer.Deferred() @@ -130,7 +134,7 @@ def async_check_output(args, msg): """ pprotocol = BouncerSubprocessProtocol(msg) reactor.spawnProcess(pprotocol, args[0], args) - return pprotocol.d + return pprotocol.deferred class MailReceiver(Service): -- cgit v1.2.3 From 5a45acd3486f4e7f830647953731353cda916d51 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 24 Mar 2015 15:09:18 -0300 Subject: [feat] reject incoming mail if no pgp key found Implement a PGP key lookup in the postfix smtp recipient restriction and virtual alias mapping levels. If no PGP key is found, then the address is rejected with a temporary error. Closes: #6795 --- src/leap/mx/alias_resolver.py | 45 ++++++++++++++++------------------- src/leap/mx/check_recipient_access.py | 5 +++- 2 files changed, 25 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index 45a3ed2..4247b57 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -28,10 +28,11 @@ TODO: try: # TODO: we should probably use the system alias somehow - # from twisted.mail import alias + # from twisted.mail import alias from twisted.protocols import postfix from twisted.python import log from twisted.internet import defer + from twisted.internet.protocol import ServerFactory except ImportError: print "This software requires Twisted. Please see the README file" print "for instructions on getting required dependencies." @@ -39,19 +40,20 @@ except ImportError: class LEAPPostFixTCPMapserver(postfix.PostfixTCPMapServer): def _cbGot(self, value): - if value is None: + uuid, pubkey = value + if uuid is None: self.sendCode(500, postfix.quote("NOT FOUND SRY")) + elif pubkey is None: + self.sendCode(400, postfix.quote("4.7.13 USER ACCOUNT DISABLED")) else: self.sendCode(200, postfix.quote(value)) -class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory): +class AliasResolverFactory(ServerFactory): protocol = LEAPPostFixTCPMapserver - def __init__(self, couchdb, *args, **kwargs): - postfix.PostfixTCPMapDeferringDictServerFactory.__init__( - self, *args, **kwargs) + def __init__(self, couchdb): self._cdb = couchdb def _to_str(self, result): @@ -64,14 +66,14 @@ class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory): log.msg("Result not found") return result - def spit_result(self, result): - """ - Formats the return codes in a postfix friendly format. - """ - if result is None: - return None - else: - return defer.succeed(result) + def _getPubKey(self, uuid): + if uuid is None: + return defer.succeed([None, None]) + d = defer.gatherResults([ + self._to_str(uuid), + self._cdb.getPubKey(uuid), + ]) + return d def get(self, key): """ @@ -79,13 +81,8 @@ class AliasResolverFactory(postfix.PostfixTCPMapDeferringDictServerFactory): At some point we will have to consider the domain part too. """ - try: - log.msg("Query key: %s" % (key,)) - d = self._cdb.queryByAddress(key) - - d.addCallback(self._to_str) - d.addCallback(self.spit_result) - d.addErrback(log.err) - return d - except Exception as e: - log.err('exception in get: %r' % e) + log.msg("Query key: %s" % (key,)) + d = self._cdb.queryByAddress(key) + d.addCallback(self._getPubKey) + d.addErrback(log.err) + return d diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index b80ccfd..d4ae339 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -32,8 +32,11 @@ class LEAPPostFixTCPMapserverAccess(postfix.PostfixTCPMapServer): # For more info, see: # http://www.postfix.org/tcp_table.5.html # http://www.postfix.org/access.5.html - if value is None: + uuid, pubkey = value + if uuid is None: self.sendCode(500, postfix.quote("REJECT")) + elif pubkey is None: + self.sendCode(400, postfix.quote("4.7.13 USER ACCOUNT DISABLED")) else: self.sendCode(200, postfix.quote("OK")) -- cgit v1.2.3 From b5309dc5910f35f5320c649be2ce2c6147030b39 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 25 Mar 2015 15:47:28 -0300 Subject: [refactor] separate tcp map server code Separate the common tcp map server code, used for both alias resolver and recipient access checker, to its own file. --- src/leap/mx/alias_resolver.py | 84 ++++++++++++++--------------------- src/leap/mx/check_recipient_access.py | 40 ++++++++++++----- src/leap/mx/tcp_map.py | 76 +++++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 60 deletions(-) create mode 100644 src/leap/mx/tcp_map.py (limited to 'src') diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index 4247b57..9206ffb 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -24,65 +24,49 @@ Test this with postmap -v -q "foo" tcp:localhost:4242 TODO: o Look into using twisted.protocols.postfix.policies classes for controlling concurrent connections and throttling resource consumption. + o We should probably use twisted.mail.alias somehow. """ -try: - # TODO: we should probably use the system alias somehow - # from twisted.mail import alias - from twisted.protocols import postfix - from twisted.python import log - from twisted.internet import defer - from twisted.internet.protocol import ServerFactory -except ImportError: - print "This software requires Twisted. Please see the README file" - print "for instructions on getting required dependencies." +from twisted.protocols import postfix -class LEAPPostFixTCPMapserver(postfix.PostfixTCPMapServer): - def _cbGot(self, value): - uuid, pubkey = value - if uuid is None: - self.sendCode(500, postfix.quote("NOT FOUND SRY")) - elif pubkey is None: - self.sendCode(400, postfix.quote("4.7.13 USER ACCOUNT DISABLED")) - else: - self.sendCode(200, postfix.quote(value)) - +from leap.mx.tcp_map import LEAPostfixTCPMapServerFactory +from leap.mx.tcp_map import TCP_MAP_CODE_SUCCESS +from leap.mx.tcp_map import TCP_MAP_CODE_TEMPORARY_FAILURE +from leap.mx.tcp_map import TCP_MAP_CODE_PERMANENT_FAILURE -class AliasResolverFactory(ServerFactory): - protocol = LEAPPostFixTCPMapserver +class LEAPPostfixTCPMapAliasServer(postfix.PostfixTCPMapServer): + """ + A postfix tcp map alias resolver server. + """ - def __init__(self, couchdb): - self._cdb = couchdb - - def _to_str(self, result): - """ - Properly encodes the result string if any. + def _cbGot(self, value): """ - if isinstance(result, unicode): - result = result.encode("utf8") - if result is None: - log.msg("Result not found") - return result + Return a code and message depending on the result of the factory's + get(). - def _getPubKey(self, uuid): + :param value: The uuid and public key. + :type value: list + """ + uuid, pubkey = value if uuid is None: - return defer.succeed([None, None]) - d = defer.gatherResults([ - self._to_str(uuid), - self._cdb.getPubKey(uuid), - ]) - return d + self.sendCode( + TCP_MAP_CODE_PERMANENT_FAILURE, + postfix.quote("NOT FOUND SRY")) + elif pubkey is None: + self.sendCode( + TCP_MAP_CODE_TEMPORARY_FAILURE, + postfix.quote("4.7.13 USER ACCOUNT DISABLED")) + else: + self.sendCode( + TCP_MAP_CODE_SUCCESS, + postfix.quote(uuid)) - def get(self, key): - """ - Looks up the passed key, but only up to the username id of the key. - At some point we will have to consider the domain part too. - """ - log.msg("Query key: %s" % (key,)) - d = self._cdb.queryByAddress(key) - d.addCallback(self._getPubKey) - d.addErrback(log.err) - return d +class AliasResolverFactory(LEAPostfixTCPMapServerFactory): + """ + A factory for postfix tcp map alias resolver servers. + """ + + protocol = LEAPPostfixTCPMapAliasServer diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index d4ae339..cf172c7 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -24,22 +24,42 @@ Test this with postmap -v -q "foo" tcp:localhost:2244 from twisted.protocols import postfix -from leap.mx.alias_resolver import AliasResolverFactory +from leap.mx.tcp_map import LEAPPostfixTCPMapServerFactory +from leap.mx.tcp_map import TCP_MAP_CODE_SUCCESS +from leap.mx.tcp_map import TCP_MAP_CODE_TEMPORARY_FAILURE +from leap.mx.tcp_map import TCP_MAP_CODE_PERMANENT_FAILURE -class LEAPPostFixTCPMapserverAccess(postfix.PostfixTCPMapServer): +class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): + """ + A postfix tcp map recipient access checker server. + """ + def _cbGot(self, value): - # For more info, see: - # http://www.postfix.org/tcp_table.5.html - # http://www.postfix.org/access.5.html + """ + Return a code and message depending on the result of the factory's + get(). + + For more info, see: http://www.postfix.org/access.5.html + + :param value: The uuid and public key. + :type value: list + """ uuid, pubkey = value if uuid is None: - self.sendCode(500, postfix.quote("REJECT")) + self.sendCode( + TCP_MAP_CODE_PERMANENT_FAILURE, + postfix.quote("REJECT")) elif pubkey is None: - self.sendCode(400, postfix.quote("4.7.13 USER ACCOUNT DISABLED")) + self.sendCode( + TCP_MAP_CODE_TEMPORARY_FAILURE, + postfix.quote("4.7.13 USER ACCOUNT DISABLED")) else: - self.sendCode(200, postfix.quote("OK")) + self.sendCode( + TCP_MAP_CODE_SUCCESS, + postfix.quote("OK")) + +class CheckRecipientAccessFactory(LEAPPostfixTCPMapServerFactory): -class CheckRecipientAccessFactory(AliasResolverFactory): - protocol = LEAPPostFixTCPMapserverAccess + protocol = LEAPPostFixTCPMapAccessServer diff --git a/src/leap/mx/tcp_map.py b/src/leap/mx/tcp_map.py new file mode 100644 index 0000000..b7066ff --- /dev/null +++ b/src/leap/mx/tcp_map.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# tcpmap.py +# Copyright (C) 2015 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 . + + +from twisted.python import log +from twisted.internet import defer +from twisted.internet.protocol import ServerFactory + + +# For info on codes, see: http://www.postfix.org/tcp_table.5.html +TCP_MAP_CODE_SUCCESS = 200 +TCP_MAP_CODE_TEMPORARY_FAILURE = 400 +TCP_MAP_CODE_PERMANENT_FAILURE = 500 + + +class LEAPPostfixTCPMapServerFactory(ServerFactory): + """ + A factory for postfix tcp map servers. + """ + + def __init__(self, couchdb): + """ + Initialize the factory. + + :param couchdb: A CouchDB client. + :type couchdb: leap.mx.couchdbhelper.ConnectedCouchDB + """ + self._cdb = couchdb + + def _getPubKey(self, uuid): + """ + Look up PGP public key based on user uid. + + :param uuid: The user uid. + :type uuid: str + + :return: A deferred that is fired with the uuid and the public key, if + available. + :rtype: DeferredList + """ + if uuid is None: + return defer.succeed([None, None]) + return defer.gatherResults([ + defer.succeed(uuid), + self._cdb.getPubKey(uuid), + ]) + + def get(self, key): + """ + Look up uuid based on key, only up to the username id of the key. + + At some point we will have to consider the domain part too. + + :param key: The lookup key. + :type key: str + """ + log.msg("Query key: %s" % (key,)) + d = self._cdb.queryByAddress(key) + d.addCallback(self._getPubKey) + d.addErrback(log.err) + return d -- cgit v1.2.3 From 45adb4d6cfdb8b9ed11e3efc398d00ec6dbdc0b0 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 26 Mar 2015 15:25:50 -0300 Subject: [bug] limit pgp key lookup to access check server In order to minimize the number of couchdb queries and the number of mx lookups in case of junk mail this commit restricts the pgp key lookup to the access check server (and removes it from the alias server). Closes: #6795. --- src/leap/mx/alias_resolver.py | 14 ++++-------- src/leap/mx/check_recipient_access.py | 43 +++++++++++++++++++++++++++++++++++ src/leap/mx/tcp_map.py | 20 ---------------- 3 files changed, 47 insertions(+), 30 deletions(-) (limited to 'src') diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index 9206ffb..a139dd0 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -30,9 +30,8 @@ TODO: from twisted.protocols import postfix -from leap.mx.tcp_map import LEAPostfixTCPMapServerFactory +from leap.mx.tcp_map import LEAPPostfixTCPMapServerFactory from leap.mx.tcp_map import TCP_MAP_CODE_SUCCESS -from leap.mx.tcp_map import TCP_MAP_CODE_TEMPORARY_FAILURE from leap.mx.tcp_map import TCP_MAP_CODE_PERMANENT_FAILURE @@ -41,30 +40,25 @@ class LEAPPostfixTCPMapAliasServer(postfix.PostfixTCPMapServer): A postfix tcp map alias resolver server. """ - def _cbGot(self, value): + def _cbGot(self, uuid): """ Return a code and message depending on the result of the factory's get(). - :param value: The uuid and public key. + :param value: The uuid. :type value: list """ - uuid, pubkey = value if uuid is None: self.sendCode( TCP_MAP_CODE_PERMANENT_FAILURE, postfix.quote("NOT FOUND SRY")) - elif pubkey is None: - self.sendCode( - TCP_MAP_CODE_TEMPORARY_FAILURE, - postfix.quote("4.7.13 USER ACCOUNT DISABLED")) else: self.sendCode( TCP_MAP_CODE_SUCCESS, postfix.quote(uuid)) -class AliasResolverFactory(LEAPostfixTCPMapServerFactory): +class AliasResolverFactory(LEAPPostfixTCPMapServerFactory): """ A factory for postfix tcp map alias resolver servers. """ diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index cf172c7..0977564 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -23,6 +23,7 @@ Test this with postmap -v -q "foo" tcp:localhost:2244 """ from twisted.protocols import postfix +from twisted.internet import defer from leap.mx.tcp_map import LEAPPostfixTCPMapServerFactory from leap.mx.tcp_map import TCP_MAP_CODE_SUCCESS @@ -33,6 +34,10 @@ from leap.mx.tcp_map import TCP_MAP_CODE_PERMANENT_FAILURE class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): """ A postfix tcp map recipient access checker server. + + The server potentially receives the uuid and a PGP key for the user, which + are looked up by the factory, and will return a permanent or a temporary + failure in case either the user or the key don't exist, respectivelly. """ def _cbGot(self, value): @@ -61,5 +66,43 @@ class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): class CheckRecipientAccessFactory(LEAPPostfixTCPMapServerFactory): + """ + A factory for the recipient access checker. + + When queried, the factory looks up the user's uuid and a PGP key for that + user and returns the result to the server's _cbGot() method. + """ protocol = LEAPPostFixTCPMapAccessServer + + def _getPubKey(self, uuid): + """ + Look up PGP public key based on user uid. + + :param uuid: The user uid. + :type uuid: str + + :return: A deferred that is fired with the uuid and the public key, if + available. + :rtype: DeferredList + """ + if uuid is None: + return defer.succeed([None, None]) + # properly encode uuid, otherwise twisted complains when replying + if isinstance(uuid, unicode): + uuid = uuid.encode("utf8") + return defer.gatherResults([ + defer.succeed(uuid), + self._cdb.getPubKey(uuid), + ]) + + def get(self, key): + """ + Look up uuid and PGP public key based on key. + + :param key: The lookup key. + :type key: str + """ + d = LEAPPostfixTCPMapServerFactory.get(self, key) + d.addCallback(self._getPubKey) + return d diff --git a/src/leap/mx/tcp_map.py b/src/leap/mx/tcp_map.py index b7066ff..b62441f 100644 --- a/src/leap/mx/tcp_map.py +++ b/src/leap/mx/tcp_map.py @@ -18,7 +18,6 @@ from twisted.python import log -from twisted.internet import defer from twisted.internet.protocol import ServerFactory @@ -42,24 +41,6 @@ class LEAPPostfixTCPMapServerFactory(ServerFactory): """ self._cdb = couchdb - def _getPubKey(self, uuid): - """ - Look up PGP public key based on user uid. - - :param uuid: The user uid. - :type uuid: str - - :return: A deferred that is fired with the uuid and the public key, if - available. - :rtype: DeferredList - """ - if uuid is None: - return defer.succeed([None, None]) - return defer.gatherResults([ - defer.succeed(uuid), - self._cdb.getPubKey(uuid), - ]) - def get(self, key): """ Look up uuid based on key, only up to the username id of the key. @@ -71,6 +52,5 @@ class LEAPPostfixTCPMapServerFactory(ServerFactory): """ log.msg("Query key: %s" % (key,)) d = self._cdb.queryByAddress(key) - d.addCallback(self._getPubKey) d.addErrback(log.err) return d -- cgit v1.2.3 From b0ef529cc882a96903597fb5279919969fa286c3 Mon Sep 17 00:00:00 2001 From: drebs Date: Thu, 9 Apr 2015 17:18:37 -0300 Subject: [refactor] use couch reduced views for lookups The way uuid and pgp key were being queried by means of couch views was not efficient because they weren't using the reduce function and were filtering the views results in the python code. Also, the uuid is not actually needed to find out either if the address exists or if there's a pgp public key for that address. This commit refactors the couch helper to make use of the reduce functions in queried views and to get rid of the intermediate uuid querying. --- src/leap/mx/alias_resolver.py | 10 +-- src/leap/mx/check_recipient_access.py | 25 +++--- src/leap/mx/couchdbhelper.py | 139 ++++++++-------------------------- src/leap/mx/tcp_map.py | 4 +- 4 files changed, 47 insertions(+), 131 deletions(-) (limited to 'src') diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index a139dd0..dd631c8 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -40,22 +40,22 @@ class LEAPPostfixTCPMapAliasServer(postfix.PostfixTCPMapServer): A postfix tcp map alias resolver server. """ - def _cbGot(self, uuid): + def _cbGot(self, address): """ Return a code and message depending on the result of the factory's get(). - :param value: The uuid. - :type value: list + :param address: The address returned by the factory. + :type address: str """ - if uuid is None: + if address is None: self.sendCode( TCP_MAP_CODE_PERMANENT_FAILURE, postfix.quote("NOT FOUND SRY")) else: self.sendCode( TCP_MAP_CODE_SUCCESS, - postfix.quote(uuid)) + postfix.quote(address)) class AliasResolverFactory(LEAPPostfixTCPMapServerFactory): diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index 0977564..3b61fe8 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -50,8 +50,8 @@ class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): :param value: The uuid and public key. :type value: list """ - uuid, pubkey = value - if uuid is None: + address, pubkey = value + if address is None: self.sendCode( TCP_MAP_CODE_PERMANENT_FAILURE, postfix.quote("REJECT")) @@ -75,25 +75,22 @@ class CheckRecipientAccessFactory(LEAPPostfixTCPMapServerFactory): protocol = LEAPPostFixTCPMapAccessServer - def _getPubKey(self, uuid): + def _getPubKey(self, address): """ - Look up PGP public key based on user uid. + Look up PGP public key based on email address. - :param uuid: The user uid. - :type uuid: str + :param address: The email address. + :type address: str - :return: A deferred that is fired with the uuid and the public key, if - available. + :return: A deferred that is fired with the address and the public key, if + each of them exists. :rtype: DeferredList """ - if uuid is None: + if not address: return defer.succeed([None, None]) - # properly encode uuid, otherwise twisted complains when replying - if isinstance(uuid, unicode): - uuid = uuid.encode("utf8") return defer.gatherResults([ - defer.succeed(uuid), - self._cdb.getPubKey(uuid), + defer.succeed(address), + self._cdb.getPubKey(address), ]) def get(self, key): diff --git a/src/leap/mx/couchdbhelper.py b/src/leap/mx/couchdbhelper.py index f20f1dd..7bcb5aa 100644 --- a/src/leap/mx/couchdbhelper.py +++ b/src/leap/mx/couchdbhelper.py @@ -15,24 +15,15 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . + """ Classes for working with CouchDB or BigCouch instances which store email alias maps, user UUIDs, and GPG keyIDs. """ -from functools import partial - -try: - from paisley import client -except ImportError: - print "This software requires paisley. Please see the README file" - print "for instructions on getting required dependencies." -try: - from twisted.python import log -except ImportError: - print "This software requires Twisted. Please see the README file" - print "for instructions on getting required dependencies." +from paisley import client +from twisted.python import log class ConnectedCouchDB(client.CouchDB): @@ -66,24 +57,8 @@ class ConnectedCouchDB(client.CouchDB): username=username, password=password, *args, **kwargs) - self._cache = {} - if dbName is None: - databases = self.listDB() - databases.addCallback(self._print_databases) - - def _print_databases(self, data): - """ - Callback for listDB that prints the available databases - - :param data: response from the listDB command - :type data: array - """ - log.msg("Available databases:") - for database in data: - log.msg(" * %s" % (database,)) - def createDB(self, dbName): """ Overrides ``paisley.client.CouchDB.createDB``. @@ -100,106 +75,52 @@ class ConnectedCouchDB(client.CouchDB): """ Check to see if a particular email or alias exists. - :param alias: A string representing the email or alias to check. - :type alias: str + :param address: A string representing the email or alias to check. + :type address: str :return: a deferred for this query :rtype twisted.defer.Deferred """ - assert isinstance(address, (str, unicode)), "Email or alias queries must be string" - # TODO: Cache results - d = self.openView(docId="Identity", viewId="by_address/", key=address, - reduce=False, - include_docs=True) + reduce=True, + include_docs=False) - d.addCallbacks(partial(self._get_uuid, address), log.err) + def _callback(result): + if len(result["rows"]): + return address + return None + + d.addCallbacks(_callback, log.err) return d - def _get_uuid(self, address, result): - """ - Parses the result of the by_address query and gets the uuid - - :param address: alias looked up - :type address: string - :param result: result dictionary - :type result: dict - :return: The uuid for alias if available - :rtype: str - """ - for row in result["rows"]: - if row["key"] == address: - uuid = row["doc"].get("user_id", None) - if uuid is None: - log.msg("ERROR: Found doc for %s but there's not user_id!" - % (address,)) - return uuid - return None - - def getPubKey(self, uuid): + def getPubKey(self, address): """ - Returns a deferred that will return the pubkey for the uuid provided + Returns a deferred that will fire with the pubkey for the address. - :param uuid: uuid for the user to query - :type uuid: str + :param address: email address to query + :type address: str :rtype: Deferred """ d = self.openView(docId="Identity", viewId="pgp_key_by_email/", - user_id=uuid, + key=address, reduce=False, - include_docs=True) - - d.addCallbacks(partial(self._get_pgp_key, uuid), log.err) + include_docs=False) - return d - - def _get_pgp_key(self, uuid, result): - """ - Callback used to filter the correct pubkey from the result of - the query to the couchdb + def _callback(result): + if not result["rows"]: + log.msg("No PGP public key found for %s." % address) + return None + if len(result["rows"]) > 1: + log.msg("More than one PGP public key found for %s, " + "will pick the first one found." % address) + row = result["rows"].pop(0) + return row["value"] - :param uuid: uuid for the user that was queried - :type uuid: str - :param result: result dictionary for the db query - :type result: dict + d.addCallbacks(_callback, log.err) - :rtype: str or None - """ - for row in result["rows"]: - user_id = row["doc"].get("user_id") - if not user_id: - print("User %s is in an inconsistent state") - continue - if user_id == uuid: - return row["value"] - return None - -if __name__ == "__main__": - from twisted.internet import reactor - cdb = ConnectedCouchDB("localhost", - port=6666, - dbName="users", - username="", - password="") - - d = cdb.queryByLoginOrAlias("test1") - - @d.addCallback - def right(result): - print "Should be an actual uuid:", result - print "Public Key:" - print cdb.getPubKey(result) - - d2 = cdb.queryByLoginOrAlias("asdjaoisdjoiqwjeoi") - - @d2.addCallback - def wrong(result): - print "Should be None:", result - - reactor.callLater(5, reactor.stop) - reactor.run() + return d diff --git a/src/leap/mx/tcp_map.py b/src/leap/mx/tcp_map.py index b62441f..d8cd835 100644 --- a/src/leap/mx/tcp_map.py +++ b/src/leap/mx/tcp_map.py @@ -43,9 +43,7 @@ class LEAPPostfixTCPMapServerFactory(ServerFactory): def get(self, key): """ - Look up uuid based on key, only up to the username id of the key. - - At some point we will have to consider the domain part too. + Look up if address exists. :param key: The lookup key. :type key: str -- cgit v1.2.3 From 14ef3dcce18240b756415fefa2a56936f96a12e9 Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 13 Apr 2015 15:58:16 -0300 Subject: [bug] fix bounce message recipient The bounce message was using the original message's "From:" header instead of the "To:" header to indicate the original recipient. This commit fixes that. Closes: #6854. Releases: 0.6.2, 0.7.0 --- src/leap/mx/mail_receiver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 7856594..77909b0 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -392,15 +392,16 @@ class MailReceiver(Service): :param reason: Brief explanation about why it's being bounced :type reason: str """ - to = orig_msg.get("From") + orig_from = orig_msg.get("From") + orig_to = orig_msg.get("To") msg = MIMEMultipart() msg['From'] = self._bounce_from - msg['To'] = to + msg['To'] = orig_from msg['Date'] = formatdate(localtime=True) msg['Subject'] = self._bounce_subject - decoded_to = " ".join([x[0] for x in decode_header(to)]) + decoded_to = " ".join([x[0] for x in decode_header(orig_to)]) text = BOUNCE_TEMPLATE.format(decoded_to, reason, orig_msg.as_string()) -- cgit v1.2.3 From 527d7d4a67f859a3315812b100b2c58fd0eeded6 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 15 Apr 2015 12:48:34 -0300 Subject: [bug] return uuid as result of alias resolver This fixes a bug introduced on b0ef529cc882a96903597fb5279919969fa286c3, when the alias resolver was modified to return the user's address instead of the uuid. In order to fix this, I had to revert one of the changes made by the commit above, which is to don't make use of reduced view for the uuid query. The pgp public key query remains reduced, as implemented in the commit above. We also refactor the code a bit to allow for log messages specific to each of tcp map's sublasses. Related: #6858. --- src/leap/mx/alias_resolver.py | 18 ++++++--- src/leap/mx/check_recipient_access.py | 35 +++-------------- src/leap/mx/couchdbhelper.py | 73 +++++++++++++++++++---------------- src/leap/mx/mail_receiver.py | 2 +- src/leap/mx/tcp_map.py | 30 ++++++++++---- 5 files changed, 81 insertions(+), 77 deletions(-) (limited to 'src') diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index dd631c8..752eac4 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -40,22 +40,26 @@ class LEAPPostfixTCPMapAliasServer(postfix.PostfixTCPMapServer): A postfix tcp map alias resolver server. """ - def _cbGot(self, address): + def _cbGot(self, user_data): """ Return a code and message depending on the result of the factory's get(). - :param address: The address returned by the factory. - :type address: str + :param user_data: The user's uuid and pgp public key. + :type user_data: list """ - if address is None: + uuid, _ = user_data + if uuid is None: self.sendCode( TCP_MAP_CODE_PERMANENT_FAILURE, postfix.quote("NOT FOUND SRY")) else: + # properly encode uuid, otherwise twisted complains when replying + if isinstance(uuid, unicode): + uuid = uuid.encode("utf8") self.sendCode( TCP_MAP_CODE_SUCCESS, - postfix.quote(address)) + postfix.quote(uuid)) class AliasResolverFactory(LEAPPostfixTCPMapServerFactory): @@ -64,3 +68,7 @@ class AliasResolverFactory(LEAPPostfixTCPMapServerFactory): """ protocol = LEAPPostfixTCPMapAliasServer + + @property + def _query_message(self): + return "Resolving alias for" diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index 3b61fe8..9f79dfe 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -23,7 +23,6 @@ Test this with postmap -v -q "foo" tcp:localhost:2244 """ from twisted.protocols import postfix -from twisted.internet import defer from leap.mx.tcp_map import LEAPPostfixTCPMapServerFactory from leap.mx.tcp_map import TCP_MAP_CODE_SUCCESS @@ -50,8 +49,8 @@ class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): :param value: The uuid and public key. :type value: list """ - address, pubkey = value - if address is None: + uuid, pubkey = value + if uuid is None: self.sendCode( TCP_MAP_CODE_PERMANENT_FAILURE, postfix.quote("REJECT")) @@ -75,31 +74,7 @@ class CheckRecipientAccessFactory(LEAPPostfixTCPMapServerFactory): protocol = LEAPPostFixTCPMapAccessServer - def _getPubKey(self, address): - """ - Look up PGP public key based on email address. - - :param address: The email address. - :type address: str - - :return: A deferred that is fired with the address and the public key, if - each of them exists. - :rtype: DeferredList - """ - if not address: - return defer.succeed([None, None]) - return defer.gatherResults([ - defer.succeed(address), - self._cdb.getPubKey(address), - ]) - - def get(self, key): - """ - Look up uuid and PGP public key based on key. + @property + def _query_message(self): + return "Checking recipient access for" - :param key: The lookup key. - :type key: str - """ - d = LEAPPostfixTCPMapServerFactory.get(self, key) - d.addCallback(self._getPubKey) - return d diff --git a/src/leap/mx/couchdbhelper.py b/src/leap/mx/couchdbhelper.py index 7bcb5aa..1752b4e 100644 --- a/src/leap/mx/couchdbhelper.py +++ b/src/leap/mx/couchdbhelper.py @@ -71,56 +71,63 @@ class ConnectedCouchDB(client.CouchDB): """ pass - def queryByAddress(self, address): + def getUuidAndPubkey(self, address): """ - Check to see if a particular email or alias exists. + Query couch and return a deferred that will fire with the uuid and pgp + public key for address. :param address: A string representing the email or alias to check. :type address: str - :return: a deferred for this query + :return: A deferred that will fire with the user's uuid and pgp public + key. :rtype twisted.defer.Deferred """ # TODO: Cache results d = self.openView(docId="Identity", viewId="by_address/", key=address, - reduce=True, - include_docs=False) - - def _callback(result): - if len(result["rows"]): - return address - return None - - d.addCallbacks(_callback, log.err) - + reduce=False, + include_docs=True) + + def _get_uuid_and_pubkey_cbk(result): + uuid = None + pubkey = None + if result["rows"]: + doc = result["rows"][0]["doc"] + uuid = doc["user_id"] + if "keys" in doc: + pubkey = doc["keys"]["pgp"] + return uuid, pubkey + + d.addCallback(_get_uuid_and_pubkey_cbk) return d - def getPubKey(self, address): + def getPubkey(self, uuid): """ - Returns a deferred that will fire with the pubkey for the address. + Query couch and return a deferred that will fire with the pgp public + key for user with given uuid. - :param address: email address to query - :type address: str + :param uuid: The uuid of a user + :type uuid: str + :return: A deferred that will fire with the pgp public key for + the user. :rtype: Deferred """ d = self.openView(docId="Identity", - viewId="pgp_key_by_email/", - key=address, + viewId="by_user_id/", + key=uuid, reduce=False, - include_docs=False) - - def _callback(result): - if not result["rows"]: - log.msg("No PGP public key found for %s." % address) - return None - if len(result["rows"]) > 1: - log.msg("More than one PGP public key found for %s, " - "will pick the first one found." % address) - row = result["rows"].pop(0) - return row["value"] - - d.addCallbacks(_callback, log.err) - + include_docs=True) + + def _get_pubkey_cbk(result): + pubkey = None + try: + doc = result["rows"][0]["doc"] + pubkey = doc["keys"]["pgp"] + except (KeyError, IndexError): + pass + return pubkey + + d.addCallbacks(_get_pubkey_cbk, log.err) return d diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 77909b0..f0b9c03 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -491,7 +491,7 @@ class MailReceiver(Service): log.msg("BUG: There was no uuid!") defer.returnValue(None) - pubkey = yield self._users_cdb.getPubKey(uuid) + pubkey = yield self._users_cdb.getPubkey(uuid) if pubkey is None or len(pubkey) == 0: log.msg("No public key, stopping the processing chain") bounce_reason = "Missing PubKey: There was a problem " \ diff --git a/src/leap/mx/tcp_map.py b/src/leap/mx/tcp_map.py index d8cd835..108c2aa 100644 --- a/src/leap/mx/tcp_map.py +++ b/src/leap/mx/tcp_map.py @@ -17,8 +17,11 @@ # along with this program. If not, see . -from twisted.python import log +from abc import ABCMeta +from abc import abstractproperty + from twisted.internet.protocol import ServerFactory +from twisted.python import log # For info on codes, see: http://www.postfix.org/tcp_table.5.html @@ -27,11 +30,14 @@ TCP_MAP_CODE_TEMPORARY_FAILURE = 400 TCP_MAP_CODE_PERMANENT_FAILURE = 500 -class LEAPPostfixTCPMapServerFactory(ServerFactory): +class LEAPPostfixTCPMapServerFactory(ServerFactory, object): """ A factory for postfix tcp map servers. """ + __metaclass__ = ABCMeta + + def __init__(self, couchdb): """ Initialize the factory. @@ -41,14 +47,22 @@ class LEAPPostfixTCPMapServerFactory(ServerFactory): """ self._cdb = couchdb - def get(self, key): + @abstractproperty + def _query_message(self): + pass + + def get(self, lookup_key): """ - Look up if address exists. + Look up user based on lookup_key. + + :param lookup_key: The lookup key. + :type lookup_key: str - :param key: The lookup key. - :type key: str + :return: A deferred that will be fired with the user's address, uuid + and pgp key. + :rtype: Deferred """ - log.msg("Query key: %s" % (key,)) - d = self._cdb.queryByAddress(key) + log.msg("%s %s" % (self._query_message, lookup_key,)) + d = self._cdb.getUuidAndPubkey(lookup_key) d.addErrback(log.err) return d -- cgit v1.2.3 From ae90151c632b376abc2a5bdf76d136b3a3629ea6 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 15 Apr 2015 14:16:13 -0300 Subject: [bug] fix extraction of uuid from message headers Before this commit, the mail receiver system used to compare the domain of the delivery addresses found in the "Delivered-To" header to find out the final delivery address. If we assume that the mail server delivery to the spool mail directory was correct, then we have two facts: (1) the topmost "Delivered-To" header is the one that indicates the correct final delivery address; and (2) we should expect the address to be @ because of the earlier alias resolve query made by the mail server. Another problem is that the domain comparison would compare whatever is in the "Delivered-To" header with whatever the python's socket module would return, which depends on the values on /etc/hosts and the order of the values in that file. This was causing problems whenever the platform made changes in /etc/hosts. So this commit eliminates the domain check and gets the uuid from the first "Delivered-To" header found in the message. Related: #6858. --- src/leap/mx/mail_receiver.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index f0b9c03..6b384f2 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -39,7 +39,6 @@ import signal import json import email.utils -import socket from email import message_from_string from email.MIMEMultipart import MIMEMultipart @@ -181,8 +180,6 @@ class MailReceiver(Service): self._directories = directories self._bounce_from = bounce_from self._bounce_subject = bounce_subject - - self._domain = socket.gethostbyaddr(socket.gethostname())[0] self._processing_skipped = False def startService(self): @@ -357,7 +354,7 @@ class MailReceiver(Service): def _get_owner(self, mail): """ - Given an email, returns the uuid of the owner. + Given an email, return the uuid of the owner. :param mail: mail to analyze :type mail: email.message.Message @@ -365,18 +362,17 @@ class MailReceiver(Service): :returns: uuid :rtype: str or None """ - uuid = None - + # we expect the topmost "Delivered-To" header to indicate the correct + # final delivery address. It should consist of @, as the + # earlier alias resolver query should have translated the username to + # the user id. See https://leap.se/code/issues/6858 for more info. delivereds = mail.get_all("Delivered-To") if delivereds is None: + # XXX this should not happen! see the comment above return None - 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 - + final_address = delivereds.pop(0) + _, addr = email.utils.parseaddr(final_address) + uuid, _ = addr.split("@") return uuid @defer.inlineCallbacks -- cgit v1.2.3 From 3353e2bccb2625ae06472721cfbb8cf53144a255 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 15 Apr 2015 14:49:56 -0300 Subject: [bug] implement message bouncing according to RFCs If we do not adhere to the standads, we may have a lot of problems when bouncing a message. This commit implements a bounce message according to: * RFC 6522 - The Multipart/Report Media Type for the Reporting of Mail System Administrative Messages * RFC 3834 - Do not bounce for unknown or invalid addresses. * RFC 3464 - An Extensible Message Format for Delivery Status Notification. Closes: #6858. --- src/leap/mx/bounce.py | 526 +++++++++++++++++++++++++++++++++++++++++++ src/leap/mx/mail_receiver.py | 132 ++--------- 2 files changed, 549 insertions(+), 109 deletions(-) create mode 100644 src/leap/mx/bounce.py (limited to 'src') diff --git a/src/leap/mx/bounce.py b/src/leap/mx/bounce.py new file mode 100644 index 0000000..2ece6df --- /dev/null +++ b/src/leap/mx/bounce.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +# bounce.py +# Copyright (C) 2015 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 . + + +""" +Everything you need to correctly bounce a message! + +This is built from the following RFCs: + + * The Multipart/Report Media Type for the Reporting of Mail System + Administrative Messages + https://tools.ietf.org/html/rfc6522 + + * Recommendations for Automatic Responses to Electronic Mail + https://tools.ietf.org/html/rfc3834 + + * An Extensible Message Format for Delivery Status Notifications + https://tools.ietf.org/html/rfc3464 +""" + + +import re +import socket + +from StringIO import StringIO +from textwrap import wrap + +from email.errors import MessageError +from email.message import Message +from email.utils import formatdate +from email.utils import parseaddr +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from email.generator import Generator +from email.generator import NL + +from twisted.internet import defer +from twisted.internet import protocol +from twisted.internet import reactor +from twisted.internet.error import ProcessDone +from twisted.python import log + + +EMAIL_ADDRESS_REGEXP = re.compile("[^@]+@[^@]+\.[^@]+") +HOSTNAME = socket.gethostbyaddr(socket.gethostname())[0] + + +def _valid_address(address): + """ + Return whether address is a valid email address. + + :param address: An email address candidate. + :type address: str + + :return: Whether address is valid. + :rtype: bool + """ + return bool(EMAIL_ADDRESS_REGEXP.match(address)) + + +def bounce_message(bounce_from, bounce_subject, orig_msg, reason): + """ + Bounce a message. + + :param bounce_from: The sender of the bounce message. + :type bounce_from: str + :param bounce_subject: The subject of the bounce message. + :type bounce_subject: str + :param orig_msg: The original message that will be bounced. + :type orig_msg: email.message.Message + :param reason: The reason for bouncing the message. + :type reason: str + + :return: A deferred that will fire with the output of the sendmail process + if it was successful or with a failure containing the reason for + the end of the process if it failed. + :rtype: Deferred + """ + orig_rpath = orig_msg.get("Return-Path") + + # do not bounce if sender address is invalid + _, addr = parseaddr(orig_rpath) + if not _valid_address(addr): + log.msg( + "Will not send a bounce message to an invalid address: %s" + % orig_rpath) + return + + msg = _build_bounce_message( + bounce_from, bounce_subject, orig_msg, reason) + return _async_check_output(["/usr/sbin/sendmail", "-t"], msg.as_string()) + + +def _check_valid_return_path(return_path): + """ + Check if a certain return path is valid. + + From RFC 3834: + + Responders MUST NOT generate any response for which the + destination of that response would be a null address (e.g., an + address for which SMTP MAIL FROM or Return-Path is <>), since the + response would not be delivered to a useful destination. + Responders MAY refuse to generate responses for addresses commonly + used as return addresses by responders - e.g., those with local- + parts matching "owner-*", "*-request", "MAILER-DAEMON", etc. + Responders are encouraged to check the destination address for + validity before generating the response, to avoid generating + responses that cannot be delivered or are unlikely to be useful. + + :return: Whether the return_path is valid. + :rtype: bool + """ + _, addr = parseaddr(return_path) + + # check null address + if not addr: + return False + + # check addresses commonly used as return addresses by responders + local, _ = addr.split("@", 1) + if local.startswith("owner-") \ + or local.endswith("-request") \ + or local.startswith("MAILER-DAEMON"): + return False + + return True + + +class DeliveryStatusNotificationMessage(MIMEBase): + """ + A delivery status message, as per RFC 3464. + """ + + def __init__(self, orig_msg): + """ + Initialize the DSN. + """ + MIMEBase.__init__(self, "message", "delivery-status") + self.__delitem__("MIME-Version") + self._build_dsn(orig_msg) + + def _build_dsn(self, orig_msg): + """ + Build an RFC 3464 compliant delivery status message. + + :param orig_msg: The original bouncing message. + :type orig_msg: email.message.Message + """ + content = [] + + # Per-Message DSN fields + # ====================== + + # Original-Envelope-Id (optional) + envelope_id = orig_msg.get("Envelope-Id") + if envelope_id: + content.append("Original-Envelope-Id: %s" % envelope_id) + + # Reporting-MTA (required) + content.append("Reporting-MTA: dns; %s" % HOSTNAME) + + # XXX add Arrival-Date DSN field? (optional). + + content.append("") + + # Per-Recipient DSN fields + # ======================== + + # Original-Recipient (optional) + orig_to = orig_msg.get("X-Original-To") # added by postfix + _, orig_addr = parseaddr(orig_to) + if orig_addr: + content.append("Original-Recipient: rfc822; %s" % orig_addr) + + # Final-Recipient (required) + delivered_to = orig_msg.get("Delivered-To") + content.append("Final-Recipient: rfc822; %s" % delivered_to) + + # Action (required) + content.append("Action: failed") + + # Status (required) + content.append("Status: 5.0.0") # permanent failure + + # XXX add other optional fields? (Remote-MTA, Diagnostic-Code, + # Last-Attempt-Date, Final-Log-ID, Will-Retry-Until) + + # return a "message/delivery-status" message + msg = Message() + msg.set_payload("\n".join(content)) + self.attach(msg) + + +class RFC822Headers(MIMEText): + """ + A text/rfc822-headers mime message as defined in RFC 6522. + """ + + def __init__(self, _text, **kwargs): + """ + Initialize the message. + + :param _text: The contents of the message. + :type _text: str + """ + MIMEText.__init__( + self, _text, + # set "text/rfc822-headers" mime type + _subtype='rfc822-headers', + **kwargs) + + +BOUNCE_TEMPLATE = """ +This is the mail system at {0}. + +I'm sorry to have to inform you that your message could not +be delivered to one or more recipients. It's attached below. + +For further assistance, please send mail to postmaster. + +If you do so, please include this problem report. You can +delete your own text from the attached returned message. + + The mail system + +{1} +""".strip() + + +class InvalidReturnPathError(MessageError): + """ + Exception raised when the return path is invalid. + """ + + +def _build_bounce_message(bounce_from, bounce_subject, orig_msg, reason): + """ + Build a bounce message. + + :param bounce_from: The sender address of the bounce message. + :type bounce_from: str + :param bounce_subject: The subject of the bounce message. + :type bounce_subject: str + :param orig_msg: The original bouncing message. + :type orig_msg: email.message.Message + :param reason: The reason for the bounce. + :type reason: str + + :return: The bounce message. + :rtype: MIMEMultipartReport + + :raise InvalidReturnPathError: Raised when the "Return-Path" header of the + original message is invalid for creating a + bounce message. + """ + # abort creation if "Return-Path" header is invalid + orig_rpath = orig_msg.get("Return-Path") + if not _check_valid_return_path(orig_rpath): + raise InvalidReturnPathError + + msg = MIMEMultipartReport() + msg['From'] = bounce_from + msg['To'] = orig_rpath + msg['Date'] = formatdate(localtime=True) + msg['Subject'] = bounce_subject + msg['Return-Path'] = "<>" # prevent bounce message loop, see RFC 3834 + + # create and attach first required part + orig_to = orig_msg.get("X-Original-To") # added by postfix + wrapped_reason = wrap(("<%s>: " % orig_to) + reason, 74) + for i in xrange(1, len(wrapped_reason)): + wrapped_reason[i] = " " + wrapped_reason[i] + wrapped_reason = "\n".join(wrapped_reason) + text = BOUNCE_TEMPLATE.format(HOSTNAME, wrapped_reason) + msg.attach(MIMEText(text)) + + # create and attach second required part + msg.attach(DeliveryStatusNotificationMessage(orig_msg)) + + # attach third (optional) part. + # + # XXX From RFC 6522: + # + # When 8-bit or binary data not encoded in a 7-bit form is to be + # returned, and the return path is not guaranteed to be 8-bit or + # binary capable, two options are available. The original message + # MAY be re-encoded into a legal 7-bit MIME message or the + # text/rfc822-headers media type MAY be used to return only the + # original message headers. + # + # This is not implemented yet, we should detect if content is 7bit and + # use the class RFC822Headers if it is not. +# try: +# payload = orig_msg.get_payload() +# payload.encode("ascii") +# except UnicodeError: +# headers = [] +# for k in orig_msg.keys(): +# headers.append("%s: %s" % (k, orig_msg[k])) +# orig_msg = RFC822Headers("\n".join(headers)) + msg.attach(orig_msg) + + return msg + + +class BouncerSubprocessProtocol(protocol.ProcessProtocol): + """ + Bouncer subprocess protocol that will feed the msg contents to be + bounced through stdin + """ + + def __init__(self, msg): + """ + Constructor for the BouncerSubprocessProtocol + + :param msg: Message to send to stdin when the process has + launched + :type msg: str + """ + self._msg = msg + self._outBuffer = "" + self._errBuffer = "" + self._d = defer.Deferred() + + @property + def deferred(self): + return self._d + + def connectionMade(self): + self.transport.write(self._msg) + self.transport.closeStdin() + + def outReceived(self, data): + self._outBuffer += data + + def errReceived(self, data): + self._errBuffer += data + + def processEnded(self, reason): + if reason.check(ProcessDone): + self._d.callback(self._outBuffer) + else: + self._d.errback(reason) + + +def _async_check_output(args, msg): + """ + Async spawn a process and return a defer to be able to check the + output with a callback/errback + + :param args: the command to execute along with the params for it + :type args: list of str + :param msg: string that will be send to stdin of the process once + it's spawned + :type msg: str + + :rtype: defer.Deferred + """ + pprotocol = BouncerSubprocessProtocol(msg) + reactor.spawnProcess(pprotocol, args[0], args) + return pprotocol.deferred + + +class DSNGenerator(Generator): + """ + A slightly modified generator to correctly parse delivery status + notifications. + """ + + def _handle_message_delivery_status(self, msg): + """ + Handle a message of type "message/delivery-status". + + This is modified from upstream version in that it also removes empty + lines in the beginning of each part. + + :param msg: The message to be handled. + :type msg: Message + """ + # We can't just write the headers directly to self's file object + # because this will leave an extra newline between the last header + # block and the boundary. Sigh. + blocks = [] + for part in msg.get_payload(): + s = StringIO() + g = self.clone(s) + g.flatten(part, unixfrom=False) + text = s.getvalue() + lines = text.split('\n') + # Strip off the unnecessary trailing empty line + if lines: + if lines[0] == '': + lines.pop(0) + if lines[-1] == '': + lines.pop() + blocks.append(NL.join(lines)) + else: + blocks.append(text) + # Now join all the blocks with an empty line. This has the lovely + # effect of separating each block with an empty line, but not adding + # an extra one after the last one. + self._fp.write(NL.join(blocks)) + + +class MIMEMultipartReport(MIMEMultipart): + """ + Implement multipart/report MIME type as defined in RFC 6522. + + The syntax of multipart/report is identical to the multipart/mixed + content type defined in https://tools.ietf.org/html/rfc2045. + + The multipart/report media type contains either two or three sub- + parts, in the following order: + + 1. (REQUIRED) A human-readable message. + 2. (REQUIRED) A machine-parsable body part containing an account of + the reported message handling event. + 3. (OPTIONAL) A body part containing the returned message or a + portion thereof. + """ + + def __init__( + self, report_type="message/delivery-status", boundary=None, + _subparts=None): + """ + Initialize the message. + + As per RFC 6522, boundary and report_type are required parameters. + + :param report_type: The type of report. This is set as a + "Content-Type" parameter, and should match the + MIME subtype of the second body part. + :type report_type: str + + """ + MIMEMultipart.__init__( + self, + # set mime type to "multipart/report" + _subtype="report", + boundary=boundary, + _subparts=_subparts, + # add "report-type" as a "Content-Type" parameter + report_type=report_type) + self._report_type = report_type + + def attach(self, payload): + """ + Add the given payload to the current payload, but first verify if it's + valid according to RFC6522. + + :param payload: The payload to be attached. + :type payload: Message + + :raise MessageError: Raised if the payload is invalid. + """ + idx = len(self.get_payload()) + 1 + self._check_valid_payload(idx, payload) + MIMEMultipart.attach(self, payload) + + def _check_valid_payload(self, idx, payload): + """ + Check that an attachment is valid according to RFC6522. + + :param payload: The payload to be attached. + :type payload: Message + + :raise MessageError: Raised if the payload is invalid. + """ + if idx == 1: + # The text in the first section can use any IANA-registered MIME + # media type, charset, or language. + cond = lambda payload: isinstance(payload, MIMEBase) + error_msg = "The first attachment must be a MIME message." + elif idx == 2: + # RFC 6522 requires that the report-type parameter is equal to the + # MIME subtype of the second body type of the multipart/report. + cond = lambda payload: \ + payload.get_content_type() == self._report_type + error_msg = "The second attachment's subtype must be %s." \ + % self._report_type + elif idx == 3: + # A body part containing the returned message or a portion thereof. + cond = lambda payload: isinstance(payload, Message) + error_msg = "The third attachment must be a message." + else: + # The multipart/report media type contains either two or three sub- + # parts. + cond = lambda _: False + error_msg = "The multipart/report media type contains either " \ + "two or three sub-parts." + if not cond(payload): + raise MessageError("Invalid attachment: %s" % error_msg) + + def as_string(self, unixfrom=False): + """ + Return the entire formatted message as string. + + This is modified from upstream to use our own generator. + + :param as_string: Whether to include the Unix From envelope heder. + :type as_string: bool + + :return: The entire formatted message. + :rtype: str + """ + fp = StringIO() + g = DSNGenerator(fp) + g.flatten(self, unixfrom=unixfrom) + return fp.getvalue() diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 6b384f2..446fd38 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -41,10 +41,6 @@ import json import email.utils from email import message_from_string -from email.MIMEMultipart import MIMEMultipart -from email.MIMEText import MIMEText -from email.Utils import formatdate -from email.header import decode_header from twisted.application.service import Service, IService from twisted.internet import inotify, defer, task, reactor @@ -52,88 +48,15 @@ from twisted.python import filepath, log from zope.interface import implements -from leap.soledad.common.crypto import ( - EncryptionSchemes, - ENC_JSON_KEY, - ENC_SCHEME_KEY, -) +from leap.soledad.common.crypto import EncryptionSchemes +from leap.soledad.common.crypto import ENC_JSON_KEY +from leap.soledad.common.crypto import ENC_SCHEME_KEY from leap.soledad.common.couch import CouchDatabase, CouchDocument -from leap.keymanager import openpgp - -BOUNCE_TEMPLATE = """ -Delivery to the following recipient failed: - {0} - -Reasons: - {1} - -Original message: - {2} -""".strip() - - -from twisted.internet import protocol -from twisted.internet.error import ProcessDone - - -class BouncerSubprocessProtocol(protocol.ProcessProtocol): - """ - Bouncer subprocess protocol that will feed the msg contents to be - bounced through stdin - """ - - def __init__(self, msg): - """ - Constructor for the BouncerSubprocessProtocol - - :param msg: Message to send to stdin when the process has - launched - :type msg: str - """ - self._msg = msg - self._outBuffer = "" - self._errBuffer = "" - self._d = None - - @property - def deferred(self): - return self._d - - def connectionMade(self): - self._d = defer.Deferred() - self.transport.write(self._msg) - self.transport.closeStdin() - - def outReceived(self, data): - self._outBuffer += data - - def errReceived(self, data): - self._errBuffer += data - - def processEnded(self, reason): - if reason.check(ProcessDone): - self._d.callback(self._outBuffer) - else: - self._d.errback(reason) - - -def async_check_output(args, msg): - """ - Async spawn a process and return a defer to be able to check the - output with a callback/errback - - :param args: the command to execute along with the params for it - :type args: list of str - :param msg: string that will be send to stdin of the process once - it's spawned - :type msg: str +from leap.keymanager import openpgp - :rtype: defer.Deferred - """ - pprotocol = BouncerSubprocessProtocol(msg) - reactor.spawnProcess(pprotocol, args[0], args) - return pprotocol.deferred +from leap.mx.bounce import bounce_message +from leap.mx.bounce import InvalidReturnPathError class MailReceiver(Service): @@ -376,10 +299,10 @@ class MailReceiver(Service): return uuid @defer.inlineCallbacks - def _bounce_mail(self, orig_msg, filepath, reason): + def _bounce_message(self, orig_msg, filepath, reason): """ - Bounces the email contained in orig_msg to it's sender and - removes it from the queue. + Bounce the message contained in orig_msg to it's sender and + remove it from the queue. :param orig_msg: Message that is going to be bounced :type orig_msg: email.message.Message @@ -388,23 +311,12 @@ class MailReceiver(Service): :param reason: Brief explanation about why it's being bounced :type reason: str """ - orig_from = orig_msg.get("From") - orig_to = orig_msg.get("To") - - msg = MIMEMultipart() - msg['From'] = self._bounce_from - msg['To'] = orig_from - msg['Date'] = formatdate(localtime=True) - msg['Subject'] = self._bounce_subject - - decoded_to = " ".join([x[0] for x in decode_header(orig_to)]) - text = BOUNCE_TEMPLATE.format(decoded_to, - reason, - orig_msg.as_string()) - - msg.attach(MIMEText(text)) - - yield async_check_output(["/usr/sbin/sendmail", "-t"], msg.as_string()) + try: + yield bounce_message( + self._bounce_from, self._bounce_subject, orig_msg, reason) + except InvalidReturnPathError: + # give up bouncing this message! + log.msg("Will not bounce message because of invalid return path.") yield self._conditional_remove(True, filepath) def sleep(self, secs): @@ -479,7 +391,7 @@ class MailReceiver(Service): (filepath.path,)) bounce_reason = "Missing UUID: There was a problem " \ "locating the user in our database." - yield self._bounce_mail(msg, filepath, bounce_reason) + yield self._bounce_message(msg, filepath, bounce_reason) defer.returnValue(None) log.msg("Mail owner: %s" % (uuid,)) @@ -489,11 +401,13 @@ class MailReceiver(Service): pubkey = yield self._users_cdb.getPubkey(uuid) if pubkey is None or len(pubkey) == 0: - log.msg("No public key, stopping the processing chain") - bounce_reason = "Missing PubKey: There was a problem " \ - "locating the user's public key in our " \ - "database." - yield self._bounce_mail(msg, filepath, bounce_reason) + log.msg( + "No public key for %s, stopping the processing chain." + % uuid) + bounce_reason = "Missing PGP public key: There was a " \ + "problem locating the user's public key in " \ + "our database." + yield self._bounce_message(msg, filepath, bounce_reason) defer.returnValue(None) log.msg("Encrypting message to %s's pubkey" % (uuid,)) -- cgit v1.2.3 From 62def16809c1cf739db5a7a8e7aa24fec70fdf5d Mon Sep 17 00:00:00 2001 From: drebs Date: Fri, 17 Apr 2015 16:35:58 -0300 Subject: [doc] update documentation I'm updating (1) some very outdated doc from when the program was not yet written, and (2) some small stuff inside classes docstrings. --- src/leap/mx/alias_resolver.py | 6 ++++++ src/leap/mx/check_recipient_access.py | 10 +++++++++- src/leap/mx/tcp_map.py | 4 ++++ 3 files changed, 19 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/leap/mx/alias_resolver.py b/src/leap/mx/alias_resolver.py index 752eac4..c6f2acc 100644 --- a/src/leap/mx/alias_resolver.py +++ b/src/leap/mx/alias_resolver.py @@ -19,6 +19,12 @@ """ Classes for resolving postfix aliases. +The resolver is queried by the mail server before delivery to the mail spool +directory, and should return the user uuid. This way, we get rid from the user +address early and the mail server will delivery the message to +"@". Later, the mail receiver part of MX will parse the +"Delivered-To" header to extract the uuid and fetch the user's pgp public key. + Test this with postmap -v -q "foo" tcp:localhost:4242 TODO: diff --git a/src/leap/mx/check_recipient_access.py b/src/leap/mx/check_recipient_access.py index 9f79dfe..55460a6 100644 --- a/src/leap/mx/check_recipient_access.py +++ b/src/leap/mx/check_recipient_access.py @@ -17,7 +17,12 @@ # along with this program. If not, see . """ -Classes for resolving postfix recipient access +Classes for resolving postfix recipient access. + +The resolver is queried by the mail server before delivery to the mail spool +directory, and should check if the address is able to receive messages. +Examples of reasons for denying delivery would be that the user is out of +quota, is user, or have no pgp public key in the server. Test this with postmap -v -q "foo" tcp:localhost:2244 """ @@ -44,6 +49,9 @@ class LEAPPostFixTCPMapAccessServer(postfix.PostfixTCPMapServer): Return a code and message depending on the result of the factory's get(). + If there's no pgp public key for the user, we currently return a + temporary failure saying that the user account is disabled. + For more info, see: http://www.postfix.org/access.5.html :param value: The uuid and public key. diff --git a/src/leap/mx/tcp_map.py b/src/leap/mx/tcp_map.py index 108c2aa..597c830 100644 --- a/src/leap/mx/tcp_map.py +++ b/src/leap/mx/tcp_map.py @@ -30,6 +30,10 @@ TCP_MAP_CODE_TEMPORARY_FAILURE = 400 TCP_MAP_CODE_PERMANENT_FAILURE = 500 +# we have to also extend from object here to make the class a new-style class. +# If we don't, we get a TypeError because "new-style classes can't have only +# classic bases". This has to do with the way abc.ABCMeta works and the old +# and new style of python classes. class LEAPPostfixTCPMapServerFactory(ServerFactory, object): """ A factory for postfix tcp map servers. -- cgit v1.2.3