summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/leap/bitmask/core/mail_services.py60
-rw-r--r--src/leap/bitmask/mail/outgoing/sender.py130
-rw-r--r--src/leap/bitmask/mail/outgoing/service.py135
-rw-r--r--src/leap/bitmask/mail/smtp/gateway.py155
-rw-r--r--src/leap/bitmask/mail/smtp/service.py12
-rw-r--r--src/leap/bitmask/mail/testing/smtp.py22
-rw-r--r--tests/integration/mail/outgoing/test_outgoing.py21
-rw-r--r--tests/integration/mail/smtp/test_gateway.py9
-rw-r--r--tests/unit/mail/outgoing/test_service.py2
9 files changed, 271 insertions, 275 deletions
diff --git a/src/leap/bitmask/core/mail_services.py b/src/leap/bitmask/core/mail_services.py
index 6dfc2410..bafd8803 100644
--- a/src/leap/bitmask/core/mail_services.py
+++ b/src/leap/bitmask/core/mail_services.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# mail_services.py
-# Copyright (C) 2016 LEAP Encryption Acess Project
+# Copyright (C) 2016-2017 LEAP Encryption Acess Project
#
# 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
@@ -43,6 +43,7 @@ try:
from leap.bitmask.keymanager import KeyManager
from leap.bitmask.keymanager.errors import KeyNotFound
from leap.bitmask.keymanager.validation import ValidationLevels
+ from leap.bitmask.mail import errors
from leap.bitmask.mail.constants import INBOX_NAME
from leap.bitmask.mail.mail import Account
from leap.bitmask.mail.imap import service as imap_service
@@ -50,6 +51,9 @@ try:
from leap.bitmask.mail.incoming.service import IncomingMail
from leap.bitmask.mail.incoming.service import INCOMING_CHECK_PERIOD
from leap.bitmask.mail.utils import first
+ from leap.bitmask.mail.outgoing.service import OutgoingMail
+ from leap.bitmask.mail.outgoing.sender import SMTPSender
+ from leap.bitmask.mail.smtp.bounces import bouncerFactory
from leap.soledad.client.api import Soledad
HAS_MAIL = True
except ImportError:
@@ -482,7 +486,7 @@ class StandardMailService(service.MultiService, HookableService):
self._basedir = basedir
self._soledad_sessions = {}
self._keymanager_sessions = {}
- self._sendmail_opts = {}
+ self._outgoing_sessions = {}
self._service_tokens = {}
self._mixnet_enabled = mixnet_enabled
super(StandardMailService, self).__init__()
@@ -490,9 +494,8 @@ class StandardMailService(service.MultiService, HookableService):
def initializeChildrenServices(self):
self.addService(IMAPService(self._soledad_sessions))
- self.addService(SMTPService(
- self._soledad_sessions, self._keymanager_sessions,
- self._sendmail_opts))
+ self.addService(SMTPService(self._outgoing_sessions,
+ self._soledad_sessions))
# TODO adapt the service to receive soledad/keymanager sessions object.
# See also the TODO before IncomingMailService.startInstance
self.addService(IncomingMailService(self))
@@ -506,20 +509,19 @@ class StandardMailService(service.MultiService, HookableService):
super(StandardMailService, self).stopService()
def startInstance(self, userid, soledad, keymanager):
- username, provider = userid.split('@')
self._soledad_sessions[userid] = soledad
self._keymanager_sessions[userid] = keymanager
- sendmail_opts = _get_sendmail_opts(self._basedir, provider, username)
- self._sendmail_opts[userid] = sendmail_opts
-
def registerToken(token):
self._service_tokens[userid] = token
return token
incoming = self.getServiceNamed('incoming_mail')
- d = incoming.startInstance(userid)
+ d = bouncerFactory(soledad)
+ d.addCallback(self._create_outgoing_service, userid, keymanager,
+ soledad)
+ d.addCallback(lambda _: incoming.startInstance(userid))
d.addCallback(
lambda _: soledad.get_or_create_service_token('mail_auth'))
d.addCallback(registerToken)
@@ -528,6 +530,22 @@ class StandardMailService(service.MultiService, HookableService):
self._maybe_start_pixelated, userid, soledad, keymanager)
return d
+ def _create_outgoing_service(self, bouncer, userid, keymanager, soledad):
+ outgoing = OutgoingMail(userid, keymanager, bouncer)
+
+ username, provider = userid.split('@')
+ key = _get_smtp_client_cert_path(self._basedir, provider, username)
+ prov = _get_provider_for_service('smtp', self._basedir, provider)
+ hostname = prov.hostname
+ port = prov.port
+ if not os.path.isfile(key):
+ raise errors.ConfigurationError(
+ 'No valid SMTP certificate could be found for %s!' % userid)
+ smtp_sender = SMTPSender(userid, key, hostname, port)
+ outgoing.add_sender(smtp_sender)
+
+ self._outgoing_sessions[userid] = outgoing
+
# hooks
def hook_on_new_keymanager_instance(self, **kw):
@@ -745,13 +763,12 @@ class SMTPService(service.Service):
name = 'smtp'
log = Logger()
- def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
+ def __init__(self, outgoing_sessions, soledad_sessions,
basedir=DEFAULT_BASEDIR):
self._basedir = os.path.expanduser(basedir)
+ self._outgoing_sessions = outgoing_sessions
self._soledad_sessions = soledad_sessions
- self._keymanager_sessions = keymanager_sessions
- self._sendmail_opts = sendmail_opts
self._port = None
self._factory = None
super(SMTPService, self).__init__()
@@ -762,9 +779,8 @@ class SMTPService(service.Service):
return
self.log.info('starting smtp service')
port, factory = smtp_service.run_service(
+ self._outgoing_sessions,
self._soledad_sessions,
- self._keymanager_sessions,
- self._sendmail_opts,
factory=self._factory)
self._port = port
self._factory = factory
@@ -846,6 +862,7 @@ class IncomingMailService(service.MultiService):
self.log.debug('Setting Incoming Mail Service for %s' % userid)
incoming_mail.setName(userid)
self.addService(incoming_mail)
+ return incoming_mail
def setStatusOn(res):
self._set_status(userid, "on")
@@ -882,9 +899,6 @@ SERVICES = ('soledad', 'smtp', 'vpn')
Provider = namedtuple(
'Provider', ['hostname', 'ip_address', 'location', 'port'])
-SendmailOpts = namedtuple(
- 'SendmailOpts', ['cert', 'key', 'hostname', 'port'])
-
def _get_ca_cert_path(basedir, provider):
path = os.path.join(
@@ -892,16 +906,6 @@ def _get_ca_cert_path(basedir, provider):
return path
-def _get_sendmail_opts(basedir, provider, username):
- cert = _get_smtp_client_cert_path(basedir, provider, username)
- key = cert
- prov = _get_provider_for_service('smtp', basedir, provider)
- hostname = prov.hostname
- port = prov.port
- opts = SendmailOpts(cert, key, hostname, port)
- return opts
-
-
def _get_smtp_client_cert_path(basedir, provider, username):
path = os.path.join(
basedir, 'providers', provider, 'keys', 'client', 'smtp_%s.pem' %
diff --git a/src/leap/bitmask/mail/outgoing/sender.py b/src/leap/bitmask/mail/outgoing/sender.py
new file mode 100644
index 00000000..d8c049c8
--- /dev/null
+++ b/src/leap/bitmask/mail/outgoing/sender.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+# outgoing/sender.py
+# Copyright (C) 2017 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/>.
+
+from OpenSSL import SSL
+from StringIO import StringIO
+from twisted.internet import reactor
+from twisted.internet import defer
+from twisted.logger import Logger
+from twisted.mail import smtp
+from twisted.protocols.amp import ssl
+from zope.interface import Interface, implements
+
+from leap.common.check import leap_assert_type, leap_assert
+from leap.bitmask import __version__
+
+
+class ISender(Interface):
+ def can_send(self, recipient):
+ """
+ Checks if ISender implementor can send messages to recipient
+
+ :type recipient: string
+ :rtype: bool
+ """
+
+ def send(self, recipient, message):
+ """
+ Send a messages to recipient
+
+ :type recipient: string
+ :type message: string
+
+ :return: A Deferred that will be called with the recipient address
+ :raises SendError: in case of failure in send
+ """
+
+
+class SendError(Exception):
+ pass
+
+
+class SMTPSender(object):
+ implements(ISender)
+
+ log = Logger()
+
+ def __init__(self, from_address, key, host, port):
+ """
+ :param from_address: The sender address.
+ :type from_address: 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
+ """
+ leap_assert_type(host, (str, unicode))
+ leap_assert(host != '')
+ leap_assert_type(port, int)
+ leap_assert(port is not 0)
+ leap_assert_type(key, basestring)
+ leap_assert(key != '')
+
+ self._port = port
+ self._host = host
+ self._key = key
+ self._from_address = from_address
+
+ def can_send(self, recipient):
+ return '@' in recipient
+
+ def send(self, recipient, message):
+ self.log.info(
+ 'Connecting to SMTP server %s:%s' % (self._host, self._port))
+
+ # we construct a defer to pass to the ESMTPSenderFactory
+ d = defer.Deferred()
+ # 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, no client auth here
+ "", # password is blank, no client auth here
+ self._from_address,
+ recipient.dest.addrstr,
+ StringIO(message),
+ d,
+ heloFallback=True,
+ requireAuthentication=False,
+ requireTransportSecurity=True)
+ factory.domain = bytes('leap.bitmask.mail-' + __version__)
+ reactor.connectSSL(
+ self._host, self._port, factory,
+ contextFactory=SSLContextFactory(self._key, self._key))
+ d.addCallback(lambda result: result[1][0][0])
+ d.addErrback(self._send_errback)
+ return d
+
+ def _send_errback(self, failure):
+ raise SendError(failure.getErrorMessage())
+
+
+class SSLContextFactory(ssl.ClientContextFactory):
+ def __init__(self, cert, key):
+ self.cert = cert
+ self.key = key
+
+ def getContext(self):
+ # FIXME -- we should use sslv23 to allow for tlsv1.2
+ # and, if possible, explicitely disable sslv3 clientside.
+ # Servers should avoid sslv3
+ 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
diff --git a/src/leap/bitmask/mail/outgoing/service.py b/src/leap/bitmask/mail/outgoing/service.py
index 4f39691b..a48e363c 100644
--- a/src/leap/bitmask/mail/outgoing/service.py
+++ b/src/leap/bitmask/mail/outgoing/service.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# outgoing/service.py
-# Copyright (C) 2013-2015 LEAP
+# Copyright (C) 2013-2017 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
@@ -21,7 +21,6 @@ OutgoingMail module.
The OutgoingMail class allows to send mail, and encrypts/signs it if needed.
"""
-import os.path
import re
from StringIO import StringIO
from copy import deepcopy
@@ -31,19 +30,14 @@ from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
-from OpenSSL import SSL
-
from twisted.mail import smtp
-from twisted.internet import reactor
from twisted.internet import defer
+from twisted.python.failure import Failure
from twisted.logger import Logger
-from twisted.protocols.amp import ssl
from leap.common.check import leap_assert_type, leap_assert
from leap.common.events import emit_async, catalog
-from leap.bitmask import __version__
from leap.bitmask.keymanager.errors import KeyNotFound, KeyAddressMismatch
-from leap.bitmask.mail import errors
from leap.bitmask.mail.utils import validate_address
from leap.bitmask.mail.rfc3156 import MultipartEncrypted
from leap.bitmask.mail.rfc3156 import MultipartSigned
@@ -57,39 +51,6 @@ from leap.bitmask.mail.rfc3156 import PGPEncrypted
# of IService
-class SSLContextFactory(ssl.ClientContextFactory):
- def __init__(self, cert, key):
- self.cert = cert
- self.key = key
-
- def getContext(self):
- # FIXME -- we should use sslv23 to allow for tlsv1.2
- # and, if possible, explicitely disable sslv3 clientside.
- # Servers should avoid sslv3
- 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
-
-
-def outgoingFactory(userid, keymanager, opts, check_cert=True, bouncer=None):
-
- cert = unicode(opts.cert)
- key = unicode(opts.key)
- hostname = str(opts.hostname)
- port = opts.port
-
- if check_cert:
- if not os.path.isfile(cert):
- raise errors.ConfigurationError(
- 'No valid SMTP certificate could be found for %s!' % userid)
-
- return OutgoingMail(
- str(userid), keymanager, cert, key, hostname, port,
- bouncer)
-
-
class OutgoingMail(object):
"""
Sends Outgoing Mail, encrypting and signing if needed.
@@ -97,8 +58,7 @@ class OutgoingMail(object):
log = Logger()
- def __init__(self, from_address, keymanager, cert, key, host, port,
- bouncer=None):
+ def __init__(self, from_address, keymanager, bouncer=None):
"""
Initialize the outgoing mail service.
@@ -106,39 +66,25 @@ class OutgoingMail(object):
: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
"""
# assert params
- leap_assert_type(from_address, str)
+ leap_assert_type(from_address, (str, unicode))
leap_assert('@' in from_address)
# XXX it can be a zope.proxy too
# 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, basestring)
- leap_assert(cert != '')
- leap_assert_type(key, basestring)
- leap_assert(key != '')
-
- self._port = port
- self._host = host
- self._key = key
- self._cert = cert
self._from_address = from_address
self._keymanager = keymanager
self._bouncer = bouncer
+ self._senders = []
+
+ def add_sender(self, sender):
+ """
+ Add an ISender to the outgoing service
+ """
+ self._senders.append(sender)
def send_message(self, raw, recipient):
"""
@@ -151,19 +97,26 @@ class OutgoingMail(object):
:return: a deferred which delivers the message when fired
"""
d = self._maybe_encrypt_and_sign(raw, recipient)
- d.addCallback(self._route_msg, raw)
+ d.addCallback(self._route_msg, recipient, raw)
d.addErrback(self.sendError, raw)
return d
- def sendSuccess(self, smtp_sender_result):
+ def can_encrypt_for(self, recipient):
+ def cb(_):
+ return True
+
+ def eb(failure):
+ failure.trap(KeyNotFound)
+ return False
+
+ d = self._keymanager.get_key(recipient)
+ d.addCallbacks(cb, eb)
+ return d
+
+ def sendSuccess(self, dest_addrstr):
"""
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]
fromaddr = self._from_address
self.log.info('Message sent from %s to %s' % (fromaddr, dest_addrstr))
emit_async(catalog.SMTP_SEND_MESSAGE_SUCCESS,
@@ -197,7 +150,7 @@ class OutgoingMail(object):
else:
failure.raiseException()
- def _route_msg(self, encrypt_and_sign_result, raw):
+ def _route_msg(self, encrypt_and_sign_result, recipient, raw):
"""
Sends the msg using the ESMTPSenderFactory.
@@ -206,32 +159,24 @@ class OutgoingMail(object):
:type encrypt_and_sign_result: tuple
"""
message, recipient = encrypt_and_sign_result
- self.log.info(
- '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.addCallback(self.sendSuccess)
- d.addErrback(self.sendError, raw)
- # 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, no client auth here
- "", # password is blank, no client auth here
- self._from_address,
- recipient.dest.addrstr,
- StringIO(msg),
- d,
- heloFallback=True,
- requireAuthentication=False,
- requireTransportSecurity=True)
- factory.domain = bytes('leap.bitmask.mail-' + __version__)
+ d = None
+ for sender in self._senders:
+ if sender.can_send(recipient.dest.addrstr):
+ self.log.debug('Sending message to %s with: %s'
+ % (recipient, str(sender)))
+ d = sender.send(recipient, msg)
+ break
+
+ if d is None:
+ return self.sendError(Failure(), raw)
+
emit_async(catalog.SMTP_SEND_MESSAGE_START,
self._from_address, recipient.dest.addrstr)
- reactor.connectSSL(
- self._host, self._port, factory,
- contextFactory=SSLContextFactory(self._cert, self._key))
+ d.addCallback(self.sendSuccess)
+ d.addErrback(self.sendError, raw)
+ return d
def _maybe_encrypt_and_sign(self, raw, recipient, fetch_remote=True):
"""
diff --git a/src/leap/bitmask/mail/smtp/gateway.py b/src/leap/bitmask/mail/smtp/gateway.py
index 08d59e11..4dfccd1a 100644
--- a/src/leap/bitmask/mail/smtp/gateway.py
+++ b/src/leap/bitmask/mail/smtp/gateway.py
@@ -38,7 +38,7 @@ 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.internet import protocol
from twisted.logger import Logger
from leap.common.check import leap_assert_type
@@ -47,9 +47,6 @@ from leap.bitmask.mail import errors
from leap.bitmask.mail.cred import LocalSoledadTokenChecker
from leap.bitmask.mail.utils import validate_address
from leap.bitmask.mail.rfc3156 import RFC3156CompliantGenerator
-from leap.bitmask.mail.outgoing.service import outgoingFactory
-from leap.bitmask.mail.smtp.bounces import bouncerFactory
-from leap.bitmask.keymanager.errors import KeyNotFound
# replace email generator with a RFC 3156 compliant one.
generator.Generator = RFC3156CompliantGenerator
@@ -62,81 +59,30 @@ 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
+ def __init__(self, outgoing_sessions, encrypted_only=False):
+ self._outgoing_sessions = outgoing_sessions
self.encrypted_only = encrypted_only
def requestAvatar(self, avatarId, mind, *interfaces):
+ if smtp.IMessageDelivery not in interfaces:
+ raise NotImplementedError(self, 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
+ outgoing = self.lookupOutgoingInstance(avatarId)
+ avatar = SMTPDelivery(avatarId, self.encrypted_only, outgoing)
+ return (smtp.IMessageDelivery, avatar,
+ getattr(avatar, 'logout', lambda: None))
- def lookupKeymanagerInstance(self, userid):
+ def lookupOutgoingInstance(self, userid):
try:
- keymanager = self._keymanager_sessions[userid]
+ outgoing = self._outgoing_sessions[userid]
except:
raise errors.AuthenticationError(
- 'No keymanager session found for user %s. Is it authenticated?'
+ 'No outgoing 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
+ return outgoing
class SMTPTokenChecker(LocalSoledadTokenChecker):
@@ -155,11 +101,9 @@ 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,
+ def __init__(self, outgoing_sessions, soledad_sessions,
encrypted_only=False):
- realm = LocalSMTPRealm(
- keymanager_sessions, soledad_sessions, sendmail_opts,
- encrypted_only)
+ realm = LocalSMTPRealm(outgoing_sessions, encrypted_only)
portal = Portal(realm)
checker = SMTPTokenChecker(soledad_sessions)
@@ -177,12 +121,9 @@ class LocalSMTPServer(smtp.ESMTP, LEAPInitMixin):
# 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)
+ def __init__(self, outgoings, soledads, encrypted_only=False):
+ LEAPInitMixin.__init__(self, outgoings, soledads, encrypted_only)
+ smtp.ESMTP.__init__(self)
# TODO implement retries -- see smtp.SenderMixin
@@ -196,17 +137,14 @@ class SMTPFactory(protocol.ServerFactory):
timeout = 600
encrypted_only = False
- def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts,
- deferred=None, retries=3):
-
+ def __init__(self, outgoing_sessions, soledad_sessions, deferred=None,
+ retries=3):
+ self._outgoing_sessions = outgoing_sessions
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 = self.protocol(self._outgoing_sessions, self._soledad_sessions,
+ encrypted_only=self.encrypted_only)
p.factory = self
p.host = LOCAL_FQDN
p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials}
@@ -226,14 +164,12 @@ class SMTPDelivery(object):
log = Logger()
- def __init__(self, userid, keymanager, encrypted_only, outgoing_mail):
+ def __init__(self, userid, 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
@@ -241,8 +177,7 @@ class SMTPDelivery(object):
:type outgoing_mail: leap.bitmask.mail.outgoing.service.OutgoingMail
"""
self._userid = userid
- self._outgoing_mail = outgoing_mail
- self._km = keymanager
+ self._outgoing = outgoing_mail
self._encrypted_only = encrypted_only
self._origin = None
@@ -297,32 +232,28 @@ class SMTPDelivery(object):
# try to find recipient's public key
address = validate_address(user.dest.addrstr)
- # verify if recipient key is available in keyring
- def found(_):
- self.log.debug('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:
+ def verify_if_can_encrypt_for_recipient(can_encrypt):
+ if can_encrypt:
+ self.log.debug('Accepting mail for %s...' % user.dest.addrstr)
+ emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED,
+ self._userid, user.dest.addrstr)
+ elif self._encrypted_only:
emit_async(catalog.SMTP_RECIPIENT_REJECTED, self._userid,
user.dest.addrstr)
raise smtp.SMTPBadRcpt(user.dest.addrstr)
- self.log.warn(
- '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)
+ else:
+ self.log.warn(
+ '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)
+ return lambda: EncryptedMessage(user, self._outgoing)
- d = self._km.get_key(address)
- d.addCallbacks(found, not_found)
+ d = self._outgoing.can_encrypt_for(address)
+ d.addCallback(verify_if_can_encrypt_for_recipient)
d.addCallback(encrypt_func)
return d
@@ -379,7 +310,7 @@ class EncryptedMessage(object):
self._user = user
self._lines = []
- self._outgoing_mail = outgoing_mail
+ self._outgoing = outgoing_mail
def lineReceived(self, line):
"""
@@ -402,7 +333,7 @@ class EncryptedMessage(object):
self._lines.append('') # add a trailing newline
raw_mail = '\r\n'.join(self._lines)
- return self._outgoing_mail.send_message(raw_mail, self._user)
+ return self._outgoing.send_message(raw_mail, self._user)
def connectionLost(self):
"""
diff --git a/src/leap/bitmask/mail/smtp/service.py b/src/leap/bitmask/mail/smtp/service.py
index 6fd000ce..788b2791 100644
--- a/src/leap/bitmask/mail/smtp/service.py
+++ b/src/leap/bitmask/mail/smtp/service.py
@@ -31,16 +31,15 @@ log = Logger()
SMTP_PORT = 2013
-def run_service(soledad_sessions, keymanager_sessions, sendmail_opts,
- port=SMTP_PORT, factory=None):
+def run_service(outgoing_sessions, soledad_sessions, port=SMTP_PORT,
+ factory=None):
"""
Main entry point to run the service from the client.
+ :param outgoing_sessions: a dict-like object, containing instances
+ of outgoing, indexed by userid.
: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.
:param factory: a factory for the protocol that will listen in the given
port
@@ -49,8 +48,7 @@ def run_service(soledad_sessions, keymanager_sessions, sendmail_opts,
:rtype: tuple
"""
if not factory:
- factory = SMTPFactory(soledad_sessions, keymanager_sessions,
- sendmail_opts)
+ factory = SMTPFactory(outgoing_sessions, soledad_sessions)
try:
interface = "localhost"
diff --git a/src/leap/bitmask/mail/testing/smtp.py b/src/leap/bitmask/mail/testing/smtp.py
index 834433f3..36628187 100644
--- a/src/leap/bitmask/mail/testing/smtp.py
+++ b/src/leap/bitmask/mail/testing/smtp.py
@@ -2,7 +2,6 @@ from twisted.mail import smtp
from leap.bitmask.mail.smtp.gateway import SMTPFactory, LOCAL_FQDN
from leap.bitmask.mail.smtp.gateway import SMTPDelivery
-from leap.bitmask.mail.outgoing.service import outgoingFactory
TEST_USER = u'anotheruser@leap.se'
@@ -11,21 +10,12 @@ class UnauthenticatedSMTPServer(smtp.SMTP):
encrypted_only = False
- def __init__(self, soledads, keyms, opts, encrypted_only=False):
+ def __init__(self, outgoing_s, soledad_s, encrypted_only=False):
smtp.SMTP.__init__(self)
userid = TEST_USER
- keym = keyms[userid]
-
- class Opts:
- cert = '/tmp/cert'
- key = '/tmp/cert'
- hostname = 'remote'
- port = 666
-
- outgoing = outgoingFactory(
- userid, keym, Opts, check_cert=False)
- avatar = SMTPDelivery(userid, keym, encrypted_only, outgoing)
+ outgoing = outgoing_s[userid]
+ avatar = SMTPDelivery(userid, encrypted_only, outgoing)
self.delivery = avatar
def validateFrom(self, helo, origin):
@@ -42,10 +32,8 @@ class UnauthenticatedSMTPFactory(SMTPFactory):
encrypted_only = False
-def getSMTPFactory(soledad_s, keymanager_s, sendmail_opts,
- encrypted_only=False):
+def getSMTPFactory(outgoing_s, soledad_s, encrypted_only=False):
factory = UnauthenticatedSMTPFactory
factory.encrypted_only = encrypted_only
- proto = factory(
- soledad_s, keymanager_s, sendmail_opts).buildProtocol(('127.0.0.1', 0))
+ proto = factory(outgoing_s, soledad_s).buildProtocol(('127.0.0.1', 0))
return proto
diff --git a/tests/integration/mail/outgoing/test_outgoing.py b/tests/integration/mail/outgoing/test_outgoing.py
index 72731925..e2787a8b 100644
--- a/tests/integration/mail/outgoing/test_outgoing.py
+++ b/tests/integration/mail/outgoing/test_outgoing.py
@@ -32,6 +32,7 @@ from mock import Mock
from leap.bitmask.mail.rfc3156 import RFC3156CompliantGenerator
from leap.bitmask.mail.outgoing.service import OutgoingMail
+from leap.bitmask.mail.outgoing.sender import SMTPSender
from leap.bitmask.mail.testing import ADDRESS, ADDRESS_2, PUBLIC_KEY_2
from leap.bitmask.mail.testing import KeyManagerWithSoledadTestCase
from leap.bitmask.mail.testing.smtp import getSMTPFactory
@@ -75,9 +76,9 @@ class TestOutgoingMail(KeyManagerWithSoledadTestCase):
self.opts = opts
def init_outgoing_and_proto(_):
- self.outgoing_mail = OutgoingMail(
- self.fromAddr, self.km, opts.cert,
- opts.key, opts.hostname, opts.port)
+ self.outgoing = OutgoingMail(self.fromAddr, self.km)
+ self.outgoing.add_sender(
+ SMTPSender(self.fromAddr, opts.key, opts.hostname, opts.port))
user = TEST_USER
@@ -101,7 +102,7 @@ class TestOutgoingMail(KeyManagerWithSoledadTestCase):
decrypted,
'Decrypted text does not contain the original text.')
- d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)
+ d = self.outgoing._maybe_encrypt_and_sign(self.raw, self.dest)
d.addCallback(self._assert_encrypted)
d.addCallback(lambda message: self.km.decrypt(
message.get_payload(1).get_payload(), ADDRESS))
@@ -122,7 +123,7 @@ class TestOutgoingMail(KeyManagerWithSoledadTestCase):
self.assertTrue(ADDRESS_2 in signkey.address,
"Verification failed")
- d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)
+ d = self.outgoing._maybe_encrypt_and_sign(self.raw, self.dest)
d.addCallback(self._assert_encrypted)
d.addCallback(lambda message: self.km.decrypt(
message.get_payload(1).get_payload(), ADDRESS, verify=ADDRESS_2))
@@ -139,9 +140,7 @@ class TestOutgoingMail(KeyManagerWithSoledadTestCase):
return_value=fail(errors.KeyNotFound()))
recipient = User('ihavenopubkey@nonleap.se',
'gateway.leap.se', self.proto, ADDRESS)
- self.outgoing_mail = OutgoingMail(
- self.fromAddr, self.km, self.opts.cert, self.opts.key,
- self.opts.hostname, self.opts.port)
+ self.outgoing = OutgoingMail(self.fromAddr, self.km)
def check_signed(res):
message, _ = res
@@ -187,13 +186,13 @@ class TestOutgoingMail(KeyManagerWithSoledadTestCase):
return d
# TODO shouldn't depend on private method on this test
- d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, recipient)
+ d = self.outgoing._maybe_encrypt_and_sign(self.raw, recipient)
d.addCallback(check_signed)
d.addCallback(verify)
return d
def test_attach_key(self):
- d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)
+ d = self.outgoing._maybe_encrypt_and_sign(self.raw, self.dest)
d.addCallback(self._assert_encrypted)
d.addCallback(self._check_headers, self.lines[:4])
d.addCallback(lambda message: self.km.decrypt(
@@ -209,7 +208,7 @@ class TestOutgoingMail(KeyManagerWithSoledadTestCase):
raw = '\r\n'.join(lines)
dest = User(unknown_address, 'gateway.leap.se', self.proto, ADDRESS_2)
- d = self.outgoing_mail._maybe_encrypt_and_sign(
+ d = self.outgoing._maybe_encrypt_and_sign(
raw, dest, fetch_remote=False)
d.addCallback(lambda (message, _):
self._check_headers(message, lines[:4]))
diff --git a/tests/integration/mail/smtp/test_gateway.py b/tests/integration/mail/smtp/test_gateway.py
index aac90b50..254ee029 100644
--- a/tests/integration/mail/smtp/test_gateway.py
+++ b/tests/integration/mail/smtp/test_gateway.py
@@ -29,6 +29,7 @@ from twisted.test import proto_helpers
from mock import Mock
from leap.bitmask.keymanager import openpgp, errors
+from leap.bitmask.mail.outgoing.service import OutgoingMail
from leap.bitmask.mail.testing import KeyManagerWithSoledadTestCase
from leap.bitmask.mail.testing import ADDRESS, ADDRESS_2
from leap.bitmask.mail.testing.smtp import getSMTPFactory, TEST_USER
@@ -91,7 +92,8 @@ class TestSmtpGateway(KeyManagerWithSoledadTestCase):
'354 Continue']
user = TEST_USER
- proto = getSMTPFactory({user: None}, {user: self.km}, {user: None})
+ proto = getSMTPFactory({user: OutgoingMail(user, self.km)},
+ {user: None})
transport = proto_helpers.StringTransport()
proto.makeConnection(transport)
reply = ""
@@ -117,7 +119,7 @@ class TestSmtpGateway(KeyManagerWithSoledadTestCase):
return_value=fail(errors.KeyNotFound()))
user = TEST_USER
proto = getSMTPFactory(
- {user: None}, {user: self.km}, {user: None},
+ {user: OutgoingMail(user, self.km)}, {user: None},
encrypted_only=True)
transport = proto_helpers.StringTransport()
proto.makeConnection(transport)
@@ -147,7 +149,8 @@ class TestSmtpGateway(KeyManagerWithSoledadTestCase):
self.km._fetch_keys_from_server_and_store_local = Mock(
return_value=fail(errors.KeyNotFound()))
user = TEST_USER
- proto = getSMTPFactory({user: None}, {user: self.km}, {user: None})
+ proto = getSMTPFactory({user: OutgoingMail(user, self.km)},
+ {user: None})
transport = proto_helpers.StringTransport()
proto.makeConnection(transport)
yield self.getReply(self.EMAIL_DATA[0] + '\r\n', proto, transport)
diff --git a/tests/unit/mail/outgoing/test_service.py b/tests/unit/mail/outgoing/test_service.py
index 9200397b..7b6df847 100644
--- a/tests/unit/mail/outgoing/test_service.py
+++ b/tests/unit/mail/outgoing/test_service.py
@@ -36,7 +36,6 @@ class TestService(unittest.TestCase):
def test_send_error_bounces_if_bouncer_is_provided(self):
bouncer = MagicMock()
outgoing_mail = OutgoingMail(self.from_address, self.keymanager,
- self.cert, self.key, self.host, self.port,
bouncer)
failure = Failure(exc_value=Exception())
@@ -48,7 +47,6 @@ class TestService(unittest.TestCase):
def test_send_error_raises_exception_if_there_is_no_bouncer(self):
bouncer = None
outgoing_mail = OutgoingMail(self.from_address, self.keymanager,
- self.cert, self.key, self.host, self.port,
bouncer)
failure = Failure(exc_value=Exception('smtp error'))