From 040faf2bf94db227246a00b38da8f92bfa0c8fa8 Mon Sep 17 00:00:00 2001 From: Duda Dornelles Date: Wed, 12 Nov 2014 18:27:46 -0200 Subject: Moving encrypt, sign and send logic from gateway (SMTP) to a MailService --- ..._encryptio_and_sending_out_of_encrypted_message | 1 + src/leap/mail/service.py | 384 +++++++++++++++++ src/leap/mail/smtp/__init__.py | 6 +- src/leap/mail/smtp/gateway.py | 458 ++------------------- src/leap/mail/smtp/tests/__init__.py | 0 src/leap/mail/smtp/tests/test_gateway.py | 185 +-------- src/leap/mail/tests/test_service.py | 185 +++++++++ src/leap/mail/utils.py | 23 ++ 8 files changed, 639 insertions(+), 603 deletions(-) create mode 100644 changes/feature-6357_factor_encryptio_and_sending_out_of_encrypted_message create mode 100644 src/leap/mail/service.py create mode 100644 src/leap/mail/smtp/tests/__init__.py create mode 100644 src/leap/mail/tests/test_service.py diff --git a/changes/feature-6357_factor_encryptio_and_sending_out_of_encrypted_message b/changes/feature-6357_factor_encryptio_and_sending_out_of_encrypted_message new file mode 100644 index 0000000..6b95c6a --- /dev/null +++ b/changes/feature-6357_factor_encryptio_and_sending_out_of_encrypted_message @@ -0,0 +1 @@ +- Creates a OutgoingMail class that has the logic for encrypting, signing and sending messages. Factors that logic out of EncryptedMessage so it can be used by other clients (Closes: #6357) diff --git a/src/leap/mail/service.py b/src/leap/mail/service.py new file mode 100644 index 0000000..d595067 --- /dev/null +++ b/src/leap/mail/service.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- +# service.py +# Copyright (C) 2013 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 . +import re +from StringIO import StringIO +from email.parser import Parser +from email.mime.application import MIMEApplication + +from OpenSSL import SSL + +from twisted.mail import smtp +from twisted.internet import reactor +from twisted.internet import defer +from twisted.internet.threads import deferToThread +from twisted.protocols.amp import ssl +from twisted.python import log + +from leap.common.check import leap_assert_type, leap_assert +from leap.common.events import proto, signal +from leap.keymanager import KeyManager +from leap.keymanager.openpgp import OpenPGPKey +from leap.keymanager.errors import KeyNotFound +from leap.mail import __version__ +from leap.mail.utils import validate_address +from leap.mail.smtp.rfc3156 import MultipartEncrypted +from leap.mail.smtp.rfc3156 import MultipartSigned +from leap.mail.smtp.rfc3156 import encode_base64_rec +from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator +from leap.mail.smtp.rfc3156 import PGPSignature +from leap.mail.smtp.rfc3156 import PGPEncrypted + + +class SSLContextFactory(ssl.ClientContextFactory): + def __init__(self, cert, key): + self.cert = cert + self.key = key + + def getContext(self): + self.method = SSL.TLSv1_METHOD # SSLv23_METHOD + ctx = ssl.ClientContextFactory.getContext(self) + ctx.use_certificate_file(self.cert) + ctx.use_privatekey_file(self.key) + return ctx + + +class OutgoingMail: + """ + A service for handling encrypted mail. + """ + + FOOTER_STRING = "I prefer encrypted email" + + def __init__(self, from_address, keymanager, cert, key, host, port): + """ + Initialize the mail service. + + :param from_address: The sender address. + :type from_address: str + :param keymanager: A KeyManager for retrieving recipient's keys. + :type keymanager: leap.common.keymanager.KeyManager + :param cert: The client certificate for SSL authentication. + :type cert: str + :param key: The client private key for SSL authentication. + :type key: str + :param host: The hostname of the remote SMTP server. + :type host: str + :param port: The port of the remote SMTP server. + :type port: int + """ + + # XXX: should we keep these checks? + # assert params + leap_assert_type(keymanager, KeyManager) + 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 != '') + + self._port = port + self._host = host + self._key = key + self._cert = cert + self._from_address = from_address + self._keymanager = keymanager + + def send_message(self, raw, recipient): + """ + Sends a message to a recipient. Maybe encrypts and signs. + + :param raw: The raw message + :type raw: str + :param recipient: The recipient for the message + :type recipient: smtp.User + :return: a deferred which delivers the message when fired + """ + d = deferToThread(lambda: self._maybe_encrypt_and_sign(raw, recipient)) + d.addCallback(self._route_msg) + d.addErrback(self.sendError) + + return d + + def sendSuccess(self, smtp_sender_result): + """ + Callback for a successful send. + + :param smtp_sender_result: The result from the ESMTPSender from _route_msg + :type smtp_sender_result: tuple(int, list(tuple)) + """ + dest_addrstr = smtp_sender_result[1][0][0] + log.msg('Message sent to %s' % dest_addrstr) + signal(proto.SMTP_SEND_MESSAGE_SUCCESS, dest_addrstr) + + def sendError(self, failure): + """ + Callback for an unsuccessfull send. + + :param e: The result from the last errback. + :type e: anything + """ + # XXX: need to get the address from the exception to send signal + # signal(proto.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) + err = failure.value + log.err(err) + raise err + + def _route_msg(self, encrypt_and_sign_result): + """ + Sends the msg using the ESMTPSenderFactory. + + :param encrypt_and_sign_result: A tuple containing the 'maybe' encrypted message and the recipient + :type encrypt_and_sign_result: tuple + """ + message, recipient = encrypt_and_sign_result + log.msg("Connecting to SMTP server %s:%s" % (self._host, self._port)) + msg = message.as_string(False) + + # 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( + "", # username is blank because client auth is done on SSL protocol level + "", # password is blank because client auth is done on SSL protocol level + self._from_address, + recipient.dest.addrstr, + StringIO(msg), + d, + heloFallback=True, + requireAuthentication=False, + requireTransportSecurity=True) + factory.domain = __version__ + signal(proto.SMTP_SEND_MESSAGE_START, recipient.dest.addrstr) + reactor.connectSSL( + self._host, self._port, factory, + contextFactory=SSLContextFactory(self._cert, self._key)) + + + def _maybe_encrypt_and_sign(self, raw, recipient): + """ + 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. + + 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 | + +---------------------+-------------+---------------+----------------+ + + :param raw: The raw message + :type raw: str + :param recipient: The recipient for the message + :type: recipient: smtp.User + + """ + # pass if the original message's content-type is "multipart/encrypted" + lines = raw.split('\r\n') + origmsg = Parser().parsestr(raw) + + if origmsg.get_content_type() == 'multipart/encrypted': + return origmsg + + from_address = validate_address(self._from_address) + username, domain = from_address.split('@') + + # add a nice footer to the outgoing message + # XXX: footer will eventually optional or be removed + if origmsg.get_content_type() == 'text/plain': + lines.append('--') + lines.append('%s - https://%s/key/%s' % + (self.FOOTER_STRING, domain, username)) + lines.append('') + + origmsg = Parser().parsestr('\r\n'.join(lines)) + + # get sender and recipient data + signkey = self._keymanager.get_key(from_address, OpenPGPKey, private=True) + log.msg("Will sign the message with %s." % signkey.fingerprint) + to_address = validate_address(recipient.dest.addrstr) + try: + # try to get the recipient pubkey + pubkey = self._keymanager.get_key(to_address, OpenPGPKey) + log.msg("Will encrypt the message to %s." % pubkey.fingerprint) + signal(proto.SMTP_START_ENCRYPT_AND_SIGN, + "%s,%s" % (self._from_address, to_address)) + newmsg = self._encrypt_and_sign(origmsg, pubkey, signkey) + + signal(proto.SMTP_END_ENCRYPT_AND_SIGN, + "%s,%s" % (self._from_address, to_address)) + except KeyNotFound: + # at this point we _can_ send unencrypted mail, because if the + # configuration said the opposite the address would have been + # rejected in SMTPDelivery.validateTo(). + log.msg('Will send unencrypted message to %s.' % to_address) + signal(proto.SMTP_START_SIGN, self._from_address) + newmsg = self._sign(origmsg, signkey) + signal(proto.SMTP_END_SIGN, self._from_address) + return newmsg, recipient + + + def _encrypt_and_sign(self, origmsg, pubkey, signkey): + """ + Create an RFC 3156 compliang PGP encrypted and signed message using + C{pubkey} to encrypt and C{signkey} to sign. + + :param origmsg: The original message + :type origmsg: email.message.Message + :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 + :return: The encrypted and signed message + :rtype: MultipartEncrypted + """ + # 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 + self._fix_headers(origmsg, newmsg, signkey) + # create 'application/octet-stream' encrypted message + encmsg = MIMEApplication( + self._keymanager.encrypt(origmsg.as_string(unixfrom=False), pubkey, + sign=signkey), + _subtype='octet-stream', _encoder=lambda x: x) + encmsg.add_header('content-disposition', 'attachment', + filename='msg.asc') + # create meta message + metamsg = PGPEncrypted() + metamsg.add_header('Content-Disposition', 'attachment') + # attach pgp message parts to new message + newmsg.attach(metamsg) + newmsg.attach(encmsg) + return newmsg + + + def _sign(self, origmsg, signkey): + """ + Create an RFC 3156 compliant PGP signed MIME message using C{signkey}. + + :param origmsg: The original message + :type origmsg: email.message.Message + :param signkey: The private key used to sign the message. + :type signkey: leap.common.keymanager.openpgp.OpenPGPKey + :return: The signed message. + :rtype: MultipartSigned + """ + # create new multipart/signed message + newmsg = MultipartSigned('application/pgp-signature', 'pgp-sha512') + # move (almost) all headers from original message to the new message + self._fix_headers(origmsg, newmsg, signkey) + # apply base64 content-transfer-encoding + encode_base64_rec(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) + 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 not msgtext.endswith("\r\n"): + msgtext += "\r\n" + # calculate signature + signature = self._keymanager.sign(msgtext, signkey, digest_algo='SHA512', + clearsign=False, detach=True, binary=False) + sigmsg = PGPSignature(signature) + # attach original message and signature to new message + newmsg.attach(origmsg) + newmsg.attach(sigmsg) + return newmsg + + + 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/key/%s' % (domain, username), + preference='signencrypt') + # delete user-agent from origmsg + del (origmsg['user-agent']) diff --git a/src/leap/mail/smtp/__init__.py b/src/leap/mail/smtp/__init__.py index bbd4064..f740f5e 100644 --- a/src/leap/mail/smtp/__init__.py +++ b/src/leap/mail/smtp/__init__.py @@ -22,6 +22,8 @@ import logging from twisted.internet import reactor from twisted.internet.error import CannotListenError +from twisted.mail import smtp +from leap.mail.service import OutgoingMail logger = logging.getLogger(__name__) @@ -59,8 +61,8 @@ def setup_smtp_gateway(port, userid, keymanager, smtp_host, smtp_port, :returns: tuple of SMTPFactory, twisted.internet.tcp.Port """ # configure the use of this service with twistd - factory = SMTPFactory(userid, keymanager, smtp_host, smtp_port, smtp_cert, - smtp_key, encrypted_only) + outgoing_mail = OutgoingMail(str(userid), keymanager, smtp_cert, smtp_key, smtp_host, smtp_port) + factory = SMTPFactory(userid, keymanager, encrypted_only, outgoing_mail) try: tport = reactor.listenTCP(port, factory, interface="localhost") signal(proto.SMTP_SERVICE_STARTED, str(port)) diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py index 13d3bbf..b022091 100644 --- a/src/leap/mail/smtp/gateway.py +++ b/src/leap/mail/smtp/gateway.py @@ -31,39 +31,27 @@ The following classes comprise the SMTP gateway service: """ -import re -from StringIO import StringIO -from email.Header import Header -from email.utils import parseaddr -from email.parser import Parser -from email.mime.application import MIMEApplication from zope.interface import implements -from OpenSSL import SSL 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 +from email.Header import Header +from leap.common.check import leap_assert_type from leap.common.events import proto, signal -from leap.keymanager import KeyManager from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import KeyNotFound -from leap.mail import __version__ +from leap.mail.utils import validate_address + from leap.mail.smtp.rfc3156 import ( - MultipartSigned, - MultipartEncrypted, - PGPEncrypted, - PGPSignature, RFC3156CompliantGenerator, - encode_base64_rec, ) +from leap.mail.service import OutgoingMail # replace email generator with a RFC 3156 compliant one. from email import generator + generator.Generator = RFC3156CompliantGenerator @@ -74,31 +62,6 @@ generator.Generator = RFC3156CompliantGenerator 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 - - @return: A valid address. - @rtype: str - - @raise smtp.SMTPBadRcpt: Raised if C{address} is invalid. - """ - leap_assert_type(address, str) - # in the following, the address is parsed as described in RFC 2822 and - # ('', '') is returned if the parse fails. - _, address = parseaddr(address) - if address == '': - raise smtp.SMTPBadRcpt(address) - return address - - -# -# SMTPFactory -# - class SMTPHeloLocalhost(smtp.SMTP): """ An SMTP class that ensures a proper FQDN @@ -119,45 +82,26 @@ class SMTPFactory(ServerFactory): """ domain = LOCAL_FQDN - def __init__(self, userid, keymanager, host, port, cert, key, - encrypted_only): + def __init__(self, userid, keymanager, encrypted_only, outgoing_mail): """ Initialize the SMTP factory. :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 keymanager: A Key Manager from where to get recipients' public + keys. :param encrypted_only: Whether the SMTP gateway should send unencrypted mail or not. :type encrypted_only: bool + :param outgoing_mail: The outgoing mail to send the message + :type outgoing_mail: leap.mail.service.OutgoingMail """ - # assert params - leap_assert_type(keymanager, KeyManager) - 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._host = host - self._port = port - self._cert = cert - self._key = key + self._outgoing_mail = outgoing_mail self._encrypted_only = encrypted_only def buildProtocol(self, addr): @@ -170,9 +114,7 @@ class SMTPFactory(ServerFactory): @return: The protocol. @rtype: SMTPDelivery """ - smtpProtocol = SMTPHeloLocalhost(SMTPDelivery( - self._userid, self._km, self._host, self._port, self._cert, - self._key, self._encrypted_only)) + smtpProtocol = SMTPHeloLocalhost(SMTPDelivery(self._userid, self._km, self._encrypted_only, self._outgoing_mail)) smtpProtocol.factory = self return smtpProtocol @@ -188,33 +130,23 @@ class SMTPDelivery(object): implements(smtp.IMessageDelivery) - def __init__(self, userid, keymanager, host, port, cert, key, - encrypted_only): + def __init__(self, userid, keymanager, encrypted_only, outgoing_mail): """ Initialize the SMTP delivery object. :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 keymanager: A Key Manager from where to get recipients' public + keys. :param encrypted_only: Whether the SMTP gateway should send unencrypted mail or not. :type encrypted_only: bool + :param outgoing_mail: The outgoing mail to send the message + :type outgoing_mail: leap.mail.service.OutgoingMail """ self._userid = userid + self._outgoing_mail = outgoing_mail self._km = keymanager - self._host = host - self._port = port - self._cert = cert - self._key = key self._encrypted_only = encrypted_only self._origin = None @@ -280,9 +212,7 @@ class SMTPDelivery(object): "encrypted_only' is set to False).") signal( proto.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED, user.dest.addrstr) - return lambda: EncryptedMessage( - self._origin, user, self._km, self._host, self._port, self._cert, - self._key) + return lambda: EncryptedMessage(user, self._outgoing_mail) def validateFrom(self, helo, origin): """ @@ -314,19 +244,6 @@ class SMTPDelivery(object): # EncryptedMessage # -class SSLContextFactory(ssl.ClientContextFactory): - def __init__(self, cert, key): - self.cert = cert - self.key = key - - def getContext(self): - self.method = SSL.TLSv1_METHOD # SSLv23_METHOD - ctx = ssl.ClientContextFactory.getContext(self) - ctx.use_certificate_file(self.cert) - ctx.use_privatekey_file(self.key) - return ctx - - class EncryptedMessage(object): """ Receive plaintext from client, encrypt it and send message to a @@ -334,44 +251,21 @@ class EncryptedMessage(object): """ implements(smtp.IMessage) - FOOTER_STRING = "I prefer encrypted email" - - def __init__(self, fromAddress, user, keymanager, host, port, cert, key): + def __init__(self, user, outgoing_mail): """ 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 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 outgoing_mail: The outgoing mail to send the message + :type outgoing_mail: leap.mail.service.OutgoingMail """ # assert params leap_assert_type(user, smtp.User) - leap_assert_type(keymanager, KeyManager) - # and store them - self._fromAddress = fromAddress - self._user = user - self._km = keymanager - self._host = host - self._port = port - self._cert = cert - self._key = key - # initialize list for message's lines - self.lines = [] - # - # methods from smtp.IMessage - # + self._user = user + self._lines = [] + self._outgoing_mail = outgoing_mail def lineReceived(self, line): """ @@ -380,7 +274,7 @@ class EncryptedMessage(object): :param line: The received line. :type line: str """ - self.lines.append(line) + self._lines.append(line) def eomReceived(self): """ @@ -391,10 +285,10 @@ class EncryptedMessage(object): :returns: a deferred """ log.msg("Message data complete.") - self.lines.append('') # add a trailing newline - d = deferToThread(self._maybe_encrypt_and_sign) - d.addCallbacks(self.sendMessage, self.skipNoKeyErrBack) - return d + self._lines.append('') # add a trailing newline + raw_mail = '\r\n'.join(self._lines) + + return self._outgoing_mail.send_message(raw_mail, self._user) def connectionLost(self): """ @@ -404,290 +298,4 @@ class EncryptedMessage(object): log.err() signal(proto.SMTP_CONNECTION_LOST, self._user.dest.addrstr) # 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 - """ - log.msg(r) - signal(proto.SMTP_SEND_MESSAGE_SUCCESS, self._user.dest.addrstr) - - def sendError(self, failure): - """ - Callback for an unsuccessfull send. - - :param e: The result from the last errback. - :type e: anything - """ - signal(proto.SMTP_SEND_MESSAGE_ERROR, self._user.dest.addrstr) - err = failure.value - log.err(err) - raise err - - def sendMessage(self, *args): - """ - Sends the message. - - :return: A deferred with callback and errback for - this #message send. - :rtype: twisted.internet.defer.Deferred - """ - d = deferToThread(self._route_msg) - d.addCallbacks(self.sendQueued, self.sendError) - return d - - 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) - - # 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( - "", # username is blank because server does not use auth. - "", # password is blank because server does not use auth. - self._fromAddress.addrstr, - self._user.dest.addrstr, - StringIO(msg), - d, - heloFallback=True, - requireAuthentication=False, - requireTransportSecurity=True) - factory.domain = __version__ - signal(proto.SMTP_SEND_MESSAGE_START, self._user.dest.addrstr) - reactor.connectSSL( - self._host, self._port, factory, - contextFactory=SSLContextFactory(self._cert, self._key)) - - # - # encryption methods - # - - def _encrypt_and_sign(self, pubkey, signkey): - """ - 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: OpenPGPKey - :param signkey: The private key used to sign the message. - :type signkey: OpenPGPKey - """ - # 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 - self._fix_headers(self._origmsg, newmsg, signkey) - # create 'application/octet-stream' encrypted message - encmsg = MIMEApplication( - 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', - filename='msg.asc') - # create meta message - metamsg = PGPEncrypted() - metamsg.add_header('Content-Disposition', 'attachment') - # attach pgp message parts to new message - newmsg.attach(metamsg) - newmsg.attach(encmsg) - self._msg = newmsg - - def _sign(self, signkey): - """ - 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 - """ - # create new multipart/signed message - newmsg = MultipartSigned('application/pgp-signature', 'pgp-sha512') - # move (almost) all headers from original message to the new message - self._fix_headers(self._origmsg, newmsg, signkey) - # apply base64 content-transfer-encoding - 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(self._origmsg) - msgtext = re.sub('\r?\n', '\r\n', fp.getvalue()) - # make sure signed message ends with \r\n as per OpenPGP stantard. - if self._origmsg.is_multipart(): - if not msgtext.endswith("\r\n"): - msgtext += "\r\n" - # calculate signature - signature = self._km.sign(msgtext, signkey, digest_algo='SHA512', - clearsign=False, detach=True, binary=False) - sigmsg = PGPSignature(signature) - # attach original message and signature to new message - newmsg.attach(self._origmsg) - newmsg.attach(sigmsg) - self._msg = newmsg - - def _maybe_encrypt_and_sign(self): - """ - 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. - - 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) - username, domain = from_address.split('@') - - # add a nice footer to the outgoing message - if self._origmsg.get_content_type() == 'text/plain': - self.lines.append('--') - self.lines.append('%s - https://%s/key/%s' % - (self.FOOTER_STRING, domain, username)) - self.lines.append('') - - self._origmsg = self.parseMessage() - - # get sender and recipient data - signkey = self._km.get_key(from_address, OpenPGPKey, private=True) - log.msg("Will sign the message with %s." % signkey.fingerprint) - to_address = validate_address(self._user.dest.addrstr) - try: - # try to get the recipient pubkey - pubkey = self._km.get_key(to_address, OpenPGPKey) - log.msg("Will encrypt the message to %s." % pubkey.fingerprint) - signal(proto.SMTP_START_ENCRYPT_AND_SIGN, - "%s,%s" % (self._fromAddress.addrstr, to_address)) - self._encrypt_and_sign(pubkey, signkey) - signal(proto.SMTP_END_ENCRYPT_AND_SIGN, - "%s,%s" % (self._fromAddress.addrstr, to_address)) - except KeyNotFound: - # at this point we _can_ send unencrypted mail, because if the - # configuration said the opposite the address would have been - # rejected in SMTPDelivery.validateTo(). - log.msg('Will send unencrypted message to %s.' % to_address) - 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/key/%s' % (domain, username), - preference='signencrypt') - # delete user-agent from origmsg - del(origmsg['user-agent']) + self._lines = [] diff --git a/src/leap/mail/smtp/tests/__init__.py b/src/leap/mail/smtp/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/leap/mail/smtp/tests/test_gateway.py b/src/leap/mail/smtp/tests/test_gateway.py index 3635a9f..aeace4a 100644 --- a/src/leap/mail/smtp/tests/test_gateway.py +++ b/src/leap/mail/smtp/tests/test_gateway.py @@ -20,17 +20,14 @@ SMTP gateway tests. """ - import re from datetime import datetime + from twisted.test import proto_helpers -from twisted.mail.smtp import User, Address from mock import Mock - from leap.mail.smtp.gateway import ( - SMTPFactory, - EncryptedMessage, + SMTPFactory ) from leap.mail.tests import ( TestCaseWithKeyManager, @@ -39,6 +36,7 @@ from leap.mail.tests import ( ) from leap.keymanager import openpgp + # some regexps IP_REGEX = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}" + \ "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" @@ -71,20 +69,6 @@ class TestSmtpGateway(TestCaseWithKeyManager): % (string, pattern)) raise self.failureException(msg) - def test_openpgp_encrypt_decrypt(self): - "Test if openpgp can encrypt and decrypt." - text = "simple raw text" - pubkey = self._km.get_key( - ADDRESS, openpgp.OpenPGPKey, private=False) - encrypted = self._km.encrypt(text, pubkey) - self.assertNotEqual( - text, encrypted, "Ciphertext is equal to plaintext.") - privkey = self._km.get_key( - ADDRESS, openpgp.OpenPGPKey, private=True) - decrypted = self._km.decrypt(encrypted, privkey) - self.assertEqual(text, decrypted, - "Decrypted text differs from plaintext.") - def test_gateway_accepts_valid_email(self): """ Test if SMTP server responds correctly for valid interaction. @@ -102,10 +86,8 @@ class TestSmtpGateway(TestCaseWithKeyManager): # method... 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)) + self._km, + self._config['encrypted_only'], outgoing_mail=Mock()).buildProtocol(('127.0.0.1', 0)) # snip... transport = proto_helpers.StringTransport() proto.makeConnection(transport) @@ -116,151 +98,6 @@ class TestSmtpGateway(TestCaseWithKeyManager): '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( - 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, '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() # this includes a defer, so we avoid calling it here - m.lines.append('') # add a trailing newline - # we need to call the following explicitelly because it was deferred - # inside the previous method - m._maybe_encrypt_and_sign() - # assert structure of encrypted message - self.assertTrue('Content-Type' in m._msg) - self.assertEqual('multipart/encrypted', m._msg.get_content_type()) - self.assertEqual('application/pgp-encrypted', - m._msg.get_param('protocol')) - self.assertEqual(2, len(m._msg.get_payload())) - self.assertEqual('application/pgp-encrypted', - m._msg.get_payload(0).get_content_type()) - self.assertEqual('application/octet-stream', - m._msg.get_payload(1).get_content_type()) - privkey = self._km.get_key( - ADDRESS, openpgp.OpenPGPKey, private=True) - decrypted = self._km.decrypt( - m._msg.get_payload(1).get_payload(), privkey) - self.assertEqual( - '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n\r\n--\r\n' + - 'I prefer encrypted email - https://leap.se/key/anotheruser\r\n', - decrypted, - 'Decrypted text differs from plaintext.') - - def test_message_encrypt_sign(self): - """ - Test if message gets encrypted to destination email and signed with - sender key. - """ - 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['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 - # m.eomReceived() # this includes a defer, so we avoid calling it here - m.lines.append('') # add a trailing newline - # we need to call the following explicitelly because it was deferred - # inside the previous method - m._maybe_encrypt_and_sign() - # assert structure of encrypted message - self.assertTrue('Content-Type' in m._msg) - self.assertEqual('multipart/encrypted', m._msg.get_content_type()) - self.assertEqual('application/pgp-encrypted', - m._msg.get_param('protocol')) - self.assertEqual(2, len(m._msg.get_payload())) - self.assertEqual('application/pgp-encrypted', - m._msg.get_payload(0).get_content_type()) - self.assertEqual('application/octet-stream', - m._msg.get_payload(1).get_content_type()) - # decrypt and verify - privkey = self._km.get_key( - ADDRESS, openpgp.OpenPGPKey, private=True) - pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) - decrypted = self._km.decrypt( - m._msg.get_payload(1).get_payload(), privkey, verify=pubkey) - self.assertEqual( - '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n\r\n--\r\n' + - 'I prefer encrypted email - https://leap.se/key/anotheruser\r\n', - decrypted, - 'Decrypted text differs from plaintext.') - - def test_message_sign(self): - """ - Test if message is signed with sender key. - """ - # mock the key fetching - self._km.fetch_keys_from_server = Mock(return_value=[]) - 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['host'], - self._config['port'], self._config['cert'], self._config['key']) - for line in self.EMAIL_DATA[4:12]: - m.lineReceived(line) - # trigger signing - # m.eomReceived() # this includes a defer, so we avoid calling it here - m.lines.append('') # add a trailing newline - # we need to call the following explicitelly because it was deferred - # inside the previous method - m._maybe_encrypt_and_sign() - # assert structure of signed message - self.assertTrue('Content-Type' in m._msg) - self.assertEqual('multipart/signed', m._msg.get_content_type()) - self.assertEqual('application/pgp-signature', - m._msg.get_param('protocol')) - self.assertEqual('pgp-sha512', m._msg.get_param('micalg')) - # assert content of message - self.assertEqual( - '\r\n'.join(self.EMAIL_DATA[9:13]) + '\r\n--\r\n' + - 'I prefer encrypted email - https://leap.se/key/anotheruser\r\n', - m._msg.get_payload(0).get_payload(decode=True)) - # assert content of signature - self.assertTrue( - m._msg.get_payload(1).get_payload().startswith( - '-----BEGIN PGP SIGNATURE-----\n'), - 'Message does not start with signature header.') - self.assertTrue( - m._msg.get_payload(1).get_payload().endswith( - '-----END PGP SIGNATURE-----\n'), - 'Message does not end with signature footer.') - # assert signature is valid - pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) - # replace EOL before verifying (according to rfc3156) - signed_text = re.sub('\r?\n', '\r\n', - m._msg.get_payload(0).as_string()) - self.assertTrue( - self._km.verify(signed_text, - pubkey, - detached_sig=m._msg.get_payload(1).get_payload()), - 'Signature could not be verified.') - def test_missing_key_rejects_address(self): """ Test if server rejects to send unencrypted when 'encrypted_only' is @@ -276,10 +113,8 @@ class TestSmtpGateway(TestCaseWithKeyManager): # prepare the SMTP factory 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)) + self._km, + self._config['encrypted_only'], outgoing_mail=Mock()).buildProtocol(('127.0.0.1', 0)) transport = proto_helpers.StringTransport() proto.makeConnection(transport) proto.lineReceived(self.EMAIL_DATA[0] + '\r\n') @@ -307,10 +142,8 @@ class TestSmtpGateway(TestCaseWithKeyManager): # 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)) + self._km, + False, outgoing_mail=Mock()).buildProtocol(('127.0.0.1', 0)) transport = proto_helpers.StringTransport() proto.makeConnection(transport) proto.lineReceived(self.EMAIL_DATA[0] + '\r\n') diff --git a/src/leap/mail/tests/test_service.py b/src/leap/mail/tests/test_service.py new file mode 100644 index 0000000..f0a807d --- /dev/null +++ b/src/leap/mail/tests/test_service.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +# test_gateway.py +# Copyright (C) 2013 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 . + + +""" +SMTP gateway tests. +""" + +import re +from datetime import datetime +from twisted.mail.smtp import User, Address + +from mock import Mock + +from leap.mail.smtp.gateway import SMTPFactory +from leap.mail.service import OutgoingMail +from leap.mail.tests import ( + TestCaseWithKeyManager, + ADDRESS, + ADDRESS_2, +) +from leap.keymanager import openpgp + + +class TestOutgoingMail(TestCaseWithKeyManager): + EMAIL_DATA = ['HELO gateway.leap.se', + 'MAIL FROM: <%s>' % ADDRESS_2, + 'RCPT TO: <%s>' % ADDRESS, + 'DATA', + 'From: User <%s>' % ADDRESS_2, + 'To: Leap <%s>' % ADDRESS, + 'Date: ' + datetime.now().strftime('%c'), + 'Subject: test message', + '', + 'This is a secret message.', + 'Yours,', + 'A.', + '', + '.', + 'QUIT'] + + def setUp(self): + TestCaseWithKeyManager.setUp(self) + self.lines = [line for line in self.EMAIL_DATA[4:12]] + self.lines.append('') # add a trailing newline + self.raw = '\r\n'.join(self.lines) + self.fromAddr = ADDRESS_2 + self.outgoing_mail = OutgoingMail(self.fromAddr, self._km, self._config['cert'], self._config['key'], + self._config['host'], self._config['port']) + self.proto = SMTPFactory( + u'anotheruser@leap.se', + self._km, + self._config['encrypted_only'], + self.outgoing_mail).buildProtocol(('127.0.0.1', 0)) + self.dest = User(ADDRESS, 'gateway.leap.se', self.proto, ADDRESS) + + def test_openpgp_encrypt_decrypt(self): + "Test if openpgp can encrypt and decrypt." + text = "simple raw text" + pubkey = self._km.get_key( + ADDRESS, openpgp.OpenPGPKey, private=False) + encrypted = self._km.encrypt(text, pubkey) + self.assertNotEqual( + text, encrypted, "Ciphertext is equal to plaintext.") + privkey = self._km.get_key( + ADDRESS, openpgp.OpenPGPKey, private=True) + decrypted = self._km.decrypt(encrypted, privkey) + self.assertEqual(text, decrypted, + "Decrypted text differs from plaintext.") + + def test_message_encrypt(self): + """ + Test if message gets encrypted to destination email. + """ + + message, _ = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest) + + # assert structure of encrypted message + self.assertTrue('Content-Type' in message) + self.assertEqual('multipart/encrypted', message.get_content_type()) + self.assertEqual('application/pgp-encrypted', + message.get_param('protocol')) + self.assertEqual(2, len(message.get_payload())) + self.assertEqual('application/pgp-encrypted', + message.get_payload(0).get_content_type()) + self.assertEqual('application/octet-stream', + message.get_payload(1).get_content_type()) + privkey = self._km.get_key( + ADDRESS, openpgp.OpenPGPKey, private=True) + decrypted = self._km.decrypt( + message.get_payload(1).get_payload(), privkey) + + expected = '\n' + '\r\n'.join( + self.EMAIL_DATA[9:12]) + '\r\n\r\n--\r\n' + 'I prefer encrypted email - https://leap.se/key/anotheruser\r\n' + self.assertEqual( + expected, + decrypted, + 'Decrypted text differs from plaintext.') + + def test_message_encrypt_sign(self): + """ + Test if message gets encrypted to destination email and signed with + sender key. + """ + message, _ = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest) + + # assert structure of encrypted message + self.assertTrue('Content-Type' in message) + self.assertEqual('multipart/encrypted', message.get_content_type()) + self.assertEqual('application/pgp-encrypted', + message.get_param('protocol')) + self.assertEqual(2, len(message.get_payload())) + self.assertEqual('application/pgp-encrypted', + message.get_payload(0).get_content_type()) + self.assertEqual('application/octet-stream', + message.get_payload(1).get_content_type()) + # decrypt and verify + privkey = self._km.get_key( + ADDRESS, openpgp.OpenPGPKey, private=True) + pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) + decrypted = self._km.decrypt( + message.get_payload(1).get_payload(), privkey, verify=pubkey) + self.assertEqual( + '\n' + '\r\n'.join(self.EMAIL_DATA[9:12]) + '\r\n\r\n--\r\n' + + 'I prefer encrypted email - https://leap.se/key/anotheruser\r\n', + decrypted, + 'Decrypted text differs from plaintext.') + + def test_message_sign(self): + """ + Test if message is signed with sender key. + """ + # mock the key fetching + self._km.fetch_keys_from_server = Mock(return_value=[]) + recipient = User('ihavenopubkey@nonleap.se', + 'gateway.leap.se', self.proto, ADDRESS) + self.outgoing_mail = OutgoingMail(self.fromAddr, self._km, self._config['cert'], self._config['key'], + self._config['host'], self._config['port']) + + message, _ = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, recipient) + + # assert structure of signed message + self.assertTrue('Content-Type' in message) + self.assertEqual('multipart/signed', message.get_content_type()) + self.assertEqual('application/pgp-signature', + message.get_param('protocol')) + self.assertEqual('pgp-sha512', message.get_param('micalg')) + # assert content of message + self.assertEqual( + '\r\n'.join(self.EMAIL_DATA[9:13]) + '\r\n--\r\n' + + 'I prefer encrypted email - https://leap.se/key/anotheruser\r\n', + message.get_payload(0).get_payload(decode=True)) + # assert content of signature + self.assertTrue( + message.get_payload(1).get_payload().startswith( + '-----BEGIN PGP SIGNATURE-----\n'), + 'Message does not start with signature header.') + self.assertTrue( + message.get_payload(1).get_payload().endswith( + '-----END PGP SIGNATURE-----\n'), + 'Message does not end with signature footer.') + # assert signature is valid + pubkey = self._km.get_key(ADDRESS_2, openpgp.OpenPGPKey) + # replace EOL before verifying (according to rfc3156) + signed_text = re.sub('\r?\n', '\r\n', + message.get_payload(0).as_string()) + self.assertTrue( + self._km.verify(signed_text, + pubkey, + detached_sig=message.get_payload(1).get_payload()), + 'Signature could not be verified.') diff --git a/src/leap/mail/utils.py b/src/leap/mail/utils.py index fed24b3..457097b 100644 --- a/src/leap/mail/utils.py +++ b/src/leap/mail/utils.py @@ -17,12 +17,15 @@ """ Mail utilities. """ +from email.utils import parseaddr import json import re import traceback import Queue from leap.soledad.common.document import SoledadDocument +from leap.common.check import leap_assert_type +from twisted.mail import smtp CHARSET_PATTERN = r"""charset=([\w-]+)""" @@ -224,6 +227,26 @@ def accumulator_queue(fun, lim): return _accumulator +def validate_address(address): + """ + Validate C{address} as defined in RFC 2822. + + :param address: The address to be validated. + :type address: str + + @return: A valid address. + @rtype: str + + @raise smtp.SMTPBadRcpt: Raised if C{address} is invalid. + """ + leap_assert_type(address, str) + # in the following, the address is parsed as described in RFC 2822 and + # ('', '') is returned if the parse fails. + _, address = parseaddr(address) + if address == '': + raise smtp.SMTPBadRcpt(address) + return address + # # String manipulation # -- cgit v1.2.3