From fcdc227c23e6958aab902c95aeab35285c9993f0 Mon Sep 17 00:00:00 2001 From: Kali Kaneko Date: Tue, 22 Sep 2015 17:38:48 -0400 Subject: [refactor] avoid circular import due to rfc3156 --- src/leap/mail/imap/mailbox.py | 2 + src/leap/mail/incoming/tests/test_incoming_mail.py | 2 +- src/leap/mail/mail.py | 12 +- src/leap/mail/mailbox_indexer.py | 5 +- src/leap/mail/outgoing/service.py | 12 +- src/leap/mail/outgoing/tests/test_outgoing.py | 2 +- src/leap/mail/rfc3156.py | 390 +++++++++++++++++++++ src/leap/mail/smtp/gateway.py | 5 +- src/leap/mail/smtp/rfc3156.py | 390 --------------------- 9 files changed, 415 insertions(+), 405 deletions(-) create mode 100644 src/leap/mail/rfc3156.py delete mode 100644 src/leap/mail/smtp/rfc3156.py (limited to 'src') diff --git a/src/leap/mail/imap/mailbox.py b/src/leap/mail/imap/mailbox.py index e73994b..c7accbb 100644 --- a/src/leap/mail/imap/mailbox.py +++ b/src/leap/mail/imap/mailbox.py @@ -215,11 +215,13 @@ class IMAPMailbox(object): but in the future will be useful to get absolute UIDs from message sequence numbers. + :param message: the message sequence number. :type message: int :rtype: int :return: the UID of the message. + """ # TODO support relative sequences. The (imap) message should # receive a sequence number attribute: a deferred is not expected diff --git a/src/leap/mail/incoming/tests/test_incoming_mail.py b/src/leap/mail/incoming/tests/test_incoming_mail.py index 964c8fd..6880496 100644 --- a/src/leap/mail/incoming/tests/test_incoming_mail.py +++ b/src/leap/mail/incoming/tests/test_incoming_mail.py @@ -36,7 +36,7 @@ from leap.mail.adaptors import soledad_indexes as fields from leap.mail.constants import INBOX_NAME from leap.mail.imap.account import IMAPAccount from leap.mail.incoming.service import IncomingMail -from leap.mail.smtp.rfc3156 import MultipartEncrypted, PGPEncrypted +from leap.mail.rfc3156 import MultipartEncrypted, PGPEncrypted from leap.mail.tests import ( TestCaseWithKeyManager, ADDRESS, diff --git a/src/leap/mail/mail.py b/src/leap/mail/mail.py index fc5abd2..c0e16a6 100644 --- a/src/leap/mail/mail.py +++ b/src/leap/mail/mail.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # mail.py -# Copyright (C) 2014 LEAP +# Copyright (C) 2014,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 @@ -15,7 +15,12 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ -Generic Access to Mail objects: Public LEAP Mail API. +Generic Access to Mail objects. + +This module holds the public LEAP Mail API, which should be viewed as the main +entry point for message and account manipulation, in a protocol-agnostic way. + +In the future, pluggable transports will expose this generic API. """ import itertools import uuid @@ -148,6 +153,9 @@ class MessagePart(object): # TODO This class should be better abstracted from the data model. # TODO support arbitrarily nested multiparts (right now we only support # the trivial case) + """ + Represents a part of a multipart MIME Message. + """ def __init__(self, part_map, cdocs={}, nested=False): """ diff --git a/src/leap/mail/mailbox_indexer.py b/src/leap/mail/mailbox_indexer.py index 08e5f10..c49f808 100644 --- a/src/leap/mail/mailbox_indexer.py +++ b/src/leap/mail/mailbox_indexer.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ +.. :py:module::mailbox_indexer + Local tables to store the message Unique Identifiers for a given mailbox. """ import re @@ -72,10 +74,11 @@ class MailboxIndexer(object): These indexes are Message Attributes needed for the IMAP specification (rfc 3501), although they can be useful for other non-imap store implementations. + """ # The uids are expected to be 32-bits values, but the ROWIDs in sqlite # are 64-bit values. I *don't* think it really matters for any - # practical use, but it's good to remmeber we've got that difference going + # practical use, but it's good to remember we've got that difference going # on. store = None diff --git a/src/leap/mail/outgoing/service.py b/src/leap/mail/outgoing/service.py index 3754650..7cc5a24 100644 --- a/src/leap/mail/outgoing/service.py +++ b/src/leap/mail/outgoing/service.py @@ -36,12 +36,12 @@ from leap.keymanager.openpgp import OpenPGPKey from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch from leap.mail import __version__ from leap.mail.utils import validate_address -from leap.mail.smtp.rfc3156 import MultipartEncrypted -from leap.mail.smtp.rfc3156 import MultipartSigned -from leap.mail.smtp.rfc3156 import encode_base64_rec -from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator -from leap.mail.smtp.rfc3156 import PGPSignature -from leap.mail.smtp.rfc3156 import PGPEncrypted +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 diff --git a/src/leap/mail/outgoing/tests/test_outgoing.py b/src/leap/mail/outgoing/tests/test_outgoing.py index 2376da9..5518b33 100644 --- a/src/leap/mail/outgoing/tests/test_outgoing.py +++ b/src/leap/mail/outgoing/tests/test_outgoing.py @@ -30,7 +30,7 @@ from twisted.mail.smtp import User from mock import Mock from leap.mail.smtp.gateway import SMTPFactory -from leap.mail.smtp.rfc3156 import RFC3156CompliantGenerator +from leap.mail.rfc3156 import RFC3156CompliantGenerator from leap.mail.outgoing.service import OutgoingMail from leap.mail.tests import ( TestCaseWithKeyManager, diff --git a/src/leap/mail/rfc3156.py b/src/leap/mail/rfc3156.py new file mode 100644 index 0000000..7d7bc0f --- /dev/null +++ b/src/leap/mail/rfc3156.py @@ -0,0 +1,390 @@ +# -*- 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 . + +""" +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 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 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 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') 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 . - -""" -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 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 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 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') -- cgit v1.2.3