summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/mail/smtp
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2016-08-29 23:10:17 -0400
committerKali Kaneko (leap communications) <kali@leap.se>2016-08-29 23:11:41 -0400
commit5a3a2012bb8982ad0884ed659e61e969345e6fde (patch)
treefc2310d8d3244987bf5a1d2632cab99a60ba93f1 /src/leap/bitmask/mail/smtp
parent43df4205af42fce5d097f70bb0345b69e9d16f1c (diff)
[pkg] move mail source to leap.bitmask.mail
Diffstat (limited to 'src/leap/bitmask/mail/smtp')
-rw-r--r--src/leap/bitmask/mail/smtp/README.rst44
-rw-r--r--src/leap/bitmask/mail/smtp/__init__.py73
-rw-r--r--src/leap/bitmask/mail/smtp/bounces.py90
-rw-r--r--src/leap/bitmask/mail/smtp/gateway.py413
-rw-r--r--src/leap/bitmask/mail/smtp/tests/185CA770.key79
-rw-r--r--src/leap/bitmask/mail/smtp/tests/185CA770.pub52
-rw-r--r--src/leap/bitmask/mail/smtp/tests/cert/server.crt29
-rw-r--r--src/leap/bitmask/mail/smtp/tests/cert/server.key51
-rw-r--r--src/leap/bitmask/mail/smtp/tests/mail.txt10
-rw-r--r--src/leap/bitmask/mail/smtp/tests/test_gateway.py181
10 files changed, 1022 insertions, 0 deletions
diff --git a/src/leap/bitmask/mail/smtp/README.rst b/src/leap/bitmask/mail/smtp/README.rst
new file mode 100644
index 0000000..1d3a903
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/README.rst
@@ -0,0 +1,44 @@
+Leap SMTP Gateway
+=================
+
+The Bitmask Client runs a thin SMTP gateway on the user's device, which
+intends to encrypt and sign outgoing messages to achieve point to point
+encryption.
+
+The gateway is bound to localhost and the user's MUA should be configured to
+send messages to it. After doing its thing, the gateway will relay the
+messages to the remote SMTP server.
+
+Outgoing mail workflow:
+
+ * SMTP gateway receives a message from the MUA.
+
+ * SMTP gateway queries Key Manager for the user's private key.
+
+ * For each recipient (including addresses in "To", "Cc" anc "Bcc" fields),
+ the following happens:
+
+ - The recipient's address is validated against RFC2822.
+
+ - An attempt is made to fetch the recipient's public PGP key.
+
+ - If key is not found:
+
+ - If the gateway is configured to only send encrypted messages the
+ recipient is rejected.
+
+ - Otherwise, the message is signed and sent as plain text.
+
+ - If the key is found, the message is encrypted to the recipient and
+ signed with the sender's private PGP key.
+
+ * Finally, one message for each recipient is gatewayed to provider's SMTP
+ server.
+
+
+Running tests
+-------------
+
+Tests are run using Twisted's Trial API, like this::
+
+ python setup.py test -s leap.mail.gateway.tests
diff --git a/src/leap/bitmask/mail/smtp/__init__.py b/src/leap/bitmask/mail/smtp/__init__.py
new file mode 100644
index 0000000..9fab70a
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/__init__.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+# __init__.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 <http://www.gnu.org/licenses/>.
+
+"""
+SMTP gateway helper function.
+"""
+import logging
+import os
+
+from twisted.internet import reactor
+from twisted.internet.error import CannotListenError
+
+from leap.common.events import emit_async, catalog
+
+from leap.mail.smtp.gateway import SMTPFactory
+
+logger = logging.getLogger(__name__)
+
+
+SMTP_PORT = 2013
+
+
+def run_service(soledad_sessions, keymanager_sessions, sendmail_opts,
+ port=SMTP_PORT):
+ """
+ Main entry point to run the service from the client.
+
+ :param soledad_sessions: a dict-like object, containing instances
+ of a Store (soledad instances), indexed by userid.
+ :param keymanager_sessions: a dict-like object, containing instances
+ of Keymanager, indexed by userid.
+ :param sendmail_opts: a dict-like object of sendmailOptions.
+
+ :returns: the port as returned by the reactor when starts listening, and
+ the factory for the protocol.
+ :rtype: tuple
+ """
+ factory = SMTPFactory(soledad_sessions, keymanager_sessions,
+ sendmail_opts)
+
+ try:
+ interface = "localhost"
+ # don't bind just to localhost if we are running on docker since we
+ # won't be able to access smtp from the host
+ if os.environ.get("LEAP_DOCKERIZED"):
+ interface = ''
+
+ # TODO Use Endpoints instead --------------------------------
+ tport = reactor.listenTCP(port, factory, interface=interface)
+ emit_async(catalog.SMTP_SERVICE_STARTED, str(port))
+
+ return factory, tport
+ except CannotListenError:
+ logger.error("STMP Service failed to start: "
+ "cannot listen in port %s" % port)
+ emit_async(catalog.SMTP_SERVICE_FAILED_TO_START, str(port))
+ except Exception as exc:
+ logger.error("Unhandled error while launching smtp gateway service")
+ logger.exception(exc)
diff --git a/src/leap/bitmask/mail/smtp/bounces.py b/src/leap/bitmask/mail/smtp/bounces.py
new file mode 100644
index 0000000..7a4674b
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/bounces.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# bounces.py
+# Copyright (C) 2016 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 <http://www.gnu.org/licenses/>.
+"""
+Deliver bounces to the user Inbox.
+"""
+import time
+from email.message import Message
+from email.utils import formatdate
+
+from leap.mail.constants import INBOX_NAME
+from leap.mail.mail import Account
+
+
+# TODO implement localization for this template.
+
+BOUNCE_TEMPLATE = """This is your local Bitmask Mail Agent running at localhost.
+
+I'm sorry to have to inform you that your message could not be delivered to one
+or more recipients.
+
+The reasons I got for the error are:
+
+{raw_error}
+
+If the problem persists and it's not a network connectivity issue, you might
+want to contact your provider ({provider}) with this information (remove any
+sensitive data before).
+
+--- Original message (*before* it was encrypted by bitmask) below ----:
+
+{orig}"""
+
+
+class Bouncer(object):
+ """
+ Implements a mechanism to deliver bounces to user inbox.
+ """
+ # TODO this should follow RFC 6522, and compose a correct multipart
+ # attaching the report and the original message. Leaving this for a future
+ # iteration.
+
+ def __init__(self, inbox_collection):
+ self._inbox_collection = inbox_collection
+
+ def bounce_message(self, error_data, to, date=None, orig=''):
+ if not date:
+ date = formatdate(time.time())
+
+ raw_data = self._format_msg(error_data, to, date, orig)
+ d = self._inbox_collection.add_msg(
+ raw_data, ('\\Recent',), date=date)
+ return d
+
+ def _format_msg(self, error_data, to, date, orig):
+ provider = to.split('@')[1]
+
+ msg = Message()
+ msg.add_header(
+ 'From', 'bitmask-bouncer@localhost (Bitmask Local Agent)')
+ msg.add_header('To', to)
+ msg.add_header('Subject', 'Undelivered Message')
+ msg.add_header('Date', date)
+ msg.set_payload(BOUNCE_TEMPLATE.format(
+ raw_error=error_data,
+ provider=provider,
+ orig=orig))
+
+ return msg.as_string()
+
+
+def bouncerFactory(soledad):
+ user_id = soledad.uuid
+ acc = Account(soledad, user_id)
+ d = acc.callWhenReady(lambda _: acc.get_collection_by_mailbox(INBOX_NAME))
+ d.addCallback(lambda inbox: Bouncer(inbox))
+ return d
diff --git a/src/leap/bitmask/mail/smtp/gateway.py b/src/leap/bitmask/mail/smtp/gateway.py
new file mode 100644
index 0000000..e49bbe8
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/gateway.py
@@ -0,0 +1,413 @@
+# -*- coding: utf-8 -*-
+# 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 <http://www.gnu.org/licenses/>.
+"""
+LEAP SMTP encrypted gateway.
+
+The following classes comprise the SMTP gateway service:
+
+ * SMTPFactory - A twisted.internet.protocol.ServerFactory that provides
+ the SMTPDelivery protocol.
+
+ * SMTPDelivery - A twisted.mail.smtp.IMessageDelivery implementation. It
+ knows how to validate sender and receiver of messages and it generates
+ an EncryptedMessage for each recipient.
+
+ * EncryptedMessage - An implementation of twisted.mail.smtp.IMessage that
+ knows how to encrypt/sign itself before sending.
+"""
+from email.Header import Header
+
+from zope.interface import implements
+from zope.interface import implementer
+
+from twisted.cred.portal import Portal, IRealm
+from twisted.mail import smtp
+from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials
+from twisted.internet import defer, protocol
+from twisted.python import log
+
+from leap.common.check import leap_assert_type
+from leap.common.events import emit_async, catalog
+from leap.mail import errors
+from leap.mail.cred import LocalSoledadTokenChecker
+from leap.mail.utils import validate_address
+from leap.mail.rfc3156 import RFC3156CompliantGenerator
+from leap.mail.outgoing.service import outgoingFactory
+from leap.mail.smtp.bounces import bouncerFactory
+from leap.keymanager.errors import KeyNotFound
+
+# replace email generator with a RFC 3156 compliant one.
+from email import generator
+
+generator.Generator = RFC3156CompliantGenerator
+
+
+LOCAL_FQDN = "bitmask.local"
+
+
+@implementer(IRealm)
+class LocalSMTPRealm(object):
+
+ _encoding = 'utf-8'
+
+ def __init__(self, keymanager_sessions, soledad_sessions, sendmail_opts,
+ encrypted_only=False):
+ """
+ :param keymanager_sessions: a dict-like object, containing instances
+ of a Keymanager objects, indexed by
+ userid.
+ """
+ self._keymanager_sessions = keymanager_sessions
+ self._soledad_sessions = soledad_sessions
+ self._sendmail_opts = sendmail_opts
+ self.encrypted_only = encrypted_only
+
+ def requestAvatar(self, avatarId, mind, *interfaces):
+
+ if isinstance(avatarId, str):
+ avatarId = avatarId.decode(self._encoding)
+
+ def gotKeymanagerAndSoledad(result):
+ keymanager, soledad = result
+ d = bouncerFactory(soledad)
+ d.addCallback(lambda bouncer: (keymanager, soledad, bouncer))
+ return d
+
+ def getMessageDelivery(result):
+ keymanager, soledad, bouncer = result
+ # TODO use IMessageDeliveryFactory instead ?
+ # it could reuse the connections.
+ if smtp.IMessageDelivery in interfaces:
+ userid = avatarId
+ opts = self.getSendingOpts(userid)
+
+ outgoing = outgoingFactory(
+ userid, keymanager, opts, bouncer=bouncer)
+ avatar = SMTPDelivery(userid, keymanager, self.encrypted_only,
+ outgoing)
+
+ return (smtp.IMessageDelivery, avatar,
+ getattr(avatar, 'logout', lambda: None))
+
+ raise NotImplementedError(self, interfaces)
+
+ d1 = self.lookupKeymanagerInstance(avatarId)
+ d2 = self.lookupSoledadInstance(avatarId)
+ d = defer.gatherResults([d1, d2])
+ d.addCallback(gotKeymanagerAndSoledad)
+ d.addCallback(getMessageDelivery)
+ return d
+
+ def lookupKeymanagerInstance(self, userid):
+ try:
+ keymanager = self._keymanager_sessions[userid]
+ except:
+ raise errors.AuthenticationError(
+ 'No keymanager session found for user %s. Is it authenticated?'
+ % userid)
+ # XXX this should return the instance after whenReady callback
+ return defer.succeed(keymanager)
+
+ def lookupSoledadInstance(self, userid):
+ try:
+ soledad = self._soledad_sessions[userid]
+ except:
+ raise errors.AuthenticationError(
+ 'No soledad session found for user %s. Is it authenticated?'
+ % userid)
+ # XXX this should return the instance after whenReady callback
+ return defer.succeed(soledad)
+
+ def getSendingOpts(self, userid):
+ try:
+ opts = self._sendmail_opts[userid]
+ except KeyError:
+ raise errors.ConfigurationError(
+ 'No sendingMail options found for user %s' % userid)
+ return opts
+
+
+class SMTPTokenChecker(LocalSoledadTokenChecker):
+ """A credentials checker that will lookup a token for the SMTP service.
+ For now it will be using the same identifier than IMAPTokenChecker"""
+
+ service = 'mail_auth'
+
+ # TODO besides checking for token credential,
+ # we could also verify the certificate here.
+
+
+class LEAPInitMixin(object):
+
+ """
+ A Mixin that takes care of initialization of all the data needed to access
+ LEAP sessions.
+ """
+ def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
+ encrypted_only=False):
+ realm = LocalSMTPRealm(
+ keymanager_sessions, soledad_sessions, sendmail_opts,
+ encrypted_only)
+ portal = Portal(realm)
+
+ checker = SMTPTokenChecker(soledad_sessions)
+ self.checker = checker
+ self.portal = portal
+ portal.registerChecker(checker)
+
+
+class LocalSMTPServer(smtp.ESMTP, LEAPInitMixin):
+ """
+ The Production ESMTP Server: Authentication Needed.
+ Authenticates against SMTP Token stored in Local Soledad instance.
+ The Realm will produce a Delivery Object that handles encryption/signing.
+ """
+
+ # TODO: implement Queue using twisted.mail.mail.MailService
+
+ def __init__(self, soledads, keyms, sendmailopts, *args, **kw):
+ encrypted_only = kw.pop('encrypted_only', False)
+
+ LEAPInitMixin.__init__(self, soledads, keyms, sendmailopts,
+ encrypted_only)
+ smtp.ESMTP.__init__(self, *args, **kw)
+
+
+# TODO implement retries -- see smtp.SenderMixin
+class SMTPFactory(protocol.ServerFactory):
+ """
+ Factory for an SMTP server with encrypted gatewaying capabilities.
+ """
+
+ protocol = LocalSMTPServer
+ domain = LOCAL_FQDN
+ timeout = 600
+ encrypted_only = False
+
+ def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
+ deferred=None, retries=3):
+
+ self._soledad_sessions = soledad_sessions
+ self._keymanager_sessions = keymanager_sessions
+ self._sendmail_opts = sendmail_opts
+
+ def buildProtocol(self, addr):
+ p = self.protocol(
+ self._soledad_sessions, self._keymanager_sessions,
+ self._sendmail_opts, encrypted_only=self.encrypted_only)
+ p.factory = self
+ p.host = LOCAL_FQDN
+ p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
+ return p
+
+
+#
+# SMTPDelivery
+#
+
+@implementer(smtp.IMessageDelivery)
+class SMTPDelivery(object):
+ """
+ Validate email addresses and handle message delivery.
+ """
+
+ 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 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.outgoing.service.OutgoingMail
+ """
+ self._userid = userid
+ self._outgoing_mail = outgoing_mail
+ self._km = keymanager
+ 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
+ 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
+ bound.
+ :type: list of twisted.mail.smtp.User
+
+ @return: The full "Received" header string.
+ :type: str
+ """
+ myHostname, clientIP = helo
+ 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(s=headerValue, header_name='Received')
+
+ def validateTo(self, user):
+ """
+ Validate the address of a recipient of the message, possibly
+ rejecting it if the recipient key is not available.
+
+ This method is called once for each recipient, i.e. for each SMTP
+ protocol line beginning with "RCPT TO:", which includes all addresses
+ in "To", "Cc" and "Bcc" MUA fields.
+
+ The recipient's address is validated against the RFC 2822 definition.
+ If self._encrypted_only is True and no key is found for a recipient,
+ then that recipient is rejected.
+
+ The method returns an encrypted message object that is able to send
+ itself to the user's address.
+
+ :param user: The user whose address we wish to validate.
+ :type: twisted.mail.smtp.User
+
+ @return: A callable which takes no arguments and returns an
+ encryptedMessage.
+ @rtype: no-argument callable
+
+ @raise SMTPBadRcpt: Raised if messages to the address are not to be
+ accepted.
+ """
+ # try to find recipient's public key
+ address = validate_address(user.dest.addrstr)
+
+ # verify if recipient key is available in keyring
+ def found(_):
+ log.msg("Accepting mail for %s..." % user.dest.addrstr)
+ emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED,
+ self._userid, user.dest.addrstr)
+
+ def not_found(failure):
+ failure.trap(KeyNotFound)
+
+ # if key was not found, check config to see if will send anyway
+ if self._encrypted_only:
+ emit_async(catalog.SMTP_RECIPIENT_REJECTED, self._userid,
+ user.dest.addrstr)
+ raise smtp.SMTPBadRcpt(user.dest.addrstr)
+ log.msg("Warning: will send an unencrypted message (because "
+ "encrypted_only' is set to False).")
+ emit_async(
+ catalog.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED,
+ self._userid, user.dest.addrstr)
+
+ def encrypt_func(_):
+ return lambda: EncryptedMessage(user, self._outgoing_mail)
+
+ d = self._km.get_key(address)
+ d.addCallbacks(found, not_found)
+ d.addCallback(encrypt_func)
+ return d
+
+ 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
+ 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
+
+ @raise twisted.mail.smtp.SMTPBadSender: Raised if messages from this
+ address are not to be accepted.
+ """
+ # 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
+
+
+#
+# EncryptedMessage
+#
+
+class EncryptedMessage(object):
+ """
+ Receive plaintext from client, encrypt it and send message to a
+ recipient.
+ """
+ implements(smtp.IMessage)
+
+ def __init__(self, user, outgoing_mail):
+ """
+ Initialize the encrypted message.
+
+ :param user: The recipient of this message.
+ :type user: twisted.mail.smtp.User
+ :param outgoing_mail: The outgoing mail to send the message
+ :type outgoing_mail: leap.mail.outgoing.service.OutgoingMail
+ """
+ # assert params
+ leap_assert_type(user, smtp.User)
+
+ self._user = user
+ self._lines = []
+ self._outgoing_mail = outgoing_mail
+
+ def lineReceived(self, line):
+ """
+ Handle another line.
+
+ :param line: The received line.
+ :type line: str
+ """
+ self._lines.append(line)
+
+ def eomReceived(self):
+ """
+ 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
+ raw_mail = '\r\n'.join(self._lines)
+
+ return self._outgoing_mail.send_message(raw_mail, self._user)
+
+ def connectionLost(self):
+ """
+ Log an error when the connection is lost.
+ """
+ log.msg("Connection lost unexpectedly!")
+ log.err()
+ emit_async(catalog.SMTP_CONNECTION_LOST, self._userid,
+ self._user.dest.addrstr)
+ # unexpected loss of connection; don't save
+
+ self._lines = []
diff --git a/src/leap/bitmask/mail/smtp/tests/185CA770.key b/src/leap/bitmask/mail/smtp/tests/185CA770.key
new file mode 100644
index 0000000..587b416
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/tests/185CA770.key
@@ -0,0 +1,79 @@
+-----BEGIN PGP PRIVATE KEY BLOCK-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+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
+=6HcJ
+-----END PGP PRIVATE KEY BLOCK-----
diff --git a/src/leap/bitmask/mail/smtp/tests/185CA770.pub b/src/leap/bitmask/mail/smtp/tests/185CA770.pub
new file mode 100644
index 0000000..38af19f
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/tests/185CA770.pub
@@ -0,0 +1,52 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1.4.10 (GNU/Linux)
+
+mQINBFCJNL4BEADFsI1TCD4yq7ZqL7VhdVviTuX6JUps8/mVEhRVOZhojLcTYaqQ
+gs6T6WabRxcK7ymOnf4K8NhYdz6HFoJN46BT87etokx7J/Sl2OhpiqBQEY+jW8Rp
++3MSGrGmvFw0s1lGrz/cXzM7UNgWSTOnYZ5nJS1veMhy0jseZOUK7ekp2oEDjGZh
+pzgd3zICCR2SvlpLIXB2Nr/CUcuRWTcc5LlKmbjMybu0E/uuY14st3JL+7qI6QX0
+atFm0VhFVpagOl0vWKxakUx4hC7j1wH2ADlCvSZPG0StSLUyHkJx3UPsmYxOZFao
+ATED3Okjwga6E7PJEbzyqAkvzw/M973kaZCUSH75ZV0cQnpdgXV3DK1gSa3d3gug
+W1lE0V7pwnN2NTOYfBMi+WloCs/bp4iZSr4QP1duZ3IqKraeBDCk7MoFo4A9Wk07
+kvqPwF9IBgatu62WVEZIzwyViN+asFUGfgp+8D7gtnlWAw0V6y/lSTzyl+dnLP98
+Hfr2eLBylFs+Kl3Pivpg2uHw09LLCrjeLEN3dj9SfBbA9jDIo9Zhs1voiIK/7Shx
+E0BRJaBgG3C4QaytYEu7RFFOKuvBai9w2Y5OfsKFo8rA7v4dxFFDvzKGujCtNnwf
+oyaGlZmMBU5MUmHUNiG8ON21COZBtK5oMScuY1VC9CQonj3OClg3IbU9SQARAQAB
+tCRkcmVicyAoZ3BnIHRlc3Qga2V5KSA8ZHJlYnNAbGVhcC5zZT6JAjgEEwECACIF
+AlCJNL4CGwMGCwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJELcvXsYYXKdwXYcP
+/23T1DW6eq4CI0UbsGLxieiLohbhViUNJt4gLrgKGaORhwD6mQLhpHTFWLtHbQrW
+evDCf0KYwQk/UUS/poCIHy31Eo9mZJz+J2+m2ohubtvJdCiHf3uSAokCgZyLI8Ss
+YSwVR0s0WktT/r2XhdgtkfV8jctHgpwMMgjHA9EfE1UThkO4uGULDGzI9Sz6TsIV
+fxBqdt5w/rqVjlW5/8EBzmFdpDIgP/H4s5lkSFrFeP+vLjAPlgz+C+NAJydH3LNO
+H/XXjPT+qY5ZtNFQ+6pJWBBsOjKa9oPzO95BodZ5QL/iJkARpSzJXkV8sx2N1gIc
+5kSljuJKBbinkfUNBDM5NkeP5LmoHVhKTPm4cBOMT1dbT2YZ/ahU86UA+Kf1U+Kj
+UCjg9PmkxVq3uuUbdnBRTCyO1JhBzS4tDQfSQiAIuPpfm/VkDZZmjgvBIThsEc5n
+oVMh5lzNwTVtvDJ8ihSe2wRTHe7ic/rwDzq6+js7jb1Qr9iNiApE7IoSI/88ORl8
+ZDzu2GzLvayhtJyhdTdyQdYFCe/Ax5oW43mhkrx8PObDw9P9XT2SuPBooPIutmgs
+X/DpST5bfGYQQaJc7OiE1DWu+UcPXybI91Frr9lRWiLkee2xVpmN7APSj4HX+41U
+JQGSpVcpYuyJMmmZsQ2TRUKE3SAvbqNGoICAz/Myr9J9uQINBFCJNL4BEADjvAQE
+e7AXao9kUTETXlkgOEvRrh31d2EAEZ6mLGyE7NY0YAKO48fWeQvNHGTsTlFSuwiY
+7U6lNCQfUD/88FfYZOYusumkE8rHJUMkzTddGl7v98yZWuQ7oc/grPuVHFQJjSV/
+RYEzFb4SgxCGPyzZm/TKASzLrdQpA8fljwleOVjiAQg3Y9h6SI4nuKYbNP+uUloz
+4lYN1rBWZY1caSQhlz+PBHrdm3/Gn2B9Zcd7EDHDW8iQAiD7c/mCs3O4yCsCkls4
+NVcmIpG5PXlgsXM+lSjSSoveGB/XO1PF903WfBtJBmFJXY2A73rMpZ7kJXBgrUhi
+jgIazfwbtkdIKRYGJduO7u/Qpo6LXnzHb/ARlA1urvf0nqfcTd3VvEvO4ejP7b7N
+AoZZAw6SXncwne04XYpBrxUSX1v/LE//qqEyrJMkkGgyHjSZ8WbsisBCyzAbwmRq
+WpsJp5BBob7mVqtIfw8Mk7OEABPDlR1yUjN1De4TOJSSFIfrESidRQF103UmOHFD
+WkvjirY9iyfOcxaoXwGeKRcCwoGGybVOYXo9i5zCTW2HEpI8W5PnOSI5KGURfhMY
+2ZXNFS2DU+e2a4sGf6V+NqH4zsjEf3ZB0LhYpP+Cu79WXpn2k3ZtO0hDAZJ0ejmb
+Mv+fr4hLV4ood6bUzGk/a6MPoPnBaMXV5IrCrwARAQABiQIfBBgBAgAJBQJQiTS+
+AhsMAAoJELcvXsYYXKdw3rQP/A/yLHEgZVmN04wg9j+7d/l9+FEivBBQ90CbHvXZ
+wlji3LJBUCKCH4UTO+dnrxbtlvaMBL8UkHRvqLUADqi+9pNRB9oOy7AA7lfCOCzm
+Dpy8niBPSG/ghTwZJs0xM63WKeiE1FO6qoJIXIJ5bShh6HpLSI8RPILcQihHcsZM
+2s7pupwkEJEgWTl1VN3s5vgNkBxz+l09RhG3SmydLSLiUfAA7tPKQcdnPywwQa3l
+Ura67dQ287kflPDCbHy1XGF/0cN1figNuKC0c4Pdct3H4t7DVP5JneQsONk49NAf
+hV0J8udTKrOCsqh6G4+tiCOvHi+R0QdqXBrKGnnUMsbtnGEnlcmqwTolDzWJKBgy
+aCnQ8mUSATTStFvgEk39M3h4a2P6JLdcm01bb27esJExGylXj1GmJjZoa3tXAI+L
+jcBc2I5MyXJvvF1n7Q/k1fHBOQ8lXZxdNEjw7j86HUQD12hqUFDC5BOET69UwMHL
+qDx/WhR2f7Huq8Ok0By3dLzJNFPw6HIpF6UidTenGhN5UcCP/pxn1zYgq/DCHaH4
+JAXjvOvhu5NFF0uL+RSpNZ8E8reD5BSQ7wCV+q2T8SGfTZrQOWqOb8rtQ0+SIaF4
+A88LqwHvPkfsuoLQN9uIVyMHhTbe0s2D3yOEgI7p5ZpvDyAX96MAa5N4/sOhEKGg
+q3iu
+=RChS
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/src/leap/bitmask/mail/smtp/tests/cert/server.crt b/src/leap/bitmask/mail/smtp/tests/cert/server.crt
new file mode 100644
index 0000000..a27391c
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/tests/cert/server.crt
@@ -0,0 +1,29 @@
+-----BEGIN CERTIFICATE-----
+MIIFBjCCAu4CCQCWn3oMoQrDJTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV
+UzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0
+cyBQdHkgTHRkMB4XDTEzMTAyMzE0NDUwNFoXDTE2MDcxOTE0NDUwNFowRTELMAkG
+A1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0
+IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB
+APexTvEvG7cSmZdAERHt9TB11cSor54Y/F7NmYMdSOJNi4Y0kwkSslpdfipi+mt/
+NFg/uGKi1mcgvuXdVbVPZ9rCgVpIzMncO8RAP7a5+I2zKUzqMCCbLH16sYpo/rDk
+VQ5V15TwLsTzOFGG8Cgp68TR8zHuZ4Edf2zMGC1IaiJ6W38LTnJgsowYOCFDAF3z
+L36kxMO5gNGEUYV6tjltx+rAcXka3po+xiAgvW6q65UUgDHcIdEGG2dc9bkxxPl7
+RkprF2RwwADNzYS7Tn+Hpmjy06pfYZHNME+Iw515bCRF3GQFUU4BpGnY7EO+h4P9
+Kb1h948gUT9/oswXG+q2Kwk8AoggMJkUOWDFiCa5UjW1GBoxxb7VtZ+QTJXxlFWc
+M2VzT7M/HX+P4b05vY4MXJjxPAFKrAGS7J8DKW8WJNUnXa9XSDBHg5qijDzZ/zGm
+HTdG6iADnJLmOHBQgFQ12a/n9mYV2GPVC6FlgDzG9f0/SUPBUCafyWYz1LwKY4VM
+2NLx/iwYMQsNIMSZQfNmufNDBr70+BShe3ZpbmKB/J33d87AuJd2HjnsThTEAAr+
+6CejyYmwFutoDUCF8IaKGJEp7OGP2//ub4nt5WwW8DYLRi8EqtzEnxPo5ZiayHMY
+GHR1jpX1O5JVJFUE79bZCFFHKmtJc4kVZS4m4rTLsk83AgMBAAEwDQYJKoZIhvcN
+AQEFBQADggIBAEt4PIRqVuALQSdgZ+GiZYuvEVjxoDVtMSc/ym93Gi8R7DDivFH9
+4suQc5QUiuEF8lpEtkmh+PZ+oFdQkjhBH80h7p4BUSyBy5Yi6dy7ATTlBAqwzCYZ
+4wzHeJzu1SI6FinZLksoULbcw04n410aGHkLa6I9O3vCC4kXSnBlwU1sUsJphxM2
+3pkHBpvv79XYf5kFqZPzF16aO7rxFuVvqgXLyzwuyP9kH5zMA21Kioxs/pNyg1lm
+5h0VinpHLPse+4tYih1L1WLMpEZiSwZgFhoRtlcdIVXokZPaX4G2EkdrMmSQruWg
+Uz8Av6LEYHmRfbYwYM2kEX/+AF8thpTQDbvxjqYk5oyGX4wpKGpih1ac/jYu3O8B
+VLhbxZlBYcLxCqqNsGJrWaiHj2Jf4GhUB0O9hXfaZDMqEGXT9GzOz0yF6b3pDQVy
+H0lKIBb+kQzB/jhZKu4vrTAowXtt/av5d7D+rpAU1SxfUhBOPNSRoJUI5NSBbokp
+a7u4azdB2IQETX3d2rhDk09EbG1XmMi5Vg1oa8nxfMOWXZnDMusJoZClKjrthmwd
+rtR5et44XYhX6p217RBkYMDOVFT7aZpu4SaFeqZIuarVYodSmgXToOFXPsrLppRQ
+adOT0FpU64RPNrQz5NF1bSIjqrHSaRVacf8yr7qqxNnpMsrtkDJzsMBz
+-----END CERTIFICATE-----
diff --git a/src/leap/bitmask/mail/smtp/tests/cert/server.key b/src/leap/bitmask/mail/smtp/tests/cert/server.key
new file mode 100644
index 0000000..197a449
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/tests/cert/server.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
diff --git a/src/leap/bitmask/mail/smtp/tests/mail.txt b/src/leap/bitmask/mail/smtp/tests/mail.txt
new file mode 100644
index 0000000..9542047
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/tests/mail.txt
@@ -0,0 +1,10 @@
+HELO drebs@riseup.net
+MAIL FROM: drebs@riseup.net
+RCPT TO: drebs@riseup.net
+RCPT TO: drebs@leap.se
+DATA
+Subject: leap test
+
+Hello world!
+.
+QUIT
diff --git a/src/leap/bitmask/mail/smtp/tests/test_gateway.py b/src/leap/bitmask/mail/smtp/tests/test_gateway.py
new file mode 100644
index 0000000..9d88afb
--- /dev/null
+++ b/src/leap/bitmask/mail/smtp/tests/test_gateway.py
@@ -0,0 +1,181 @@
+# -*- 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 <http://www.gnu.org/licenses/>.
+
+"""
+SMTP gateway tests.
+"""
+import re
+import tempfile
+from datetime import datetime
+
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, fail, succeed, Deferred
+from twisted.test import proto_helpers
+
+from mock import Mock
+
+from leap.keymanager import openpgp, errors
+from leap.mail.testing import KeyManagerWithSoledadTestCase
+from leap.mail.testing import ADDRESS, ADDRESS_2
+from leap.mail.testing.smtp import getSMTPFactory, TEST_USER
+
+
+# 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])"
+HOSTNAME_REGEX = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + \
+ "([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 TestSmtpGateway(KeyManagerWithSoledadTestCase):
+
+ 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):
+ # pytest handles correctly the setupEnv for the class,
+ # but trial ignores it.
+ if not getattr(self, 'tempdir', None):
+ self.tempdir = tempfile.mkdtemp()
+ return KeyManagerWithSoledadTestCase.setUp(self)
+
+ def tearDown(self):
+ return KeyManagerWithSoledadTestCase.tearDown(self)
+
+ def assertMatch(self, string, pattern, msg=None):
+ if not re.match(pattern, string):
+ msg = self._formatMessage(msg, '"%s" does not match pattern "%s".'
+ % (string, pattern))
+ raise self.failureException(msg)
+
+ @inlineCallbacks
+ def test_gateway_accepts_valid_email(self):
+ """
+ Test if SMTP server responds correctly for valid interaction.
+ """
+
+ SMTP_ANSWERS = ['220 ' + IP_OR_HOST_REGEX +
+ ' NO UCE NO UBE NO RELAY PROBES',
+ '250 ' + IP_OR_HOST_REGEX + ' Hello ' +
+ IP_OR_HOST_REGEX + ', nice to meet you',
+ '250 Sender address accepted',
+ '250 Recipient address accepted',
+ '354 Continue']
+
+ user = TEST_USER
+ proto = getSMTPFactory({user: None}, {user: self.km}, {user: None})
+ transport = proto_helpers.StringTransport()
+ proto.makeConnection(transport)
+ reply = ""
+ for i, line in enumerate(self.EMAIL_DATA):
+ reply += yield self.getReply(line + '\r\n', proto, transport)
+ self.assertMatch(reply, '\r\n'.join(SMTP_ANSWERS),
+ 'Did not get expected answer from gateway.')
+ proto.setTimeout(None)
+
+ @inlineCallbacks
+ def test_missing_key_rejects_address(self):
+ """
+ Test if server rejects to send unencrypted when 'encrypted_only' is
+ True.
+ """
+ # remove key from key manager
+ pubkey = yield self.km.get_key(ADDRESS)
+ pgp = openpgp.OpenPGPScheme(
+ self._soledad, gpgbinary=self.gpg_binary_path)
+ yield pgp.delete_key(pubkey)
+ # mock the key fetching
+ self.km._fetch_keys_from_server = Mock(
+ return_value=fail(errors.KeyNotFound()))
+ user = TEST_USER
+ proto = getSMTPFactory(
+ {user: None}, {user: self.km}, {user: None},
+ encrypted_only=True)
+ transport = proto_helpers.StringTransport()
+ proto.makeConnection(transport)
+ yield self.getReply(self.EMAIL_DATA[0] + '\r\n', proto, transport)
+ yield self.getReply(self.EMAIL_DATA[1] + '\r\n', proto, transport)
+ reply = yield self.getReply(self.EMAIL_DATA[2] + '\r\n',
+ proto, transport)
+ # ensure the address was rejected
+ self.assertEqual(
+ '550 Cannot receive for specified address\r\n',
+ reply,
+ 'Address should have been rejected with appropriate message.')
+ proto.setTimeout(None)
+
+ @inlineCallbacks
+ def test_missing_key_accepts_address(self):
+ """
+ Test if server accepts to send unencrypted when 'encrypted_only' is
+ False.
+ """
+ # remove key from key manager
+ pubkey = yield self.km.get_key(ADDRESS)
+ pgp = openpgp.OpenPGPScheme(
+ self._soledad, gpgbinary=self.gpg_binary_path)
+ yield pgp.delete_key(pubkey)
+ # mock the key fetching
+ self.km._fetch_keys_from_server = Mock(
+ return_value=fail(errors.KeyNotFound()))
+ user = TEST_USER
+ proto = getSMTPFactory({user: None}, {user: self.km}, {user: None})
+ transport = proto_helpers.StringTransport()
+ proto.makeConnection(transport)
+ yield self.getReply(self.EMAIL_DATA[0] + '\r\n', proto, transport)
+ yield self.getReply(self.EMAIL_DATA[1] + '\r\n', proto, transport)
+ reply = yield self.getReply(self.EMAIL_DATA[2] + '\r\n',
+ proto, transport)
+ # ensure the address was accepted
+ self.assertEqual(
+ '250 Recipient address accepted\r\n',
+ reply,
+ 'Address should have been accepted with appropriate message.')
+ proto.setTimeout(None)
+
+ def getReply(self, line, proto, transport):
+ proto.lineReceived(line)
+
+ if line[:4] not in ['HELO', 'MAIL', 'RCPT', 'DATA']:
+ return succeed("")
+
+ def check_transport(_):
+ reply = transport.value()
+ if reply:
+ transport.clear()
+ return succeed(reply)
+
+ d = Deferred()
+ d.addCallback(check_transport)
+ reactor.callLater(0, lambda: d.callback(None))
+ return d
+
+ return check_transport(None)