diff options
Diffstat (limited to 'src/leap/mail/smtp')
-rw-r--r-- | src/leap/mail/smtp/README.rst | 38 | ||||
-rw-r--r-- | src/leap/mail/smtp/__init__.py | 49 | ||||
-rw-r--r-- | src/leap/mail/smtp/gateway.py (renamed from src/leap/mail/smtp/smtprelay.py) | 501 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/__init__.py | 6 | ||||
-rw-r--r-- | src/leap/mail/smtp/tests/test_gateway.py (renamed from src/leap/mail/smtp/tests/test_smtprelay.py) | 72 |
5 files changed, 367 insertions, 299 deletions
diff --git a/src/leap/mail/smtp/README.rst b/src/leap/mail/smtp/README.rst index 2b2a118..f625441 100644 --- a/src/leap/mail/smtp/README.rst +++ b/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/src/leap/mail/smtp/__init__.py b/src/leap/mail/smtp/__init__.py index b30cd20..d3eb9e8 100644 --- a/src/leap/mail/smtp/__init__.py +++ b/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,66 +26,49 @@ 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, 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. :type port: int + :param userid: The user currently logged in + :type userid: unicode :param keymanager: A Key Manager from where to get recipients' public keys. :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(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/src/leap/mail/smtp/smtprelay.py b/src/leap/mail/smtp/gateway.py index fca66c0..f09ee14 100644 --- a/src/leap/mail/smtp/smtprelay.py +++ b/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 @@ -14,11 +14,10 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. - """ -LEAP 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. @@ -32,7 +31,6 @@ The following classes comprise the SMTP relay service: """ - import re from StringIO import StringIO from email.Header import Header @@ -46,6 +44,7 @@ from twisted.mail import smtp from twisted.internet.protocol import ServerFactory from twisted.internet import reactor, ssl from twisted.internet import defer +from twisted.internet.threads import deferToThread from twisted.python import log from leap.common.check import leap_assert, leap_assert_type @@ -68,67 +67,18 @@ 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] != '') +LOCAL_FQDN = "bitmask.local" 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 @@ -148,46 +98,80 @@ def validate_address(address): # SMTPFactory # +class SMTPHeloLocalhost(smtp.SMTP): + """ + An SMTP class that ensures a proper FQDN + for localhost. + + This avoids a problem in which unproperly configured providers + would complain about the helo not being a fqdn. + """ + + def __init__(self, *args): + smtp.SMTP.__init__(self, *args) + self.host = LOCAL_FQDN + + class SMTPFactory(ServerFactory): """ - Factory for an SMTP server with encrypted relaying capabilities. + Factory for an SMTP server with encrypted gatewaying capabilities. """ + domain = LOCAL_FQDN - def __init__(self, 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 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._km, self._config)) + smtpProtocol = SMTPHeloLocalhost(SMTPDelivery( + self._userid, self._km, self._host, self._port, self._cert, + self._key, self._encrypted_only)) smtpProtocol.factory = self return smtpProtocol @@ -203,52 +187,57 @@ class SMTPDelivery(object): implements(smtp.IMessageDelivery) - def __init__(self, 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 - """ - # assert params - leap_assert_type(keymanager, KeyManager) - assert_config_structure(config) - # and store them + :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 + """ + 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 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" % ( - myHostname, clientIP, smtp.rfc822date()) + headerValue = "by bitmask.local from %s with ESMTP ; %s" % ( + clientIP, smtp.rfc822date()) # email.Header.Header used for automatic wrapping of long lines - return "Received: %s" % Header(headerValue) + return "Received: %s" % Header(s=headerValue, header_name='Received') def validateTo(self, user): """ @@ -262,8 +251,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 @@ -277,12 +266,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 " @@ -290,17 +280,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 @@ -310,6 +301,10 @@ class SMTPDelivery(object): """ # accept mail from anywhere. To reject an address, raise # smtp.SMTPBadSender here. + if str(origin) != str(self._userid): + log.msg("Rejecting sender {0}, expected {1}".format(origin, + self._userid)) + raise smtp.SMTPBadSender(origin) self._origin = origin return origin @@ -331,16 +326,6 @@ class SSLContextFactory(ssl.ClientContextFactory): return ctx -def move_headers(origmsg, newmsg): - headers = origmsg.items() - unwanted_headers = ['content-type', 'mime-version', 'content-disposition', - 'content-transfer-encoding'] - headers = filter(lambda x: x[0].lower() not in unwanted_headers, headers) - for hkey, hval in headers: - newmsg.add_header(hkey, hval) - del(origmsg[hkey]) - - class EncryptedMessage(object): """ Receive plaintext from client, encrypt it and send message to a @@ -348,36 +333,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 = [] @@ -389,8 +374,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) @@ -399,21 +384,14 @@ class EncryptedMessage(object): Handle end of message. This method will encrypt and send the message. + + :returns: a deferred """ log.msg("Message data complete.") self.lines.append('') # add a trailing newline - try: - self._maybe_encrypt_and_sign() - return self.sendMessage() - except KeyNotFound: - return None - - def parseMessage(self): - """ - Separate message headers from body. - """ - parser = Parser() - return parser.parsestr('\r\n'.join(self.lines)) + d = deferToThread(self._maybe_encrypt_and_sign) + d.addCallbacks(self.sendMessage, self.skipNoKeyErrBack) + return d def connectionLost(self): """ @@ -425,44 +403,81 @@ class EncryptedMessage(object): # unexpected loss of connection; don't save self.lines = [] + # ends IMessage implementation + + def skipNoKeyErrBack(self, failure): + """ + Errback that ignores a KeyNotFound + + :param failure: the failure + :type Failure: Failure + """ + err = failure.value + if failure.check(KeyNotFound): + pass + else: + raise err + + def parseMessage(self): + """ + Separate message headers from body. + """ + parser = Parser() + return parser.parsestr('\r\n'.join(self.lines)) + + def sendQueued(self, r): + """ + Callback for the queued message. + + :param r: The result from the last previous callback in the chain. + :type r: anything + """ + log.msg(r) + def sendSuccess(self, r): """ 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) - def sendError(self, e): + def sendError(self, failure): """ 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() signal(proto.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) + err = failure.value + log.err(err) + raise err - def sendMessage(self): + def sendMessage(self, *args): """ - Send the message. + Sends the message. - This method will prepare the message (headers and possibly encrypted - body) and send it using the ESMTPSenderFactory. + :return: A deferred with callbacks for error and success of this + #message send. + :rtype: twisted.internet.defer.Deferred + """ + d = deferToThread(self._route_msg) + d.addCallbacks(self.sendQueued, self.sendError) + return - @return: A deferred with callbacks for error and success of this - message send. - @rtype: twisted.internet.defer.Deferred + def _route_msg(self): + """ + Sends the msg using the ESMTPSenderFactory. """ + log.msg("Connecting to SMTP server %s:%s" % (self._host, self._port)) msg = self._msg.as_string(False) - log.msg("Connecting to SMTP server %s:%s" % (self._config[HOST_KEY], - self._config[PORT_KEY])) - + # we construct a defer to pass to the ESMTPSenderFactory d = defer.Deferred() + d.addCallbacks(self.sendSuccess, self.sendError) # we don't pass an ssl context factory to the ESMTPSenderFactory # because ssl will be handled by reactor.connectSSL() below. factory = smtp.ESMTPSenderFactory( @@ -472,18 +487,14 @@ class EncryptedMessage(object): self._user.dest.addrstr, StringIO(msg), d, + heloFallback=True, requireAuthentication=False, requireTransportSecurity=True) + factory.domain = LOCAL_FQDN 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])) - d.addCallback(self.sendSuccess) - d.addErrback(self.sendError) - return d + self._host, self._port, factory, + contextFactory=SSLContextFactory(self._cert, self._key)) # # encryption methods @@ -494,20 +505,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: OpenPGPKey + :param signkey: The private key used to sign the message. + :type signkey: 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) + self._fix_headers(self._origmsg, newmsg, signkey) # 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', @@ -524,25 +533,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) + self._fix_headers(self._origmsg, newmsg, signkey) # 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 @@ -550,23 +557,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. + + The behaviour of this method depends on: + + 1. the original message's content-type, and + 2. the availability of the recipient's public key. - 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. + 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. - @raise KeyNotFound: Raised when the recipient key was not found and - the ENCRYPTED_ONLY_KEY configuration parameter is set to True. + 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) @@ -588,3 +618,60 @@ class EncryptedMessage(object): signal(proto.SMTP_START_SIGN, self._fromAddress.addrstr) self._sign(signkey) signal(proto.SMTP_END_SIGN, self._fromAddress.addrstr) + + def _fix_headers(self, origmsg, newmsg, signkey): + """ + Move some headers from C{origmsg} to C{newmsg}, delete unwanted + headers from C{origmsg} and add new headers to C{newms}. + + Outgoing messages are either encrypted and signed or just signed + before being sent. Because of that, they are packed inside new + messages and some manipulation has to be made on their headers. + + Allowed headers for passing through: + + - From + - Date + - To + - Subject + - Reply-To + - References + - In-Reply-To + - Cc + + Headers to be added: + + - Message-ID (i.e. should not use origmsg's Message-Id) + - Received (this is added automatically by twisted smtp API) + - OpenPGP (see #4447) + + Headers to be deleted: + + - User-Agent + + :param origmsg: The original message. + :type origmsg: email.message.Message + :param newmsg: The new message being created. + :type newmsg: email.message.Message + :param signkey: The key used to sign C{newmsg} + :type signkey: OpenPGPKey + """ + # move headers from origmsg to newmsg + headers = origmsg.items() + passthrough = [ + 'from', 'date', 'to', 'subject', 'reply-to', 'references', + 'in-reply-to', 'cc' + ] + headers = filter(lambda x: x[0].lower() in passthrough, headers) + for hkey, hval in headers: + newmsg.add_header(hkey, hval) + del(origmsg[hkey]) + # add a new message-id to newmsg + newmsg.add_header('Message-Id', smtp.messageid()) + # add openpgp header to newmsg + username, domain = signkey.address.split('@') + newmsg.add_header( + 'OpenPGP', 'id=%s' % signkey.key_id, + url='https://%s/openpgp/%s' % (domain, username)) + # delete user-agent from origmsg + del(origmsg['user-agent']) diff --git a/src/leap/mail/smtp/tests/__init__.py b/src/leap/mail/smtp/tests/__init__.py index 9b54de3..62b015f 100644 --- a/src/leap/mail/smtp/tests/__init__.py +++ b/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/src/leap/mail/smtp/tests/test_smtprelay.py b/src/leap/mail/smtp/tests/test_gateway.py index 7fefe77..f9ea027 100644 --- a/src/leap/mail/smtp/tests/test_smtprelay.py +++ b/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') |