summaryrefslogtreecommitdiff
path: root/src/leap/mail/smtp
diff options
context:
space:
mode:
authorKali Kaneko <kali@leap.se>2015-09-22 17:38:48 -0400
committerKali Kaneko <kali@leap.se>2015-09-23 12:00:15 -0400
commitfcdc227c23e6958aab902c95aeab35285c9993f0 (patch)
tree30b8e0f597267e828a98db971dff570ad54daf2e /src/leap/mail/smtp
parent309578890ba5d188f3d569473d756c28e06aa7ab (diff)
[refactor] avoid circular import due to rfc3156
Diffstat (limited to 'src/leap/mail/smtp')
-rw-r--r--src/leap/mail/smtp/gateway.py5
-rw-r--r--src/leap/mail/smtp/rfc3156.py390
2 files changed, 1 insertions, 394 deletions
diff --git a/src/leap/mail/smtp/gateway.py b/src/leap/mail/smtp/gateway.py
index c988367..45560bf 100644
--- a/src/leap/mail/smtp/gateway.py
+++ b/src/leap/mail/smtp/gateway.py
@@ -41,10 +41,7 @@ from leap.common.events import emit_async, catalog
from leap.keymanager.openpgp import OpenPGPKey
from leap.keymanager.errors import KeyNotFound
from leap.mail.utils import validate_address
-
-from leap.mail.smtp.rfc3156 import (
- RFC3156CompliantGenerator,
-)
+from leap.mail.rfc3156 import RFC3156CompliantGenerator
# replace email generator with a RFC 3156 compliant one.
from email import generator
diff --git a/src/leap/mail/smtp/rfc3156.py b/src/leap/mail/smtp/rfc3156.py
deleted file mode 100644
index 7d7bc0f..0000000
--- a/src/leap/mail/smtp/rfc3156.py
+++ /dev/null
@@ -1,390 +0,0 @@
-# -*- coding: utf-8 -*-
-# rfc3156.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/>.
-
-"""
-Implements RFC 3156: MIME Security with OpenPGP.
-"""
-
-import base64
-from StringIO import StringIO
-
-from twisted.python import log
-from email.mime.application import MIMEApplication
-from email.mime.multipart import MIMEMultipart
-from email import errors
-from email.generator import (
- Generator,
- fcre,
- NL,
- _make_boundary,
-)
-
-
-#
-# A generator that solves http://bugs.python.org/issue14983
-#
-
-class RFC3156CompliantGenerator(Generator):
- """
- An email generator that addresses Python's issue #14983 for multipart
- messages.
-
- This is just a copy of email.generator.Generator which fixes the following
- bug: http://bugs.python.org/issue14983
- """
-
- def _handle_multipart(self, msg):
- """
- A multipart handling implementation that addresses issue #14983.
-
- This is just a copy of the parent's method which fixes the following
- bug: http://bugs.python.org/issue14983 (see the line marked with
- "(***)").
-
- :param msg: The multipart message to be handled.
- :type msg: email.message.Message
- """
- # The trick here is to write out each part separately, merge them all
- # together, and then make sure that the boundary we've chosen isn't
- # present in the payload.
- msgtexts = []
- subparts = msg.get_payload()
- if subparts is None:
- subparts = []
- elif isinstance(subparts, basestring):
- # e.g. a non-strict parse of a message with no starting boundary.
- self._fp.write(subparts)
- return
- elif not isinstance(subparts, list):
- # Scalar payload
- subparts = [subparts]
- for part in subparts:
- s = StringIO()
- g = self.clone(s)
- g.flatten(part, unixfrom=False)
- msgtexts.append(s.getvalue())
- # BAW: What about boundaries that are wrapped in double-quotes?
- boundary = msg.get_boundary()
- if not boundary:
- # Create a boundary that doesn't appear in any of the
- # message texts.
- alltext = NL.join(msgtexts)
- boundary = _make_boundary(alltext)
- msg.set_boundary(boundary)
- # If there's a preamble, write it out, with a trailing CRLF
- if msg.preamble is not None:
- preamble = msg.preamble
- if self._mangle_from_:
- preamble = fcre.sub('>From ', msg.preamble)
- self._fp.write(preamble + '\n')
- # dash-boundary transport-padding CRLF
- self._fp.write('--' + boundary + '\n')
- # body-part
- if msgtexts:
- self._fp.write(msgtexts.pop(0))
- # *encapsulation
- # --> delimiter transport-padding
- # --> CRLF body-part
- for body_part in msgtexts:
- # delimiter transport-padding CRLF
- self._fp.write('\n--' + boundary + '\n')
- # body-part
- self._fp.write(body_part)
- # close-delimiter transport-padding
- self._fp.write('\n--' + boundary + '--' + '\n') # (***) Solve #14983
- if msg.epilogue is not None:
- self._fp.write('\n')
- epilogue = msg.epilogue
- if self._mangle_from_:
- epilogue = fcre.sub('>From ', msg.epilogue)
- self._fp.write(epilogue)
-
-
-#
-# Base64 encoding: these are almost the same as python's email.encoder
-# solution, but a bit modified.
-#
-
-def _bencode(s):
- """
- Encode C{s} in base64.
-
- :param s: The string to be encoded.
- :type s: str
- """
- # We can't quite use base64.encodestring() since it tacks on a "courtesy
- # newline". Blech!
- if not s:
- return s
- value = base64.encodestring(s)
- return value[:-1]
-
-
-def encode_base64(msg):
- """
- Encode a non-multipart message's payload in Base64 (in place).
-
- This method modifies the message contents in place and adds or replaces an
- appropriate Content-Transfer-Encoding header.
-
- :param msg: The non-multipart message to be encoded.
- :type msg: email.message.Message
- """
- encoding = msg.get('Content-Transfer-Encoding', None)
- if encoding is not None:
- encoding = encoding.lower()
- # XXX Python's email module can only decode quoted-printable, base64 and
- # uuencoded data, so we might have to implement other decoding schemes in
- # order to support RFC 3156 properly and correctly calculate signatures
- # for multipart attachments (eg. 7bit or 8bit encoded attachments). For
- # now, if content is already encoded as base64 or if it is encoded with
- # some unknown encoding, we just pass.
- if encoding in [None, 'quoted-printable', 'x-uuencode', 'uue', 'x-uue']:
- orig = msg.get_payload(decode=True)
- encdata = _bencode(orig)
- msg.set_payload(encdata)
- # replace or set the Content-Transfer-Encoding header.
- try:
- msg.replace_header('Content-Transfer-Encoding', 'base64')
- except KeyError:
- msg['Content-Transfer-Encoding'] = 'base64'
- elif encoding is not 'base64':
- log.err('Unknown content-transfer-encoding: %s' % encoding)
-
-
-def encode_base64_rec(msg):
- """
- Encode (possibly multipart) messages in base64 (in place).
-
- This method modifies the message contents in place.
-
- :param msg: The non-multipart message to be encoded.
- :type msg: email.message.Message
- """
- if not msg.is_multipart():
- encode_base64(msg)
- else:
- for sub in msg.get_payload():
- encode_base64_rec(sub)
-
-
-#
-# RFC 1847: multipart/signed and multipart/encrypted
-#
-
-class MultipartSigned(MIMEMultipart):
- """
- Multipart/Signed MIME message according to RFC 1847.
-
- 2.1. Definition of Multipart/Signed
-
- (1) MIME type name: multipart
- (2) MIME subtype name: signed
- (3) Required parameters: boundary, protocol, and micalg
- (4) Optional parameters: none
- (5) Security considerations: Must be treated as opaque while in
- transit
-
- The multipart/signed content type contains exactly two body parts.
- The first body part is the body part over which the digital signature
- was created, including its MIME headers. The second body part
- contains the control information necessary to verify the digital
- signature. The first body part may contain any valid MIME content
- type, labeled accordingly. The second body part is labeled according
- to the value of the protocol parameter.
-
- When the OpenPGP digital signature is generated:
-
- (1) The data to be signed MUST first be converted to its content-
- type specific canonical form. For text/plain, this means
- conversion to an appropriate character set and conversion of
- line endings to the canonical <CR><LF> sequence.
-
- (2) An appropriate Content-Transfer-Encoding is then applied; see
- section 3. In particular, line endings in the encoded data
- MUST use the canonical <CR><LF> sequence where appropriate
- (note that the canonical line ending may or may not be present
- on the last line of encoded data and MUST NOT be included in
- the signature if absent).
-
- (3) MIME content headers are then added to the body, each ending
- with the canonical <CR><LF> sequence.
-
- (4) As described in section 3 of this document, any trailing
- whitespace MUST then be removed from the signed material.
-
- (5) As described in [2], the digital signature MUST be calculated
- over both the data to be signed and its set of content headers.
-
- (6) The signature MUST be generated detached from the signed data
- so that the process does not alter the signed data in any way.
- """
-
- def __init__(self, protocol, micalg, boundary=None, _subparts=None):
- """
- Initialize the multipart/signed message.
-
- :param boundary: the multipart boundary string. By default it is
- calculated as needed.
- :type boundary: str
- :param _subparts: a sequence of initial subparts for the payload. It
- must be an iterable object, such as a list. You can always
- attach new subparts to the message by using the attach() method.
- :type _subparts: iterable
- """
- MIMEMultipart.__init__(
- self, _subtype='signed', boundary=boundary,
- _subparts=_subparts)
- self.set_param('protocol', protocol)
- self.set_param('micalg', micalg)
-
- def attach(self, payload):
- """
- Add the C{payload} to the current payload list.
-
- Also prevent from adding payloads with wrong Content-Type and from
- exceeding a maximum of 2 payloads.
-
- :param payload: The payload to be attached.
- :type payload: email.message.Message
- """
- # second payload's content type must be equal to the protocol
- # parameter given on object creation
- if len(self.get_payload()) == 1:
- if payload.get_content_type() != self.get_param('protocol'):
- raise errors.MultipartConversionError(
- 'Wrong content type %s.' % payload.get_content_type)
- # prevent from adding more payloads
- if len(self._payload) == 2:
- raise errors.MultipartConversionError(
- 'Cannot have more than two subparts.')
- MIMEMultipart.attach(self, payload)
-
-
-class MultipartEncrypted(MIMEMultipart):
- """
- Multipart/encrypted MIME message according to RFC 1847.
-
- 2.2. Definition of Multipart/Encrypted
-
- (1) MIME type name: multipart
- (2) MIME subtype name: encrypted
- (3) Required parameters: boundary, protocol
- (4) Optional parameters: none
- (5) Security considerations: none
-
- The multipart/encrypted content type contains exactly two body parts.
- The first body part contains the control information necessary to
- decrypt the data in the second body part and is labeled according to
- the value of the protocol parameter. The second body part contains
- the data which was encrypted and is always labeled
- application/octet-stream.
- """
-
- def __init__(self, protocol, boundary=None, _subparts=None):
- """
- :param protocol: The encryption protocol to be added as a parameter to
- the Content-Type header.
- :type protocol: str
- :param boundary: the multipart boundary string. By default it is
- calculated as needed.
- :type boundary: str
- :param _subparts: a sequence of initial subparts for the payload. It
- must be an iterable object, such as a list. You can always
- attach new subparts to the message by using the attach() method.
- :type _subparts: iterable
- """
- MIMEMultipart.__init__(
- self, _subtype='encrypted', boundary=boundary,
- _subparts=_subparts)
- self.set_param('protocol', protocol)
-
- def attach(self, payload):
- """
- Add the C{payload} to the current payload list.
-
- Also prevent from adding payloads with wrong Content-Type and from
- exceeding a maximum of 2 payloads.
-
- :param payload: The payload to be attached.
- :type payload: email.message.Message
- """
- # first payload's content type must be equal to the protocol parameter
- # given on object creation
- if len(self._payload) == 0:
- if payload.get_content_type() != self.get_param('protocol'):
- raise errors.MultipartConversionError(
- 'Wrong content type.')
- # second payload is always application/octet-stream
- if len(self._payload) == 1:
- if payload.get_content_type() != 'application/octet-stream':
- raise errors.MultipartConversionError(
- 'Wrong content type %s.' % payload.get_content_type)
- # prevent from adding more payloads
- if len(self._payload) == 2:
- raise errors.MultipartConversionError(
- 'Cannot have more than two subparts.')
- MIMEMultipart.attach(self, payload)
-
-
-#
-# RFC 3156: application/pgp-encrypted, application/pgp-signed and
-# application-pgp-signature.
-#
-
-class PGPEncrypted(MIMEApplication):
- """
- Application/pgp-encrypted MIME media type according to RFC 3156.
-
- * MIME media type name: application
- * MIME subtype name: pgp-encrypted
- * Required parameters: none
- * Optional parameters: none
- """
-
- def __init__(self, version=1):
- data = "Version: %d" % version
- MIMEApplication.__init__(self, data, 'pgp-encrypted')
-
-
-class PGPSignature(MIMEApplication):
- """
- Application/pgp-signature MIME media type according to RFC 3156.
-
- * MIME media type name: application
- * MIME subtype name: pgp-signature
- * Required parameters: none
- * Optional parameters: none
- """
- def __init__(self, _data, name='signature.asc'):
- MIMEApplication.__init__(self, _data, 'pgp-signature',
- _encoder=lambda x: x, name=name)
- self.add_header('Content-Description', 'OpenPGP Digital Signature')
-
-
-class PGPKeys(MIMEApplication):
- """
- Application/pgp-keys MIME media type according to RFC 3156.
-
- * MIME media type name: application
- * MIME subtype name: pgp-keys
- * Required parameters: none
- * Optional parameters: none
- """
-
- def __init__(self, _data):
- MIMEApplication.__init__(self, _data, 'pgp-keys')