From 5dfe5a2831adbe3ec129d2ede1d9039739e98b71 Mon Sep 17 00:00:00 2001 From: Anton Khirnov Date: Sat, 30 Jan 2021 13:33:27 +0100 Subject: db/envelope: switch to the "new" (EmailMessage) python API email.mime is a part of the old API, which does not mix well with the new one (i.e. when email.policy.SMTP is used), specifically when non-ASCII headers are used. Additionally, clean the APIs that accept either EmailMessage or a str to only expect EmailMessage. Supporting both just adds confusion and complexity. --- alot/db/envelope.py | 114 ++++++++++++++++++++++++++-------------------------- 1 file changed, 57 insertions(+), 57 deletions(-) (limited to 'alot/db') diff --git a/alot/db/envelope.py b/alot/db/envelope.py index 8340ba4e..f9909308 100644 --- a/alot/db/envelope.py +++ b/alot/db/envelope.py @@ -8,12 +8,11 @@ import re import email import email.policy from email.encoders import encode_7or8bit +from email.message import MIMEPart from email.mime.audio import MIMEAudio from email.mime.base import MIMEBase from email.mime.image import MIMEImage from email.mime.text import MIMEText -from email.mime.multipart import MIMEMultipart -from email.mime.application import MIMEApplication import email.charset as charset from urllib.parse import unquote @@ -292,29 +291,33 @@ class Envelope: def construct_mail(self): """ compiles the information contained in this envelope into a - :class:`email.Message`. + :class:`email.message.EmailMessage`. """ - # Build body text part. To properly sign/encrypt messages later on, we - # convert the text to its canonical format (as per RFC 2015). - canonical_format = self.body.encode('utf-8') - textpart = MIMEText(canonical_format, 'plain', 'utf-8') - - # wrap it in a multipart container if necessary - if self.attachments: - inner_msg = MIMEMultipart() - inner_msg.attach(textpart) - # add attachments - for a in self.attachments: - inner_msg.attach(a.get_mime_representation()) - else: - inner_msg = textpart + # make suire everything is 7-bit clean to avoid + # compatibility problems + # TODO: consider SMTPUTF8 support? + policy = email.policy.SMTP.clone(cte_type = '7bit') + + # we actually use MIMEPart instead of EmailMessage, to + # avoid the subparts getting spurious MIME-Version everywhere + mail = MIMEPart(policy = policy) + mail.set_content(self.body, subtype = 'plain', charset = 'utf-8') + + # add attachments + for a in self.attachments: + maintype, _, subtype = a.get_content_type().partition('/') + fname = a.get_filename() + data = a.get_data() + mail.add_attachment(data, filename = fname, + maintype = maintype, subtype = subtype) if self.sign: - plaintext = inner_msg.as_bytes(policy=email.policy.SMTP) + to_sign = mail + plaintext = to_sign.as_bytes() logging.debug('signing plaintext: %s', plaintext) try: - signatures, signature_str = crypto.detached_signature_for( + signatures, signature_blob = crypto.detached_signature_for( plaintext, [self.sign_key]) if len(signatures) != 1: raise GPGProblem("Could not sign message (GPGME " @@ -334,55 +337,47 @@ class Envelope: code=GPGCode.BAD_PASSPHRASE) raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_SIGN) - micalg = crypto.RFC3156_micalg_from_algo(signatures[0].hash_algo) - unencrypted_msg = MIMEMultipart( - 'signed', micalg=micalg, protocol='application/pgp-signature') - - # wrap signature in MIMEcontainter - stype = 'pgp-signature; name="signature.asc"' - signature_mime = MIMEApplication( - _data=signature_str.decode('ascii'), - _subtype=stype, - _encoder=encode_7or8bit) + signature_mime = MIMEPart(policy = to_sign.policy) + signature_mime.set_content(signature_blob, maintype = 'application', + subtype = 'pgp-signature') + signature_mime.set_param('name', 'signature.asc') signature_mime['Content-Description'] = 'signature' - signature_mime.set_charset('us-ascii') - # add signed message and signature to outer message - unencrypted_msg.attach(inner_msg) - unencrypted_msg.attach(signature_mime) - unencrypted_msg['Content-Disposition'] = 'inline' - else: - unencrypted_msg = inner_msg + # FIXME: this uses private methods, because + # python's "new" EmailMessage API does not + # allow arbitrary multipart constructs + mail = MIMEPart(policy = to_sign.policy) + mail._make_multipart('signed', (), None) + mail.set_param('protocol', 'application/pgp-signature') + mail.set_param('micalg', crypto.RFC3156_micalg_from_algo(signatures[0].hash_algo)) + mail.attach(to_sign) + mail.attach(signature_mime) if self.encrypt: - plaintext = unencrypted_msg.as_bytes(policy=email.policy.SMTP) + to_encrypt = mail + plaintext = to_encrypt.as_bytes() logging.debug('encrypting plaintext: %s', plaintext) try: - encrypted_str = crypto.encrypt( + encrypted_blob = crypto.encrypt( plaintext, list(self.encrypt_keys.values())) except gpg.errors.GPGMEError as e: raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT) - outer_msg = MIMEMultipart('encrypted', - protocol='application/pgp-encrypted') + version_str = b'Version: 1' + encryption_mime = MIMEPart(policy = to_encrypt.policy) + encryption_mime.set_content(version_str, maintype = 'application', + subtype = 'pgp-encrypted') - version_str = 'Version: 1' - encryption_mime = MIMEApplication(_data=version_str, - _subtype='pgp-encrypted', - _encoder=encode_7or8bit) - encryption_mime.set_charset('us-ascii') + encrypted_mime = MIMEPart(policy = to_encrypt.policy) + encrypted_mime.set_content(encrypted_blob, maintype = 'application', + subtype = 'octet-stream') - encrypted_mime = MIMEApplication( - _data=encrypted_str.decode('ascii'), - _subtype='octet-stream', - _encoder=encode_7or8bit) - encrypted_mime.set_charset('us-ascii') - outer_msg.attach(encryption_mime) - outer_msg.attach(encrypted_mime) - - else: - outer_msg = unencrypted_msg + mail = MIMEPart(policy = to_encrypt.policy) + mail._make_multipart('encrypted', (), None) + mail.set_param('protocol', 'application/pgp-encrypted') + mail.attach(encryption_mime) + mail.attach(encrypted_mime) headers = self.headers.copy() @@ -406,9 +401,14 @@ class Envelope: # copy headers from envelope to mail for k, vlist in headers.items(): for v in vlist: - outer_msg.add_header(k, v) + mail.add_header(k, v) + + # as we are using MIMEPart instead of EmailMessage, set the + # MIME version manually + del mail['MIME-Version'] + mail['MIME-Version'] = '1.0' - return outer_msg + return mail def parse_template(self, raw, reset=False, only_body=False): """parses a template or user edited string to fills this envelope. -- cgit v1.2.3