python's email.mime
tends to use encoding base64
or 7bit
and us-ascii
. I would like to use quoted-printable
and utf-8
as this is easier for humans to read and debug.
Currently, my emails look like
--===============6135350048414329636==
MIME-Version: 1.0
Content-Type: text/plain
Content-Transfer-Encoding: base64
IyEvYmluL2Jhc2gKCmZvciBpIGluIHs4Mjg4Li44N
or
--===============0756888342500148236==
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
happy face =E2=98=BA
I would like the raw email to be in quoted-printable unicode so it is easier for humans to read.
--===============5610730199728027971==
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="utf-8"
happy face ☺
content-transfer-encoding
When creating the MIMEText
object, which will be attached to the MIMEMultipart
object, set the content-transfer-encoding
to value quoted-printable
first, then do set_payload
. The order of operations matters.
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
# first create MIMEText, then set content-transfer-encoding, then set payload
mt = MIMEText(None, _subtype='plain')
mt.replace_header('content-transfer-encoding', 'quoted-printable')
mt.set_payload(u'happy face ☺', 'utf-8')
# create the parent email object and the MIMEMultipart extension to it
email = MIMEMultipart('mixed')
inline = MIMEMultipart('alternative')
# assemble the objects
inline.attach(mt)
email.attach(inline)
charset
and various encodingscs = charset.Charset('utf-8')
cs.header_encoding = charset.QP
cs.body_encoding = charset.QP
email.set_charset(cs)
This creates a raw email that is human readable (except the base64 encoded file attachment)
>>> print(email)
--===============5610730199728027971==
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="utf-8"
happy face ☺
--===============5610730199728027971==--
--===============0985725891393820576==
Content-Type: text/x-sh
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test.sh"
Zm9vYmFyc2RmYXNkZmtqaGFzZGZrbGhhc2ZrbGpoYXNma2xqaGFzZmtsaGZkYXNmCg==
--===============0985725891393820576==--
The following is a longer script to provide more context for the prior code snippets.
This script will send a text/plain
section encoded in UTF-8. For fun, it will also attach a file.
The raw email this produces will be human readable (except for the file attachment).
from __future__ import print_function
from email import charset
from email.encoders import encode_base64
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import mimetypes
# create the parent email object
email = MIMEMultipart('mixed')
# set email charset and email encodings
cs_ = charset.Charset('utf-8')
cs_.header_encoding = charset.QP
cs_.body_encoding = charset.QP
email.set_charset(cs_)
# create the 'text/plain' MIMEText
# first create MIMEText, then set content-transfer-encoding, then set payload
mt = MIMEText(None, _subtype='plain')
mt.replace_header('content-transfer-encoding', 'quoted-printable')
mt.set_payload(u'happy face ☺', 'utf-8')
# assemble the parts
inline = MIMEMultipart('alternative')
inline.attach(mt)
email.attach(inline)
# for fun, attach a file to the email
my_file = '/tmp/test.sh'
mimetype, encoding = mimetypes.guess_type(my_file)
mimetype = mimetype or 'application/octet-stream'
mimetype = mimetype.split('/', 1)
attachment = MIMEBase(mimetype[0], mimetype[1])
attachment.set_payload(open(my_file, 'rb').read())
encode_base64(attachment)
attachment.add_header('Content-Disposition', 'attachment', filename=os.path.basename(my_file))
email.attach(attachment)
This creates a raw email that is human readable (except the base64 encoded file attachment)
>>> print(email)
--===============5610730199728027971==
MIME-Version: 1.0
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset="utf-8"
happy face ☺
--===============5610730199728027971==--
--===============0985725891393820576==
Content-Type: text/x-sh
MIME-Version: 1.0
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test.sh"
Zm9vYmFyc2RmYXNkZmtqaGFzZGZrbGhhc2ZrbGpoYXNma2xqaGFzZmtsaGZkYXNmCg==
--===============0985725891393820576==--
Using smtplib
, the email can be emailed.
import smtplib
# set email address headers
email['From'] = '[email protected]'
email['To'] = '[email protected]'
email['Subject'] = 'hello'
# send the email
smtp_srv = smtplib.SMTP('localhost')
smtp_srv.set_debuglevel(True)
print(mesg_html, end='\n\n')
print(email.as_string(), end='\n\n')
smtp_srv.sendmail('[email protected]', '[email protected]', email.as_string())
smtp_srv.quit()
In trying to alter the body of an existing message (email.Message object) and set its encoding to quoted-printable
, I found that this problem took way more effort than I had anticipated.
import email
#... 'part' is the Message object
content = part.get_payload(decode=True)
#... Modify content
part['Content-Transfer-Encoding'] = '8bit'
part.set_payload(content, 'UTF-8')
del part['Content-Transfer-Encoding']
email.encoders.encode_quopri(part)
Now, why do I set and then delete the Content-Transfer-Encoding
header? The set_payload
call will set the Content-Transfer-Encoding
header and encode the data (to Base64) if no header exists. Otherwise, the set_payload
call will assume the caller has already encoded the data and will not alter it (by encoding). So, it actually doesn't matter the value to which I set the Content-Transfer-Encoding
header, only that I don't leave it blank.
But then why do I need to delete the header? The email.encoders.encode_quopri
call will only add a header, so the message will result with multiple Content-Transfer-Encoding
headers.
So, just using set_payload
then encode_quopri
for a message with no Content-Transfer-Encoding
header will result in a quoted-printable representation of a Base64 string, and for a message with an existing Content-Transfer-Encoding
header will result in a message with duplicate headers. Using encode_quopri
then set_payload
may result in duplicate headers, but will not encode the message. Hence the add/delete rigamarole to avoid dipping into the quopri
module.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With