summaryrefslogtreecommitdiff
path: root/alot/db
diff options
context:
space:
mode:
authorAnton Khirnov <anton@khirnov.net>2021-01-30 13:33:27 +0100
committerAnton Khirnov <anton@khirnov.net>2021-01-30 13:33:27 +0100
commit5dfe5a2831adbe3ec129d2ede1d9039739e98b71 (patch)
treebf54920d084544c2e12cd532c94cf3bdcaa37f95 /alot/db
parent27e9478faefecf5b290c0fbd3df5b1fe9e18c97f (diff)
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.
Diffstat (limited to 'alot/db')
-rw-r--r--alot/db/envelope.py114
1 files changed, 57 insertions, 57 deletions
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.