from email.message import EmailMessage
from email.headerregistry import Address
msg = EmailMessage()
msg['From'] = Address("Pepé Le Pew", "pepe", "example.com")
msg['To'] = (
Address("Penelope Pussycat", "penelope", "example.com")
, Address("Fabrette Pussycat", "fabrette", "example.com")
)
msg['Subject'] = 'This email sent from Python code'
msg.set_content("""\
Salut!
Cela ressemble à un excellent recipie[1] déjeuner.
[1] http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718
--Pepé
""")
print(msg)
The above code produces an email message that uses base64 encoding. How to disable it? How to remove the field of MIME-Version?
Will the encoding of "Pepé" be correctly interpreted by the recipient? If not, what is the correct way to ensure its encoding is interpreted by the recipient correctly?
From: Pepé Le Pew <pepe#example.com>
To: Penelope Pussycat <penelope#example.com>,
Fabrette Pussycat <fabrette#example.com>
Subject: This email sent from Python code
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: base64
MIME-Version: 1.0
CQlTYWx1dCEKCgkJQ2VsYSByZXNzZW1ibGUgw6AgdW4gZXhjZWxsZW50IHJlY2lwaWVbMV0gZMOp
amV1bmVyLgoKCQlbMV0gaHR0cDovL3d3dy55dW1tbHkuY29tL3JlY2lwZS9Sb2FzdGVkLUFzcGFy
YWd1cy1FcGljdXJpb3VzLTIwMzcxOAoKCQktLVBlcMOpCgkJCg==
You absolutely must not remove the MIME-Version: header; it's what identifies this as a MIME message.
The From: header should indeed be RFC2047-encoded, and the documentation suggests that it will be "when the message is serialized". When you print(msg) you are not properly serializing it; you want print(msg.as_string()) which does exhibit the required serialization.
When it comes to the transfer encoding, Python's email library has an unattractive penchant for using base64 for content which could very well be encoded as quoted-printable instead. You can't really reliably send the content completely unencoded (though if you wanted to, the MIME 8bit or binary encodings would be able to accommodate that; but for backwards compatibility, SMTP requires everything to be encoded into a 7-bit representation).
In the old email library, various shenanigans were required to do this, but in the new EmailMessage API introduced in Python 3.6, you really only have to add cte='quoted-printable' to the set_content call.
from email.message import EmailMessage
from email.headerregistry import Address
msg = EmailMessage()
msg['From'] = Address("Pepé Le Pew", "pepe", "example.com")
msg['To'] = (
Address("Penelope Pussycat", "penelope", "example.com")
, Address("Fabrette Pussycat", "fabrette", "example.com")
)
msg['Subject'] = 'This email sent from Python code'
msg.set_content("""\
Salut!
Cela ressemble à un excellent recipie[1] déjeuner.
[1] http://www.yummly.com/recipe/Roasted-Asparagus-Epicurious-203718
--Pepé
""", cte="quoted-printable") # <-- notice added parameter
print(msg.as_string()) # <-- properly serialize
Unfortunately, figuring this out from the documentation is next to impossible. The documentation for set_content basically just defers to the policy which obscurely points to the raw_data_manager (if you even notice the link) ... where finally you hopefully notice the presence of the cte keyword argument.
Demo: https://ideone.com/eLAt11
(As an aside, you might also want to
replace('\n ', '\n')
in the body text.)
If you go for an 8bit or binary content transfer encoding, the difference between them is that the former has a line length limit (max 900-something characters) whereas the latter is completely unconstrained. But you need to be sure that the entire SMTP transfer path is 8-bit clean (at which point you might as well pivot to Unicode email / ESMTP SMTPUTF8 entirely).
For your possible entertainment, here are some old questions with crazy hacks for Python 3.5 and earlier.
Related
I'm trying to use the amazon SES simulator as documented here: http://docs.aws.amazon.com/ses/latest/DeveloperGuide/mailbox-simulator.html
The body of the message looks like this:
MIME-Version: 1.0
Content-Type: multipart/alternative; charset="utf-8";
boundary="===============123456789=="
Content-Transfer-Encoding: base64
Subject: hello test message!
Reply-To: my_address#my_provider.com
To: complaint#simulator.amazonses.com
Return-Path: my_address#my_provider.com
Bounces-to: my_address#my_provider.com
Errors-to: my_address#my_provider.com
From: my_address#my_provider.com
--===============123456789==
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: base64
Ym9keSB3aXRoIG5vbmNlOiAw
--===============123456789==
MIME-Version: 1.0
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: base64
Ym9keSB3aXRoIG5vbmNlOiAw
--===============123456789==--
I'm sending this as the body, and using the boto3 interface ses_client.send_raw_message.
I'm generating this message body with something like this
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import re
msg = MIMEMultipart('alternative')
msg.set_charset('utf8')
text_message='Ym9keSB3aXRoIG5vbmNlOiAw'
p = re.compile('<.*?>')
text_message = 'body with nonce: {}'.format(str(nonce))
text = MIMEText(p.sub('', text_message), 'plain', 'utf8')
html = MIMEText(text_message, 'html', 'utf8')
msg.attach(text)
msg.attach(html)
source = 'my_email#my_provider.com'
msg['Subject'] = 'hello test message!'
msg['Reply-To'] = source
msg['To'] = to_mail
msg['Return-Path'] = source
msg['Bounces-to'] = source
msg['Errors-to'] = source
So I can send emails through SES and it works just fine.
I can also send emails to complaint#simulator.amazonses.com, and that works.
I haven't set up any SNS messaging, but I expected, through all the header fields that I set, to get a bounce back at the desired address. However, nothing happens if i use bounce#simulator.amazonses.com.
This is a list of header fields that amazon acknowledges:
http://docs.aws.amazon.com/ses/latest/DeveloperGuide/header-fields.html
I have also enabled email feedback forwarding, as described here:
http://docs.aws.amazon.com/ses/latest/DeveloperGuide/notifications-via-email.html
Also, for sending the emails I use this logic:
amazon_client = boto3.client('ses', 'region', **credentials)
amazon_client.send_raw_email(
Source=from_mail,
Destinations=to_mails,
RawMessage={'Data': body}
)
According to the page documenting how to receive notifications, I should be getting mails back at the Source address specified - which I do set, and I do have the email feedback forwarding enabled for....
Any ideas what I might be missing?
The headers you are putting in suggest that the real problem is false expectations. Errors-To: will be ignored pretty much by everything you can find, and Return-Path: will be replaced with the envelope sender by the receiving MTA. To control bounces, you need to control the envelope sender (traditionally, with sendmail -f) and what you put in the headers is completely irrelevant.
Solved.
The bounce address was different from the 'From' address. Therefore, the bounces ended up in the SPAM of that other address.
Interestingly, that address had a filter to forward everything to the address I had used as for 'From' address, when sending the email.
So my confusion was caused because I expected to get mail forwarded from that other address, but the bounces never got forwarded.
After a bunch of fiddling, I finally hit upon the magical sequence to attach a text file to an email (many thanks to previous posts on this service).
I'm left wondering what the lines:
attachment.add_header('Content-Disposition'. . .)
--and--
e_msg = MIMEMultipart('alternative')
actually do.
Can someone unsilence the Mimes for me please (sorry couldn't resist)
import smtplib
from email import Encoders
from email.message import Message
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
smtp_server = "1.2.3.4"
smtp_login = "account"
smpt_password = "password"
server = smtplib.SMTP(smtp_server)
server.login(smtp_login,smtp_password)
f = file("filename.csv")
attachment = MIMEText(f.read())
attachment.add_header('Content-Disposition', 'attachment', filename="filename.csv")
e_msg = MIMEMultipart('alternative')
e_msg.attach(attachment)
e_msg['Subject'] = 'Domestic Toll Monitor'
e_msg['From'] = smtp_account
body = 'Some nifty text goes here'
content = MIMEText(body)
e_msg.attach(content)
server.sendmail(smtp_from, smtp_to, e_msg.as_string())
Basically, MIME is the specification defining email structure. The Multipart structure is designed to allow for multiple types of messages and attachments to be sent within the same message. For example, an email might have a plain text version for backwards compatibility and a rich text or html formatted message for modern clients. Attachments count as a "part", and thus require their own header. In this case, you're adding a "Content-Disposition" type header for the attachment. If you're really interested in what that means, you can read the specification here. As for the "Alternative portion, you're setting the message to multipart and defining the types of parts that you have attached and how the client needs to handle them. There are some standard presets defining various scenarios, but Alternative is something of a wildcard, used when there is a part whose type might not be recognized or handled by most clients. For the record, I believe you also could have used a "Mixed" type. The nice thing about MIME is that while it is complicated, its thoroughly defined and its very easy to look up the specification.
I'm generating email with the help of Python email module.
Here are few lines of code, which demonstrates my question:
msg = email.MIMEMultipart.MIMEMultipart('alternative')
msg['From'] = "somemail#somedomain.com"
msg.as_string()
Out[7]: 'Content-Type: multipart/alternative;\n boundary="===============9006870443159801881=="\nMIME-Version: 1.0\nFrom: somemail#somedomain.com\n\n--===============9006870443159801881==\n\n--===============9006870443159801881==--'
As you can see, everything is okay here, From field contains email ant it is cool. But what if I want to add some name before email? Especially unicode one:
In [8]: u.get_full_name()
Out[8]: u'\u0414\u0438\u043c\u0430 \u0426\u0443\u043a\u0430\u043d\u043e\u0432'
In [9]: msg = email.MIMEMultipart.MIMEMultipart('alternative')
In [10]: msg['From'] = "%s <%s>" % (u.get_full_name(), "email#at.com")
In [11]: msg.as_string()
Out[11]: 'Content-Type: multipart/alternative;\n boundary="===============5792069034892928634=="\nMIME-Version: 1.0\nFrom: =?utf-8?b?0JTQuNC80LAg0KbRg9C60LDQvdC+0LIgPGVtYWlsQGF0LmNvbT4=?=\n\n--===============5792069034892928634==\n\n--===============5792069034892928634==--'
Here you can see, that all the string (name, email) was encoded in base64 (and it is even quite logical, how MIMEMultipart will know that string contains unicode and non-unicode parts).
So, my question is: how do I have to tell email module to make me pretty "From" header like:
From: =?UTF-8?B?0JLQmtC+0L3RgtCw0LrRgtC1?= <admin#notify.vk.com> ?
Also, I've learned a little RFC2822 (http://www.faqs.org/rfcs/rfc2822.html , p.3.6.2). It tells:
The originator fields indicate the mailbox(es) of the source of the
message. The "From:" field specifies the author(s) of the message,
that is, the mailbox(es) of the person(s) or system(s) responsible
for the writing of the message. The "Sender:" field specifies the
mailbox of the agent responsible for the actual transmission of the
message. For example, if a secretary were to send a message for
another person, the mailbox of the secretary would appear in the
"Sender:" field and the mailbox of the actual author would appear in
the "From:" field. If the originator of the message can be indicated
by a single mailbox and the author and transmitter are identical, the
"Sender:" field SHOULD NOT be used. Otherwise, both fields SHOULD
appear.
Does it mean that I should combine these two headers? (From and Sender). I'm a bit confused, because I noticed a lot of emails in my gmail (looking through "Show original") where in From field name and email are presented.
Thanks for help.
You need to encode the name part separately using email.header.Header:
from email.MIMEMultipart import MIMEMultipart
from email.header import Header
from email.utils import formataddr
author = formataddr((str(Header(u'Alał', 'utf-8')), "somemail#somedomain.com"))
msg = MIMEMultipart('alternative')
msg['From'] = author
print msg
I hope this will help.
I am using Python module MimeWriter to construct a message and smtplib to send a mail constructed message is:
file msg.txt:
-----------------------
Content-Type: multipart/mixed;
from: me<me#abc.com>
to: me#abc.com
subject: 主題
Content-Type: text/plain;charset=utf-8
主題
I use the code below to send a mail:
import smtplib
s=smtplib.SMTP('smtp.abc.com')
toList = ['me#abc.com']
f=open('msg.txt') #above msg in msg.txt file
msg=f.read()
f.close()
s.sendmail('me#abc.com',toList,msg)
I get mail body correctly but subject is not proper,
subject: some junk characters
主題 <- body is correct.
Please suggest? Is there any way to specify the decoding to be used for the subject also,
as being specified for the body. How can I get the subject decoded correctly?
From http://docs.python.org/library/email.header.html
from email.message import Message
from email.header import Header
msg = Message()
msg['Subject'] = Header('主題', 'utf-8')
print msg.as_string()
Subject: =?utf-8?b?5Li76aGM?=
more simple:
from email.header import Header
print Header('主題', 'utf-8').encode()
=?utf-8?b?5Li76aGM?=
as complement decode may made with:
from email.header import decode_header
a = decode_header("""=?utf-8?b?5Li76aGM?=""")[0]
print(a[0].decode(a[1]))
Reference:
Python - email header decoding UTF-8
The subject is transmitted as an SMTP header, and they are required to be ASCII-only. To support encodings in the subject you need to prefix the subject with whatever encoding you want to use. In your case, I would suggest prefix the subject with ?UTF-8?B? which means UTF-8, Base64 encoded.
In other words, I believe your subject header should more or less look like this:
Subject: =?UTF-8?B?JiMyMDAyNzsmIzM4OTg4Ow=?=
In PHP you could go about it like this:
// Convert subject to base64
$subject_base64 = base64_encode($subject);
fwrite($smtp, "Subject: =?UTF-8?B?{$subject_base64}?=\r\n");
In Python:
import base64
subject_base64 = base64.encodestring(subject).strip()
subject_line = "Subject: =?UTF-8?B?%s?=" % subject_base64
In short, if you use the EmailMessage API, you should code like this:
from email.message import EmailMessage
from email.header import Header
msg = EmailMessage()
msg['Subject'] = Header('主題', 'utf-8').encode()
Answer from #Sérgio cannot be used in the EmailMessage API, cause only string object can be assigned to EmailMessage()["Subject"], but not an email.header.Header object.
I'm having a problem emailing unicode characters using smtplib in Python 3. This fails in 3.1.1, but works in 2.5.4:
import smtplib
from email.mime.text import MIMEText
sender = to = 'ABC#DEF.com'
server = 'smtp.DEF.com'
msg = MIMEText('€10')
msg['Subject'] = 'Hello'
msg['From'] = sender
msg['To'] = to
s = smtplib.SMTP(server)
s.sendmail(sender, [to], msg.as_string())
s.quit()
I tried an example from the docs, which also failed. http://docs.python.org/3.1/library/email-examples.html, the Send the contents of a directory as a MIME message example
Any suggestions?
The key is in the docs:
class email.mime.text.MIMEText(_text, _subtype='plain', _charset='us-ascii')
A subclass of MIMENonMultipart, the
MIMEText class is used to create MIME
objects of major type text. _text is
the string for the payload. _subtype
is the minor type and defaults to
plain. _charset is the character set
of the text and is passed as a
parameter to the MIMENonMultipart
constructor; it defaults to us-ascii.
No guessing or encoding is performed
on the text data.
So what you need is clearly, not msg = MIMEText('€10'), but rather:
msg = MIMEText('€10'.encode('utf-8'), _charset='utf-8')
While not all that clearly documented, sendmail needs a byte-string, not a Unicode one (that's what the SMTP protocol specifies); look to what msg.as_string() looks like for each of the two ways of building it -- given the "no guessing or encoding", your way still has that euro character in there (and no way for sendmail to turn it into a bytestring), mine doesn't (and utf-8 is clearly specified throughout).
_charset parameter of MIMEText defaults to us-ascii according to the docs. Since € is not from us-ascii set it isn't working.
example in the docs that you've tried clearly states:
For this example, assume that the text file contains only ASCII characters.
You could use .get_charset method on your message to investigate the charset, there is incidentally .set_charset as well.