summaryrefslogtreecommitdiff
path: root/mail/src/leap/mail/smtp/gateway.py
diff options
context:
space:
mode:
Diffstat (limited to 'mail/src/leap/mail/smtp/gateway.py')
-rw-r--r--mail/src/leap/mail/smtp/gateway.py413
1 files changed, 0 insertions, 413 deletions
diff --git a/mail/src/leap/mail/smtp/gateway.py b/mail/src/leap/mail/smtp/gateway.py
deleted file mode 100644
index e49bbe8..0000000
--- a/mail/src/leap/mail/smtp/gateway.py
+++ /dev/null
@@ -1,413 +0,0 @@
-# -*- 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 = []