summaryrefslogtreecommitdiff
path: root/mail/src/leap/mail/outgoing/service.py
diff options
context:
space:
mode:
Diffstat (limited to 'mail/src/leap/mail/outgoing/service.py')
-rw-r--r--mail/src/leap/mail/outgoing/service.py518
1 files changed, 0 insertions, 518 deletions
diff --git a/mail/src/leap/mail/outgoing/service.py b/mail/src/leap/mail/outgoing/service.py
deleted file mode 100644
index 8b02f2e..0000000
--- a/mail/src/leap/mail/outgoing/service.py
+++ /dev/null
@@ -1,518 +0,0 @@
-# -*- coding: utf-8 -*-
-# outgoing/service.py
-# Copyright (C) 2013-2015 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/>.
-
-"""
-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
-from email.parser import Parser
-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.protocols.amp import ssl
-from twisted.python import log
-
-from leap.common.check import leap_assert_type, leap_assert
-from leap.common.events import emit_async, catalog
-from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch
-from leap.mail import __version__
-from leap.mail import errors
-from leap.mail.utils import validate_address
-from leap.mail.rfc3156 import MultipartEncrypted
-from leap.mail.rfc3156 import MultipartSigned
-from leap.mail.rfc3156 import encode_base64_rec
-from leap.mail.rfc3156 import RFC3156CompliantGenerator
-from leap.mail.rfc3156 import PGPSignature
-from leap.mail.rfc3156 import PGPEncrypted
-
-# TODO
-# [ ] rename this module to something else, service should be the implementor
-# 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.
- """
-
- def __init__(self, from_address, keymanager, cert, key, host, port,
- bouncer=None):
- """
- Initialize the outgoing 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
- """
-
- # assert params
- leap_assert_type(from_address, str)
- 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, 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
- self._bouncer = bouncer
-
- 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 = self._maybe_encrypt_and_sign(raw, recipient)
- d.addCallback(self._route_msg, raw)
- d.addErrback(self.sendError, raw)
- 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]
- fromaddr = self._from_address
- log.msg('Message sent from %s to %s' % (fromaddr, dest_addrstr))
- emit_async(catalog.SMTP_SEND_MESSAGE_SUCCESS,
- fromaddr, dest_addrstr)
-
- def sendError(self, failure, origmsg):
- """
- Callback for an unsuccessfull send.
-
- :param failure: The result from the last errback.
- :type failure: anything
- :param origmsg: the original, unencrypted, raw message, to be passed to
- the bouncer.
- :type origmsg: str
- """
- # XXX: need to get the address from the original message to send signal
- # emit_async(catalog.SMTP_SEND_MESSAGE_ERROR, self._from_address,
- # self._user.dest.addrstr)
-
- # TODO when we implement outgoing queues/long-term-retries, we could
- # examine the error *here* and delay the notification if it's just a
- # temporal error. We might want to notify the permanent errors
- # differently.
-
- err = failure.value
- log.err(err)
-
- if self._bouncer:
- self._bouncer.bounce_message(
- err.message, to=self._from_address,
- orig=origmsg)
- else:
- raise err
-
- def _route_msg(self, encrypt_and_sign_result, raw):
- """
- 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.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.mail-' + __version__)
- 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))
-
- def _maybe_encrypt_and_sign(self, raw, recipient, fetch_remote=True):
- """
- 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
-
- :return: A Deferred that will be fired with a MIMEMultipart message
- and the original recipient Message
- :rtype: Deferred
- """
- # pass if the original message's content-type is "multipart/encrypted"
- origmsg = Parser().parsestr(raw)
-
- if origmsg.get_content_type() == 'multipart/encrypted':
- return defer.succeed((origmsg, recipient))
-
- from_address = validate_address(self._from_address)
- username, domain = from_address.split('@')
- to_address = validate_address(recipient.dest.addrstr)
-
- def maybe_encrypt_and_sign(message):
- d = self._encrypt_and_sign(
- message, to_address, from_address,
- fetch_remote=fetch_remote)
- d.addCallbacks(signal_encrypt_sign,
- if_key_not_found_send_unencrypted,
- errbackArgs=(message,))
- return d
-
- def signal_encrypt_sign(newmsg):
- emit_async(catalog.SMTP_END_ENCRYPT_AND_SIGN,
- self._from_address,
- "%s,%s" % (self._from_address, to_address))
- return newmsg, recipient
-
- def if_key_not_found_send_unencrypted(failure, message):
- failure.trap(KeyNotFound, KeyAddressMismatch)
-
- log.msg('Will send unencrypted message to %s.' % to_address)
- emit_async(catalog.SMTP_START_SIGN, self._from_address, to_address)
- d = self._sign(message, from_address)
- d.addCallback(signal_sign)
- return d
-
- def signal_sign(newmsg):
- emit_async(catalog.SMTP_END_SIGN, self._from_address)
- return newmsg, recipient
-
- log.msg("Will encrypt the message with %s and sign with %s."
- % (to_address, from_address))
- emit_async(catalog.SMTP_START_ENCRYPT_AND_SIGN,
- self._from_address,
- "%s,%s" % (self._from_address, to_address))
- d = self._maybe_attach_key(origmsg, from_address, to_address)
- d.addCallback(maybe_encrypt_and_sign)
- return d
-
- def _maybe_attach_key(self, origmsg, from_address, to_address):
- filename = "%s-email-key.asc" % (from_address,)
-
- def attach_if_address_hasnt_encrypted(to_key):
- # if the sign_used flag is true that means that we got an encrypted
- # email from this address, because we conly check signatures on
- # encrypted emails. In this case we don't attach.
- # XXX: this might not be true some time in the future
- if to_key.sign_used:
- return origmsg
- return get_key_and_attach(None)
-
- def get_key_and_attach(_):
- d = self._keymanager.get_key(from_address, fetch_remote=False)
- d.addCallback(attach_key)
- return d
-
- def attach_key(from_key):
- msg = origmsg
- if not origmsg.is_multipart():
- msg = MIMEMultipart()
- for h, v in origmsg.items():
- msg.add_header(h, v)
- msg.attach(MIMEText(origmsg.get_payload()))
-
- keymsg = MIMEApplication(from_key.key_data, _subtype='pgp-keys',
- _encoder=lambda x: x)
- keymsg.add_header('content-disposition', 'attachment',
- filename=filename)
- msg.attach(keymsg)
- return msg
-
- d = self._keymanager.get_key(to_address, fetch_remote=False)
- d.addCallbacks(attach_if_address_hasnt_encrypted, get_key_and_attach)
- d.addErrback(lambda _: origmsg)
- return d
-
- def _encrypt_and_sign(self, origmsg, encrypt_address, sign_address,
- fetch_remote=True):
- """
- Create an RFC 3156 compliang PGP encrypted and signed message using
- C{encrypt_address} to encrypt and C{sign_address} to sign.
-
- :param origmsg: The original message
- :type origmsg: email.message.Message
- :param encrypt_address: The address used to encrypt the message.
- :type encrypt_address: str
- :param sign_address: The address used to sign the message.
- :type sign_address: str
-
- :return: A Deferred with the MultipartEncrypted message
- :rtype: Deferred
- """
- # create new multipart/encrypted message with 'pgp-encrypted' protocol
-
- def encrypt(res):
- newmsg, origmsg = res
- d = self._keymanager.encrypt(
- origmsg.as_string(unixfrom=False),
- encrypt_address, sign=sign_address,
- fetch_remote=fetch_remote)
- d.addCallback(lambda encstr: (newmsg, encstr))
- return d
-
- def create_encrypted_message(res):
- newmsg, encstr = res
- encmsg = MIMEApplication(
- encstr, _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
-
- d = self._fix_headers(
- origmsg,
- MultipartEncrypted('application/pgp-encrypted'),
- sign_address)
- d.addCallback(encrypt)
- d.addCallback(create_encrypted_message)
- return d
-
- def _sign(self, origmsg, sign_address):
- """
- Create an RFC 3156 compliant PGP signed MIME message using
- C{sign_address}.
-
- :param origmsg: The original message
- :type origmsg: email.message.Message
- :param sign_address: The address used to sign the message.
- :type sign_address: str
-
- :return: A Deferred with the MultipartSigned message.
- :rtype: Deferred
- """
- # 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"
-
- def create_signed_message(res):
- (msg, _), signature = res
- sigmsg = PGPSignature(signature)
- # attach original message and signature to new message
- msg.attach(origmsg)
- msg.attach(sigmsg)
- return msg
-
- dh = self._fix_headers(
- origmsg,
- MultipartSigned('application/pgp-signature', 'pgp-sha512'),
- sign_address)
- ds = self._keymanager.sign(
- msgtext, sign_address, digest_algo='SHA512',
- clearsign=False, detach=True, binary=False)
- d = defer.gatherResults([dh, ds])
- d.addCallback(create_signed_message)
- return d
-
- def _fix_headers(self, msg, newmsg, sign_address):
- """
- 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 msg: The original message.
- :type msg: email.message.Message
- :param newmsg: The new message being created.
- :type newmsg: email.message.Message
- :param sign_address: The address used to sign C{newmsg}
- :type sign_address: str
-
- :return: A Deferred with a touple:
- (new Message with the unencrypted headers,
- original Message with headers removed)
- :rtype: Deferred
- """
- origmsg = deepcopy(msg)
- # 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())
- # delete user-agent from origmsg
- del (origmsg['user-agent'])
-
- def add_openpgp_header(signkey):
- username, domain = sign_address.split('@')
- newmsg.add_header(
- 'OpenPGP', 'id=%s' % signkey.fingerprint,
- url='https://%s/key/%s' % (domain, username),
- preference='signencrypt')
- return newmsg, origmsg
-
- d = self._keymanager.get_key(sign_address, private=True)
- d.addCallback(add_openpgp_header)
- return d