diff options
| -rw-r--r-- | mail/changes/bug_4416-change-smtp-relay-to-gateway | 1 | ||||
| -rw-r--r-- | mail/changes/feature-4324_prevent-double-encryption-when-relaying | 2 | ||||
| -rw-r--r-- | mail/src/leap/mail/imap/tests/test_imap.py | 4 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/README.rst | 38 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/__init__.py | 47 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/gateway.py (renamed from mail/src/leap/mail/smtp/smtprelay.py) | 328 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/__init__.py | 6 | ||||
| -rw-r--r-- | mail/src/leap/mail/smtp/tests/test_gateway.py (renamed from mail/src/leap/mail/smtp/tests/test_smtprelay.py) | 72 | 
8 files changed, 233 insertions, 265 deletions
| diff --git a/mail/changes/bug_4416-change-smtp-relay-to-gateway b/mail/changes/bug_4416-change-smtp-relay-to-gateway new file mode 100644 index 0000000..08bead7 --- /dev/null +++ b/mail/changes/bug_4416-change-smtp-relay-to-gateway @@ -0,0 +1 @@ +  o Change SMTP service name from "relay" to "gateway". Closes #4416. diff --git a/mail/changes/feature-4324_prevent-double-encryption-when-relaying b/mail/changes/feature-4324_prevent-double-encryption-when-relaying new file mode 100644 index 0000000..a3d70a9 --- /dev/null +++ b/mail/changes/feature-4324_prevent-double-encryption-when-relaying @@ -0,0 +1,2 @@ +  o Prevent already encrypted outgoing messages from being encrypted again. +    Closes #4324. diff --git a/mail/src/leap/mail/imap/tests/test_imap.py b/mail/src/leap/mail/imap/tests/test_imap.py index 3411795..ad11315 100644 --- a/mail/src/leap/mail/imap/tests/test_imap.py +++ b/mail/src/leap/mail/imap/tests/test_imap.py @@ -91,7 +91,7 @@ def initialize_soledad(email, gnupg_home, tempdir):      """      uuid = "foobar-uuid" -    passphrase = "verysecretpassphrase" +    passphrase = u"verysecretpassphrase"      secret_path = os.path.join(tempdir, "secret.gpg")      local_db_path = os.path.join(tempdir, "soledad.u1db")      server_url = "http://provider" @@ -101,6 +101,8 @@ def initialize_soledad(email, gnupg_home, tempdir):          get_doc = Mock(return_value=None)          put_doc = Mock() +        lock = Mock(return_value=('atoken', 300)) +        unlock = Mock(return_value=True)          def __call__(self):              return self diff --git a/mail/src/leap/mail/smtp/README.rst b/mail/src/leap/mail/smtp/README.rst index 2b2a118..f625441 100644 --- a/mail/src/leap/mail/smtp/README.rst +++ b/mail/src/leap/mail/smtp/README.rst @@ -1,43 +1,23 @@ -Leap SMTP Relay -=============== +Leap SMTP Gateway +=================  Outgoing mail workflow:      * LEAP client runs a thin SMTP proxy on the user's device, bound to        localhost. -    * User's MUA is configured outgoing SMTP to localhost -    * When SMTP proxy receives an email from MUA +    * User's MUA is configured outgoing SMTP to localhost. +    * When SMTP proxy receives an email from MUA:          * SMTP proxy queries Key Manager for the user's private key and public -          keys of all recipients +          keys of all recipients.          * Message is signed by sender and encrypted to recipients.          * If recipient's key is missing, email goes out in cleartext (unless -          user has configured option to send only encrypted email) -        * Finally, message is relayed to provider's SMTP relay - - -Dependencies ------------- - -Leap SMTP Relay depends on the following python libraries: - -  * Twisted 12.3.0 [1] -  * zope.interface 4.0.3 [2] - -[1] http://pypi.python.org/pypi/Twisted/12.3.0 -[2] http://pypi.python.org/pypi/zope.interface/4.0.3 - - -How to run ----------- - -To launch the SMTP relay, run the following command: - -  twistd -y smtprelay.tac +          user has configured option to send only encrypted email). +        * Finally, message is gatewayed to provider's SMTP server.  Running tests  ------------- -Tests are run using Twisted's Trial API, like this: +Tests are run using Twisted's Trial API, like this:: -  trial leap.email.smtp.tests +    python setup.py test -s leap.mail.gateway.tests diff --git a/mail/src/leap/mail/smtp/__init__.py b/mail/src/leap/mail/smtp/__init__.py index be568b8..d3eb9e8 100644 --- a/mail/src/leap/mail/smtp/__init__.py +++ b/mail/src/leap/mail/smtp/__init__.py @@ -16,7 +16,7 @@  # along with this program. If not, see <http://www.gnu.org/licenses/>.  """ -SMTP relay helper function. +SMTP gateway helper function.  """  import logging @@ -26,15 +26,15 @@ from twisted.internet.error import CannotListenError  logger = logging.getLogger(__name__)  from leap.common.events import proto, signal -from leap.mail.smtp.smtprelay import SMTPFactory +from leap.mail.smtp.gateway import SMTPFactory -def setup_smtp_relay(port, userid, keymanager, smtp_host, smtp_port, +def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port,                       smtp_cert, smtp_key, encrypted_only):      """ -    Setup SMTP relay to run with Twisted. +    Setup SMTP gateway to run with Twisted. -    This function sets up the SMTP relay configuration and the Twisted +    This function sets up the SMTP gateway configuration and the Twisted      reactor.      :param port: The port in which to run the server. @@ -46,48 +46,29 @@ def setup_smtp_relay(port, userid, keymanager, smtp_host, smtp_port,      :type keymanager: leap.common.keymanager.KeyManager      :param smtp_host: The hostname of the remote SMTP server.      :type smtp_host: str -    :param smtp_port:  The port of the remote SMTP server. +    :param smtp_port: The port of the remote SMTP server.      :type smtp_port: int      :param smtp_cert: The client certificate for authentication.      :type smtp_cert: str      :param smtp_key: The client key for authentication.      :type smtp_key: str -    :param encrypted_only: Whether the SMTP relay should send unencrypted mail +    :param encrypted_only: Whether the SMTP gateway should send unencrypted mail                             or not.      :type encrypted_only: bool      :returns: tuple of SMTPFactory, twisted.internet.tcp.Port      """ -    # The configuration for the SMTP relay is a dict with the following -    # format: -    # -    # { -    #     'host': '<host>', -    #     'port': <int>, -    #     'cert': '<cert path>', -    #     'key': '<key path>', -    #     'encrypted_only': <True/False> -    # } -    config = { -        'host': smtp_host, -        'port': smtp_port, -        'cert': smtp_cert, -        'key': smtp_key, -        'encrypted_only': encrypted_only -    } -      # configure the use of this service with twistd -    factory = SMTPFactory(userid, keymanager, config) +    factory = SMTPFactory(userid, keymanager, smtp_host, smtp_port, smtp_cert, +                          smtp_key, encrypted_only)      try: -        tport = reactor.listenTCP(port, factory, -                                  interface="localhost") -        signal(proto.SMTP_SERVICE_STARTED, str(smtp_port)) +        tport = reactor.listenTCP(port, factory, interface="localhost") +        signal(proto.SMTP_SERVICE_STARTED, str(port))          return factory, tport      except CannotListenError:          logger.error("STMP Service failed to start: " -                     "cannot listen in port %s" % ( -                         smtp_port,)) -        signal(proto.SMTP_SERVICE_FAILED_TO_START, str(smtp_port)) +                     "cannot listen in port %s" % port) +        signal(proto.SMTP_SERVICE_FAILED_TO_START, str(port))      except Exception as exc: -        logger.error("Unhandled error while launching smtp relay service") +        logger.error("Unhandled error while launching smtp gateway service")          logger.exception(exc) diff --git a/mail/src/leap/mail/smtp/smtprelay.py b/mail/src/leap/mail/smtp/gateway.py index 92a9f0e..06405b4 100644 --- a/mail/src/leap/mail/smtp/smtprelay.py +++ b/mail/src/leap/mail/smtp/gateway.py @@ -1,5 +1,5 @@  # -*- coding: utf-8 -*- -# smtprelay.py +# gateway.py  # Copyright (C) 2013 LEAP  #  # This program is free software: you can redistribute it and/or modify @@ -16,9 +16,9 @@  # along with this program. If not, see <http://www.gnu.org/licenses/>.  """ -LEAP SMTP encrypted relay. +LEAP SMTP encrypted gateway. -The following classes comprise the SMTP relay service: +The following classes comprise the SMTP gateway service:      * SMTPFactory - A twisted.internet.protocol.ServerFactory that provides        the SMTPDelivery protocol. @@ -68,67 +68,15 @@ generator.Generator = RFC3156CompliantGenerator  # -# Exceptions -# - -class MalformedConfig(Exception): -    """ -    Raised when the configuration dictionary passed as parameter is malformed. -    """ -    pass - - -#  # Helper utilities  # -HOST_KEY = 'host' -PORT_KEY = 'port' -CERT_KEY = 'cert' -KEY_KEY = 'key' -ENCRYPTED_ONLY_KEY = 'encrypted_only' - - -def assert_config_structure(config): -    """ -    Assert that C{config} is a dict with the following structure: - -        { -            HOST_KEY: '<str>', -            PORT_KEY: <int>, -            CERT_KEY: '<str>', -            KEY_KEY: '<str>', -            ENCRYPTED_ONLY_KEY: <bool>, -        } - -    @param config: The dictionary to check. -    @type config: dict -    """ -    # assert smtp config structure is valid -    leap_assert_type(config, dict) -    leap_assert(HOST_KEY in config) -    leap_assert_type(config[HOST_KEY], str) -    leap_assert(PORT_KEY in config) -    leap_assert_type(config[PORT_KEY], int) -    leap_assert(CERT_KEY in config) -    leap_assert_type(config[CERT_KEY], (str, unicode)) -    leap_assert(KEY_KEY in config) -    leap_assert_type(config[KEY_KEY], (str, unicode)) -    leap_assert(ENCRYPTED_ONLY_KEY in config) -    leap_assert_type(config[ENCRYPTED_ONLY_KEY], bool) -    # assert received params are not empty -    leap_assert(config[HOST_KEY] != '') -    leap_assert(config[PORT_KEY] is not 0) -    leap_assert(config[CERT_KEY] != '') -    leap_assert(config[KEY_KEY] != '') - -  def validate_address(address):      """      Validate C{address} as defined in RFC 2822. -    @param address: The address to be validated. -    @type address: str +    :param address: The address to be validated. +    :type address: str      @return: A valid address.      @rtype: str @@ -150,48 +98,63 @@ def validate_address(address):  class SMTPFactory(ServerFactory):      """ -    Factory for an SMTP server with encrypted relaying capabilities. +    Factory for an SMTP server with encrypted gatewaying capabilities.      """ -    def __init__(self, userid, keymanager, config): +    def __init__(self, userid, keymanager, host, port, cert, key, +                 encrypted_only):          """          Initialize the SMTP factory. -        @param keymanager: A KeyManager for retrieving recipient's keys. -        @type keymanager: leap.common.keymanager.KeyManager -        @param config: A dictionary with smtp configuration. Should have -            the following structure: -                { -                    HOST_KEY: '<str>', -                    PORT_KEY: <int>, -                    CERT_KEY: '<str>', -                    KEY_KEY: '<str>', -                    ENCRYPTED_ONLY_KEY: <bool>, -                } -        @type config: dict -        @param userid: The user currently logged in -        @type userid: unicode +        :param userid: The user currently logged in +        :type userid: unicode +        :param keymanager: A KeyManager for retrieving recipient's keys. +        :type keymanager: leap.common.keymanager.KeyManager +        :param host: The hostname of the remote SMTP server. +        :type host: str +        :param port: The port of the remote SMTP server. +        :type port: int +        :param cert: The client certificate for authentication. +        :type cert: str +        :param key: The client key for authentication. +        :type key: str +        :param encrypted_only: Whether the SMTP gateway should send unencrypted +                               mail or not. +        :type encrypted_only: bool          """          # assert params          leap_assert_type(keymanager, KeyManager) -        assert_config_structure(config) +        leap_assert_type(host, str) +        leap_assert(host != '') +        leap_assert_type(port, int) +        leap_assert(port is not 0) +        leap_assert_type(cert, unicode) +        leap_assert(cert != '') +        leap_assert_type(key, unicode) +        leap_assert(key != '') +        leap_assert_type(encrypted_only, bool)          # and store them          self._userid = userid          self._km = keymanager -        self._config = config +        self._host = host +        self._port = port +        self._cert = cert +        self._key = key +        self._encrypted_only = encrypted_only      def buildProtocol(self, addr):          """          Return a protocol suitable for the job. -        @param addr: An address, e.g. a TCP (host, port). -        @type addr:  twisted.internet.interfaces.IAddress +        :param addr: An address, e.g. a TCP (host, port). +        :type addr:  twisted.internet.interfaces.IAddress          @return: The protocol.          @rtype: SMTPDelivery          """ -        smtpProtocol = smtp.SMTP(SMTPDelivery(self._userid, self._km, -                                              self._config)) +        smtpProtocol = smtp.SMTP(SMTPDelivery( +            self._userid, self._km, self._host, self._port, self._cert, +            self._key, self._encrypted_only))          smtpProtocol.factory = self          return smtpProtocol @@ -207,49 +170,53 @@ class SMTPDelivery(object):      implements(smtp.IMessageDelivery) -    def __init__(self, userid, keymanager, config): +    def __init__(self, userid, keymanager, host, port, cert, key, +                 encrypted_only):          """          Initialize the SMTP delivery object. -        @param keymanager: A KeyManager for retrieving recipient's keys. -        @type keymanager: leap.common.keymanager.KeyManager -        @param config: A dictionary with smtp configuration. Should have -            the following structure: -                { -                    HOST_KEY: '<str>', -                    PORT_KEY: <int>, -                    CERT_KEY: '<str>', -                    KEY_KEY: '<str>', -                    ENCRYPTED_ONLY_KEY: <bool>, -                } -        @type config: dict -        @param userid: The user currently logged in -        @type userid: unicode +        :param userid: The user currently logged in +        :type userid: unicode +        :param keymanager: A KeyManager for retrieving recipient's keys. +        :type keymanager: leap.common.keymanager.KeyManager +        :param host: The hostname of the remote SMTP server. +        :type host: str +        :param port: The port of the remote SMTP server. +        :type port: int +        :param cert: The client certificate for authentication. +        :type cert: str +        :param key: The client key for authentication. +        :type key: str +        :param encrypted_only: Whether the SMTP gateway should send unencrypted +                               mail or not. +        :type encrypted_only: bool          """ -        # assert params -        leap_assert_type(keymanager, KeyManager) -        assert_config_structure(config) -        # and store them          self._userid = userid          self._km = keymanager -        self._config = config +        self._userid = userid +        self._km = keymanager +        self._host = host +        self._port = port +        self._cert = cert +        self._key = key +        self._encrypted_only = encrypted_only          self._origin = None      def receivedHeader(self, helo, origin, recipients):          """          Generate the 'Received:' header for a message. -        @param helo: The argument to the HELO command and the client's IP +        :param helo: The argument to the HELO command and the client's IP              address. -        @type helo: (str, str) -        @param origin: The address the message is from. -        @type origin: twisted.mail.smtp.Address -        @param recipients: A list of the addresses for which this message is +        :type helo: (str, str) +        :param origin: The address the message is from. +        :type origin: twisted.mail.smtp.Address +        :param recipients: A list of the addresses for which this message is              bound. -        @type: list of twisted.mail.smtp.User +        :type: list of twisted.mail.smtp.User          @return: The full "Received" header string. -        @type: str +        :type: str          """          myHostname, clientIP = helo          headerValue = "by %s from %s with ESMTP ; %s" % ( @@ -269,8 +236,8 @@ class SMTPDelivery(object):          In the end, it returns an encrypted message object that is able to          send itself to the C{user}'s address. -        @param user: The user whose address we wish to validate. -        @type: twisted.mail.smtp.User +        :param user: The user whose address we wish to validate. +        :type: twisted.mail.smtp.User          @return: A Deferred which becomes, or a callable which takes no              arguments and returns an object implementing IMessage. This will @@ -284,12 +251,13 @@ class SMTPDelivery(object):          # try to find recipient's public key          try:              address = validate_address(user.dest.addrstr) -            pubkey = self._km.get_key(address, OpenPGPKey) +            # verify if recipient key is available in keyring +            self._km.get_key(address, OpenPGPKey)  # might raise KeyNotFound              log.msg("Accepting mail for %s..." % user.dest.addrstr)              signal(proto.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, user.dest.addrstr)          except KeyNotFound:              # if key was not found, check config to see if will send anyway. -            if self._config[ENCRYPTED_ONLY_KEY]: +            if self._encrypted_only:                  signal(proto.SMTP_RECIPIENT_REJECTED, user.dest.addrstr)                  raise smtp.SMTPBadRcpt(user.dest.addrstr)              log.msg("Warning: will send an unencrypted message (because " @@ -297,17 +265,18 @@ class SMTPDelivery(object):              signal(                  proto.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED, user.dest.addrstr)          return lambda: EncryptedMessage( -            self._origin, user, self._km, self._config) +            self._origin, user, self._km, self._host, self._port, self._cert, +            self._key)      def validateFrom(self, helo, origin):          """          Validate the address from which the message originates. -        @param helo: The argument to the HELO command and the client's IP +        :param helo: The argument to the HELO command and the client's IP              address. -        @type: (str, str) -        @param origin: The address the message is from. -        @type origin: twisted.mail.smtp.Address +        :type: (str, str) +        :param origin: The address the message is from. +        :type origin: twisted.mail.smtp.Address          @return: origin or a Deferred whose callback will be passed origin.          @rtype: Deferred or Address @@ -359,36 +328,36 @@ class EncryptedMessage(object):      """      implements(smtp.IMessage) -    def __init__(self, fromAddress, user, keymanager, config): +    def __init__(self, fromAddress, user, keymanager, host, port, cert, key):          """          Initialize the encrypted message. -        @param fromAddress: The address of the sender. -        @type fromAddress: twisted.mail.smtp.Address -        @param user: The recipient of this message. -        @type user: twisted.mail.smtp.User -        @param keymanager: A KeyManager for retrieving recipient's keys. -        @type keymanager: leap.common.keymanager.KeyManager -        @param config: A dictionary with smtp configuration. Should have -            the following structure: -                { -                    HOST_KEY: '<str>', -                    PORT_KEY: <int>, -                    CERT_KEY: '<str>', -                    KEY_KEY: '<str>', -                    ENCRYPTED_ONLY_KEY: <bool>, -                } -        @type config: dict +        :param fromAddress: The address of the sender. +        :type fromAddress: twisted.mail.smtp.Address +        :param user: The recipient of this message. +        :type user: twisted.mail.smtp.User +        :param keymanager: A KeyManager for retrieving recipient's keys. +        :type keymanager: leap.common.keymanager.KeyManager +        :param host: The hostname of the remote SMTP server. +        :type host: str +        :param port: The port of the remote SMTP server. +        :type port: int +        :param cert: The client certificate for authentication. +        :type cert: str +        :param key: The client key for authentication. +        :type key: str          """          # assert params          leap_assert_type(user, smtp.User)          leap_assert_type(keymanager, KeyManager) -        assert_config_structure(config)          # and store them          self._fromAddress = fromAddress          self._user = user          self._km = keymanager -        self._config = config +        self._host = host +        self._port = port +        self._cert = cert +        self._key = key          # initialize list for message's lines          self.lines = [] @@ -400,8 +369,8 @@ class EncryptedMessage(object):          """          Handle another line. -        @param line: The received line. -        @type line: str +        :param line: The received line. +        :type line: str          """          self.lines.append(line) @@ -440,8 +409,8 @@ class EncryptedMessage(object):          """          Callback for a successful send. -        @param r: The result from the last previous callback in the chain. -        @type r: anything +        :param r: The result from the last previous callback in the chain. +        :type r: anything          """          log.msg(r)          signal(proto.SMTP_SEND_MESSAGE_SUCCESS, self._user.dest.addrstr) @@ -450,8 +419,8 @@ class EncryptedMessage(object):          """          Callback for an unsuccessfull send. -        @param e: The result from the last errback. -        @type e: anything +        :param e: The result from the last errback. +        :type e: anything          """          log.msg(e)          log.err() @@ -470,8 +439,7 @@ class EncryptedMessage(object):          """          msg = self._msg.as_string(False) -        log.msg("Connecting to SMTP server %s:%s" % (self._config[HOST_KEY], -                                                     self._config[PORT_KEY])) +        log.msg("Connecting to SMTP server %s:%s" % (self._host, self._port))          d = defer.Deferred()          # we don't pass an ssl context factory to the ESMTPSenderFactory @@ -487,11 +455,8 @@ class EncryptedMessage(object):              requireTransportSecurity=True)          signal(proto.SMTP_SEND_MESSAGE_START, self._user.dest.addrstr)          reactor.connectSSL( -            self._config[HOST_KEY], -            self._config[PORT_KEY], -            factory, -            contextFactory=SSLContextFactory(self._config[CERT_KEY], -                                             self._config[KEY_KEY])) +            self._host, self._port, factory, +            contextFactory=SSLContextFactory(self._cert, self._key))          d.addCallback(self.sendSuccess)          d.addErrback(self.sendError)          return d @@ -505,20 +470,18 @@ class EncryptedMessage(object):          Create an RFC 3156 compliang PGP encrypted and signed message using          C{pubkey} to encrypt and C{signkey} to sign. -        @param pubkey: The public key used to encrypt the message. -        @type pubkey: leap.common.keymanager.openpgp.OpenPGPKey -        @param signkey: The private key used to sign the message. -        @type signkey: leap.common.keymanager.openpgp.OpenPGPKey +        :param pubkey: The public key used to encrypt the message. +        :type pubkey: leap.common.keymanager.openpgp.OpenPGPKey +        :param signkey: The private key used to sign the message. +        :type signkey: leap.common.keymanager.openpgp.OpenPGPKey          """ -        # parse original message from received lines -        origmsg = self.parseMessage()          # create new multipart/encrypted message with 'pgp-encrypted' protocol          newmsg = MultipartEncrypted('application/pgp-encrypted')          # move (almost) all headers from original message to the new message -        move_headers(origmsg, newmsg) +        move_headers(self._origmsg, newmsg)          # create 'application/octet-stream' encrypted message          encmsg = MIMEApplication( -            self._km.encrypt(origmsg.as_string(unixfrom=False), pubkey, +            self._km.encrypt(self._origmsg.as_string(unixfrom=False), pubkey,                               sign=signkey),              _subtype='octet-stream', _encoder=lambda x: x)          encmsg.add_header('content-disposition', 'attachment', @@ -535,25 +498,23 @@ class EncryptedMessage(object):          """          Create an RFC 3156 compliant PGP signed MIME message using C{signkey}. -        @param signkey: The private key used to sign the message. -        @type signkey: leap.common.keymanager.openpgp.OpenPGPKey +        :param signkey: The private key used to sign the message. +        :type signkey: leap.common.keymanager.openpgp.OpenPGPKey          """ -        # parse original message from received lines -        origmsg = self.parseMessage()          # create new multipart/signed message          newmsg = MultipartSigned('application/pgp-signature', 'pgp-sha512')          # move (almost) all headers from original message to the new message -        move_headers(origmsg, newmsg) +        move_headers(self._origmsg, newmsg)          # apply base64 content-transfer-encoding -        encode_base64_rec(origmsg) +        encode_base64_rec(self._origmsg)          # get message text with headers and replace \n for \r\n          fp = StringIO()          g = RFC3156CompliantGenerator(              fp, mangle_from_=False, maxheaderlen=76) -        g.flatten(origmsg) +        g.flatten(self._origmsg)          msgtext = re.sub('\r?\n', '\r\n', fp.getvalue())          # make sure signed message ends with \r\n as per OpenPGP stantard. -        if origmsg.is_multipart(): +        if self._origmsg.is_multipart():              if not msgtext.endswith("\r\n"):                  msgtext += "\r\n"          # calculate signature @@ -561,23 +522,46 @@ class EncryptedMessage(object):                                    clearsign=False, detach=True, binary=False)          sigmsg = PGPSignature(signature)          # attach original message and signature to new message -        newmsg.attach(origmsg) +        newmsg.attach(self._origmsg)          newmsg.attach(sigmsg)          self._msg = newmsg      def _maybe_encrypt_and_sign(self):          """ -        Encrypt the message body. +        Attempt to encrypt and sign the outgoing message. -        Fetch the recipient key and encrypt the content to the -        recipient. If a key is not found, then the behaviour depends on the -        configuration parameter ENCRYPTED_ONLY_KEY. If it is False, the message -        is sent unencrypted and a warning is logged. If it is True, the -        encryption fails with a KeyNotFound exception. +        The behaviour of this method depends on: -        @raise KeyNotFound: Raised when the recipient key was not found and -            the ENCRYPTED_ONLY_KEY configuration parameter is set to True. +            1. the original message's content-type, and +            2. the availability of the recipient's public key. + +        If the original message's content-type is "multipart/encrypted", then +        the original message is not altered. For any other content-type, the +        method attempts to fetch the recipient's public key. If the +        recipient's public key is available, the message is encrypted and +        signed; otherwise it is only signed. + +        Note that, if the C{encrypted_only} configuration is set to True and +        the recipient's public key is not available, then the recipient +        address would have been rejected in SMTPDelivery.validateTo(). + +        The following table summarizes the overall behaviour of the gateway: + +        +---------------------------------------------------+----------------+ +        | content-type        | rcpt pubkey | enforce encr. | action         | +        +---------------------+-------------+---------------+----------------+ +        | multipart/encrypted | any         | any           | pass           | +        | other               | available   | any           | encrypt + sign | +        | other               | unavailable | yes           | reject         | +        | other               | unavailable | no            | sign           | +        +---------------------+-------------+---------------+----------------+          """ +        # pass if the original message's content-type is "multipart/encrypted" +        self._origmsg = self.parseMessage() +        if self._origmsg.get_content_type() == 'multipart/encrypted': +            self._msg = self._origmsg +            return +          from_address = validate_address(self._fromAddress.addrstr)          signkey = self._km.get_key(from_address, OpenPGPKey, private=True)          log.msg("Will sign the message with %s." % signkey.fingerprint) diff --git a/mail/src/leap/mail/smtp/tests/__init__.py b/mail/src/leap/mail/smtp/tests/__init__.py index 9b54de3..62b015f 100644 --- a/mail/src/leap/mail/smtp/tests/__init__.py +++ b/mail/src/leap/mail/smtp/tests/__init__.py @@ -17,7 +17,7 @@  """ -Base classes and keys for SMTP relay tests. +Base classes and keys for SMTP gateway tests.  """  import os @@ -59,7 +59,7 @@ class TestCaseWithKeyManager(BaseLeapTest):          # setup our own stuff          address = 'leap@leap.se'  # user's address in the form user@provider          uuid = 'leap@leap.se' -        passphrase = '123' +        passphrase = u'123'          secrets_path = os.path.join(self.tempdir, 'secret.gpg')          local_db_path = os.path.join(self.tempdir, 'soledad.u1db')          server_url = 'http://provider/' @@ -88,6 +88,8 @@ class TestCaseWithKeyManager(BaseLeapTest):              get_doc = Mock(return_value=None)              put_doc = Mock(side_effect=_put_doc_side_effect) +            lock = Mock(return_value=('atoken', 300)) +            unlock = Mock(return_value=True)              def __call__(self):                  return self diff --git a/mail/src/leap/mail/smtp/tests/test_smtprelay.py b/mail/src/leap/mail/smtp/tests/test_gateway.py index 7fefe77..f9ea027 100644 --- a/mail/src/leap/mail/smtp/tests/test_smtprelay.py +++ b/mail/src/leap/mail/smtp/tests/test_gateway.py @@ -1,5 +1,5 @@  # -*- coding: utf-8 -*- -# test_smtprelay.py +# test_gateway.py  # Copyright (C) 2013 LEAP  #  # This program is free software: you can redistribute it and/or modify @@ -17,7 +17,7 @@  """ -SMTP relay tests. +SMTP gateway tests.  """ @@ -33,7 +33,7 @@ from twisted.mail.smtp import (  )  from mock import Mock -from leap.mail.smtp.smtprelay import ( +from leap.mail.smtp.gateway import (      SMTPFactory,      EncryptedMessage,  ) @@ -52,9 +52,9 @@ HOSTNAME_REGEX = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + \  IP_OR_HOST_REGEX = '(' + IP_REGEX + '|' + HOSTNAME_REGEX + ')' -class TestSmtpRelay(TestCaseWithKeyManager): +class TestSmtpGateway(TestCaseWithKeyManager): -    EMAIL_DATA = ['HELO relay.leap.se', +    EMAIL_DATA = ['HELO gateway.leap.se',                    'MAIL FROM: <%s>' % ADDRESS_2,                    'RCPT TO: <%s>' % ADDRESS,                    'DATA', @@ -90,7 +90,7 @@ class TestSmtpRelay(TestCaseWithKeyManager):          self.assertEqual(text, decrypted,                           "Decrypted text differs from plaintext.") -    def test_relay_accepts_valid_email(self): +    def test_gateway_accepts_valid_email(self):          """          Test if SMTP server responds correctly for valid interaction.          """ @@ -102,26 +102,32 @@ class TestSmtpRelay(TestCaseWithKeyManager):                          '250 Sender address accepted',                          '250 Recipient address accepted',                          '354 Continue'] -        proto = SMTPFactory( -            self._km, self._config).buildProtocol(('127.0.0.1', 0)) +        proto = SMTPFactory(u'anotheruser@leap.se', +            self._km, self._config['host'], self._config['port'], +            self._config['cert'], self._config['key'], +            self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0))          transport = proto_helpers.StringTransport()          proto.makeConnection(transport)          for i, line in enumerate(self.EMAIL_DATA):              proto.lineReceived(line + '\r\n')              self.assertMatch(transport.value(),                               '\r\n'.join(SMTP_ANSWERS[0:i + 1]), -                             'Did not get expected answer from relay.') +                             'Did not get expected answer from gateway.')          proto.setTimeout(None)      def test_message_encrypt(self):          """          Test if message gets encrypted to destination email.          """ -        proto = SMTPFactory( -            self._km, self._config).buildProtocol(('127.0.0.1', 0)) +        proto = SMTPFactory(u'anotheruser@leap.se', +            self._km, self._config['host'], self._config['port'], +            self._config['cert'], self._config['key'], +            self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0))          fromAddr = Address(ADDRESS_2) -        dest = User(ADDRESS, 'relay.leap.se', proto, ADDRESS) -        m = EncryptedMessage(fromAddr, dest, self._km, self._config) +        dest = User(ADDRESS, 'gateway.leap.se', proto, ADDRESS) +        m = EncryptedMessage( +            fromAddr, dest, self._km, self._config['host'], +            self._config['port'], self._config['cert'], self._config['key'])          for line in self.EMAIL_DATA[4:12]:              m.lineReceived(line)          m.eomReceived() @@ -149,11 +155,15 @@ class TestSmtpRelay(TestCaseWithKeyManager):          Test if message gets encrypted to destination email and signed with          sender key.          """ -        proto = SMTPFactory( -            self._km, self._config).buildProtocol(('127.0.0.1', 0)) -        user = User(ADDRESS, 'relay.leap.se', proto, ADDRESS) +        proto = SMTPFactory(u'anotheruser@leap.se', +            self._km, self._config['host'], self._config['port'], +            self._config['cert'], self._config['key'], +            self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) +        user = User(ADDRESS, 'gateway.leap.se', proto, ADDRESS)          fromAddr = Address(ADDRESS_2) -        m = EncryptedMessage(fromAddr, user, self._km, self._config) +        m = EncryptedMessage( +            fromAddr, user, self._km, self._config['host'], +            self._config['port'], self._config['cert'], self._config['key'])          for line in self.EMAIL_DATA[4:12]:              m.lineReceived(line)          # trigger encryption and signing @@ -185,11 +195,15 @@ class TestSmtpRelay(TestCaseWithKeyManager):          """          # mock the key fetching          self._km.fetch_keys_from_server = Mock(return_value=[]) -        proto = SMTPFactory( -            self._km, self._config).buildProtocol(('127.0.0.1', 0)) -        user = User('ihavenopubkey@nonleap.se', 'relay.leap.se', proto, ADDRESS) +        proto = SMTPFactory(u'anotheruser@leap.se', +            self._km, self._config['host'], self._config['port'], +            self._config['cert'], self._config['key'], +            self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0)) +        user = User('ihavenopubkey@nonleap.se', 'gateway.leap.se', proto, ADDRESS)          fromAddr = Address(ADDRESS_2) -        m = EncryptedMessage(fromAddr, user, self._km, self._config) +        m = EncryptedMessage( +            fromAddr, user, self._km, self._config['host'], +            self._config['port'], self._config['cert'], self._config['key'])          for line in self.EMAIL_DATA[4:12]:              m.lineReceived(line)          # trigger signing @@ -237,8 +251,10 @@ class TestSmtpRelay(TestCaseWithKeyManager):          # mock the key fetching          self._km.fetch_keys_from_server = Mock(return_value=[])          # prepare the SMTP factory -        proto = SMTPFactory( -            self._km, self._config).buildProtocol(('127.0.0.1', 0)) +        proto = SMTPFactory(u'anotheruser@leap.se', +            self._km, self._config['host'], self._config['port'], +            self._config['cert'], self._config['key'], +            self._config['encrypted_only']).buildProtocol(('127.0.0.1', 0))          transport = proto_helpers.StringTransport()          proto.makeConnection(transport)          proto.lineReceived(self.EMAIL_DATA[0] + '\r\n') @@ -263,11 +279,11 @@ class TestSmtpRelay(TestCaseWithKeyManager):          pgp.delete_key(pubkey)          # mock the key fetching          self._km.fetch_keys_from_server = Mock(return_value=[]) -        # change the configuration -        self._config['encrypted_only'] = False -        # prepare the SMTP factory -        proto = SMTPFactory( -            self._km, self._config).buildProtocol(('127.0.0.1', 0)) +        # prepare the SMTP factory with encrypted only equal to false +        proto = SMTPFactory(u'anotheruser@leap.se', +            self._km, self._config['host'], self._config['port'], +            self._config['cert'], self._config['key'], +            False).buildProtocol(('127.0.0.1', 0))          transport = proto_helpers.StringTransport()          proto.makeConnection(transport)          proto.lineReceived(self.EMAIL_DATA[0] + '\r\n') | 
