BACKGROUND
Regarding the following articles:
https://www.drupal.org/project/mimemail/issues/31524
Exclamation Point Randomly In Result of PHP HTML-Email
https://sourceforge.net/p/phpmailer/bugs/53/
All the problems and solutions refer to PHP issue, but I have run into this problem in Python.
If I send the emails directly to recipients, all is well, no exclamation marks appear, and the message displays properly.
However, utilizing our "Sympa" (https://www.sympa.org/) system that the University uses for it "mailing list" solution, emails from this system have the exclamation marks and line breaks inserted in the message and HTML breaks causing display issues.
The problem stems from line length. Any line longer than a magical 998 character length line gets this exclamation marks and line breaks inserted.
NOW THE QUESTION
One of the solutions they mention is encoding the body of a message in base64, which apparently is immune to the line length issue. However, I can not figure out how to properly form a message in Python and have the proper headers and encoding happen so the message will display properly in an email client.
Right now, I have only succeed in sending emails with base64 encode bodies as attached files. Bleck!
I need to send HTML encoded emails (tables and some formatting). I create one very long concatenated string of all the html squished together. It is ugly but will display properly.
HELP?!
NOTE: If anyone else has had this problem and has a solution that will allow me to send emails that are not plagued by line length issue, I am all ears!
Source Code as Requested
# Add support for emailing reports
import smtplib
# from email.mime.text import MIMEText
from email.mime.message import MIMEMessage
from email.encoders import encode_base64
from email.message import Message
... ...
headerData = {"rDate": datetime.datetime.now().strftime('%Y-%m-%d')}
msg_body = msg_header.format(**headerData) + contact_table + spacer + svc_table
theMsg = Message()
theMsg.set_payload(msg_body)
encode_base64(theMsg)
theMsg.add_header('Content-Transfer-Encoding', 'base64')
envelope = MIMEMessage(theMsg, 'html')
theSubject = "Audit for: "+aService['description']
envelope['Subject'] = theSubject
from_addr = "xxx#xxx"
envelope['From'] = from_addr
to_addrs = "xxx#xxxx"
# to_addrs = aService['contact_email']
envelope['To'] = to_addrs
# Send the message via our own SMTP server.
s = smtplib.SMTP('x.x.x.x')
s.sendmail(from_addr, to_addrs, envelope.as_string())
s.quit()
SOLUTION, thank you #Serge Ballesta
Going back to MIMEText, and specifying a character set seemed to do the trick:
envelope = MIMEText(msg_body, 'html', _charset='utf-8')
assert envelope['Content-Transfer-Encoding'] == 'base64'
envelope['Subject'] = "Audit for: "+aService['description']
from_addr = "f5-admins#utlists.utexas.edu"
envelope['From'] = from_addr
to_addrs = "xx-test#xx.xx.edu"
envelope['To'] = to_addrs
# Send the message via our own SMTP server.
s = smtplib.SMTP('xx.xx.xx.edu')
s.sendmail(from_addr, to_addrs, envelope.as_string())
s.quit()
Apparently I was just stabbing around and did not account for character set. Using MIMEText and not MIMEMessage.
Normally, email.mime.MIMEText automatically sets the Content-Transfert-Encoding to base64 if the body is not declared to be plain ASCII. So, assuming that body contains the HTML text of the body of the message (no mail headers there), declaring it as utf-8 should be enough:
msg = email.mime.text.MIMEText(body, 'html', _charset='utf-8')
# assert the cte:
assert msg['Content-Transfer-Encoding'] == 'base64'
theSubject = "Audit for: "+aService['description']
msg['Subject'] = theSubject
from_addr = "xxx#xxx"
msg['From'] = from_addr
to_addrs = "xxx#xxxx"
# to_addrs = aService['contact_email']
msg['To'] = to_addrs
# optionally set other headers
# msg['Date']=datetime.datetime.now().isoformat()
# Send the message
s = smtplib.SMTP('x.x.x.x')
s.sendmail(from_addr, to_addrs, msg.as_bytes())
s.quit()
Related
This question already has answers here:
smtplib sends blank message if the message contain certain characters
(3 answers)
Closed 2 years ago.
Before encoding the msg variable, I was getting this error:
UnicodeEncodeError: 'ascii' codec can't encode character '\xfc' in
position 4: ordinal not in range(128)
So I did some research, and finally encoded the variable:
msg = (os.path.splitext(base)[0] + ': ' + text).encode('utf-8')
server.sendmail('...#gmail.com', '...#gmail.com', msg)
Here's the rest of the code on request:
def remind_me(path, time, day_freq):
for filename in glob.glob(os.path.join(path, '*.docx')):
# file_count = sum(len(files))
# random_file = random.randint(0, file_number-1)
doc = docx.Document(filename)
p_number = len(doc.paragraphs)
text = ''
while text == '':
rp = random.randint(0, p_number-1) # random paragraph number
text = doc.paragraphs[rp].text # gives the entire text in the paragraph
base = os.path.basename(filename)
print(os.path.splitext(base)[0] + ': ' + text)
server = smtplib.SMTP('smtp.gmail.com', 587)
server.starttls()
server.login('...#gmail.com', 'password')
msg = (os.path.splitext(base)[0] + ': ' + text).encode('utf-8')
server.sendmail('...#gmail.com', '...#gmail.com', msg)
server.quit()
Now, it sends empty emails instead of delivering the message. Does it return None? If so, why?
Note: Word documents contain some characters like ş, ö, ğ, ç.
The msg argument to smtplib.sendmail should be a bytes sequence containing a valid RFC5322 message. Taking a string and encoding it as UTF-8 is very unlikely to produce one (if it's already ASCII, encoding it does nothing useful; and if it isn't, you are most probably Doing It Wrong).
To explain why that is unlikely to work, let me provide a bit of background. The way to transport non-ASCII strings in MIME messages depends on the context of the string in the message structure. Here is a simple message with the word "Hëlló" embedded in three different contexts which require different encodings, none of which accept raw UTF-8 easily.
From: me <sender#example.org>
To: you <recipient#example.net>
Subject: =?utf-8?Q?H=C3=ABll=C3=B3?= (RFC2047 encoding)
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="fooo"
--fooo
Content-type: text/plain; charset="utf-8"
Content-transfer-encoding: quoted-printable
H=C3=ABll=C3=B3 is bare quoted-printable (RFC2045),
like what you see in the Subject header but without
the RFC2047 wrapping.
--fooo
Content-type: application/octet-stream; filename*=UTF-8''H%C3%ABll%C3%B3
This is a file whose name has been RFC2231-encoded.
--fooo--
There are recent extensions which allow for parts of messages between conforming systems to contain bare UTF-8 (even in the headers!) but I have a strong suspicion that this is not the scenario you are in. Maybe tangentially see also https://en.wikipedia.org/wiki/Unicode_and_email
Returning to your code, I suppose it could work if base is coincidentally also the name of a header you want to add to the start of the message, and text contains a string with the rest of the message. You are not showing enough of your code to reason intelligently about this, but it seems highly unlikely. And if text already contains a valid MIME message, encoding it as UTF-8 should not be necessary or useful (but it clearly doesn't, as you get the encoding error).
Let's suppose base contains Subject and text is defined thusly:
text='''=?utf-8?B?H=C3=ABll=C3=B3?= (RFC2047 encoding)
MIME-Version: 1.0
Content-type: multipart/mixed; boundary="fooo"
....'''
Now, the concatenation base + ': ' + text actually produces a message similar to the one above (though I reordered some headers to put Subject: first for this scenario) but again, I imagine this is not how things actually are in your code.
If your goal is to send an extracted piece of text as the body of an email message, the way to do that is roughly
from email.message import EmailMessage
body_text = os.path.splitext(base)[0] + ': ' + text
message = EmailMessage()
message.set_content(body_text)
message["subject"] = "Extracted text"
message["from"] = "you#example.net"
message["to"] = "me#example.org"
with smtplib.SMTP("smtp.gmail.com", 587) as server:
# ... smtplib setup, login, authenticate?
server.send_message(message)
This answer was updated for the current email library API; the text below the line is the earlier code from the original answer.
The modern Python 3.3+ EmailMessage API rather straightforwardly translates into human concepts, unlike the older API which required you to understand many nitty-gritty details of how the MIME structure of your message should look.
from email.mime.text import MIMEText
body_text = os.path.splitext(base)[0] + ": " + text
sender = "you#example.net"
recipient = "me#example.org"
message = MIMEText(body_text)
message["subject"] = "Extracted text"
message["from"] = sender
message["to"] = recipient
server = smtplib.SMTP("smtp.gmail.com", 587)
# ... smtplib setup, login, authenticate?
server.sendmail(from, to, message.as_string())
The MIMEText() invocation builds an email object with room for a sender, a subject, a list of recipients, and a body; its as_text() method returns a representation which looks roughly similar to the ad hoc example message above (though simpler still, with no multipart structure) which is suitable for transmitting over SMTP. It transparently takes care of putting in the correct character set and applying suitable content-transfer encodings for non-ASCII header elements and body parts (payloads).
Python's standard library contains fairly low-level functions so you have to know a fair bit in order to connect all the pieces correctly. There are third-party libraries which hide some of this nitty-gritty; but you would exepect anything with email to have at the very least both a subject and a body, as well as of course a sender and recipients.
I have written a script that writes a message to a text file and also sends it as an email.
Everything goes well, except the email finally appears to be all in one line.
I add line breaks by \n and it works for the text file but not for the email.
Do you know what could be the possible reason?
Here's my code:
import smtplib, sys
import traceback
def send_error(sender, recipient, headers, body):
SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 587
session = smtplib.SMTP('smtp.gmail.com', 587)
session.ehlo()
session.starttls()
session.ehlo
session.login(sender, 'my password')
send_it = session.sendmail(sender, recipient, headers + "\r\n\r\n" + body)
session.quit()
return send_it
SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 587
sender = 'sender_id#gmail.com'
recipient = 'recipient_id#yahoo.com'
subject = 'report'
body = "Dear Student, \n Please send your report\n Thank you for your attention"
open('student.txt', 'w').write(body)
headers = ["From: " + sender,
"Subject: " + subject,
"To: " + recipient,
"MIME-Version: 1.0",
"Content-Type: text/html"]
headers = "\r\n".join(headers)
send_error(sender, recipient, headers, body)
Unfortunately for us all, not every type of program or application uses the same standardization that python does.
Looking at your question i notice your header is: "Content-Type: text/html"
Which means you need to use HTML style tags for your new-lines, these are called line-breaks. <br>
Your text should be:
"Dear Student, <br> Please send your report<br> Thank you for your attention"
If you would rather use character type new-lines, you must change the header to read: "Content-Type: text/plain"
You would still have to change the new-line character from a single \n to the double \r\n which is used in email.
Your text would be:
"Dear Student, \r\n Please send your report\r\n Thank you for your attention"
You have your message body declared to have HTML content ("Content-Type: text/html"). The HTML code for line break is <br>. You should either change your content type to text/plain or use the HTML markup for line breaks instead of plain \n as the latter gets ignored when rendering a HTML document.
As a side note, also have a look at the email package. There are some classes that can simplify the definition of E-Mail messages for you (with examples).
For example you could try (untested):
import smtplib
from email.mime.text import MIMEText
# define content
recipients = ["recipient_id#yahoo.com"]
sender = "sender_id#gmail.com"
subject = "report reminder"
body = """
Dear Student,
Please send your report
Thank you for your attention
"""
# make up message
msg = MIMEText(body)
msg['Subject'] = subject
msg['From'] = sender
msg['To'] = ", ".join(recipients)
# sending
session = smtplib.SMTP('smtp.gmail.com', 587)
session.starttls()
session.login(sender, 'my password')
send_it = session.sendmail(sender, recipients, msg.as_string())
session.quit()
In my case '\r\n' didn't work, but '\r\r\n' did. So my code was:
from email.mime.text import MIMEText
body = 'Dear Student,\r\r\nPlease send your report\r\r\nThank you for your attention'
msg.attach(MIMEText(body, 'plain'))
The message is written in multiple lines and is displayed correctly in Outlook.
I've also run into this as well. I had found a little bit of white space at the end of the line was enough for my SMTP service to recognize the new line
body = 'value of variable x = ' + myVarX + " \r\n" \
+ 'value of variable y = ' + myVarY
I believe this to be more of a SMTP issue, rather than a Python issue, which may explain the range in solutions in this thread
Setting the content-type header to Content-Type: text/plain (with \r\n at the end) allowed me to send multi-line plain-text emails.
Outlook will remove line feeds from plain text it believes are extras. https://support.microsoft.com/en-us/kb/287816
You can try below update to make the lines look like bullets. That worked for me.
body = "Dear Student, \n- Please send your report\n- Thank you for your attention"
I ran into this issue as well and this thread was helpful in determining why it was happening in the first place. I was at a lost, because I knew my code was correct. I tried a few things, but what worked for me was adding a \t\n for each line in the body.
from email.mime.text import MIMEText
lines = ['line1', 'line2', 'line3']
body = '\t\n'.join(lines)
msg = MIMEText(body)
please check the version of python.
for version 3.11.1,
you should use '\r\n' for line break
reference the official doc https://docs.python.org/3/library/smtplib.html#:~:text=using%20BytesGenerator%20with-,%5Cr%5Cn%20as%20the%20linesep,-%2C%20and%20calls%20sendmail
Adding \n\n works for me
body = """"""
body += "This is message1\n\n"
body += "This is message2"
Output:
This is message1
This is message2
I currently trying to send e-mails from a python script to a set of recipients that a got from a database (name + email address) and I want so send the mails such that their name is also included in the message header. But if the name includes an umlaut (äöü...) my script fails with (in case there is an ü inside the name):
UnicodeEncodeError: 'ascii' codec can't encode character '\xfc' in position 5: ordinal not in range(128)
But in the content of the message all umlauts are correctly replaced with for example =C3=BC for the ü.
Here is the example code I used:
import smtplib
from email.message import EmailMessage
from email.headerregistry import Address
from email.utils import make_msgid
msg = EmailMessage()
msg["Subject"] = "Python Test"
msg["From"] = Address("Max Müller" , addr_spec="maxmueller#localhost.localnet")
msg["To"] = Address("Max Möller", addr_spec="moeller#localhost.localnet")
msg.set_content("""\
This is a test with umlauts. öäü
--- Max Müller """)
server = smtplib.SMTP('localhost', 25)
server.send_message(msg)
server.quit()
I tried it with Python 3.5.2.
If replace
msg["From"] = Address("Max Müller" , addr_spec="maxmueller#localhost.localnet")
by
msg["From"] = Address("Max Müller".encode("utf-8") , addr_spec="maxmueller#localhost.localnet")
I got in my mail client as senders name b'Max M\xc3\xbcller' which is useless to the most e-mail clients.
So how do I get the umlauts in the recipient's and the sender's encoded as in the mail's content?
Almost all was fine, but the send_message method is not really non-ASCII friendly, so you must use the good old sendmail. The problem is that send_message tries to use the content of the To and From headers to build the envelope addresses but unfortunately it is broken for addresses containing non ASCII characters even if they are in the name part.
So if you have only one recipient address, you code should simply be:
...
server.sendmail(msg["From"], msg["To"], msg.as_string())
If you want to be able to process multiple recipient addresses, you must add an explicit processing:
def envelopeAddr(header):
return [a.addr_spec for a in header.addresses]
The sendmail command becomes:
server.sendmail(msg["From"], envelopeAddr(msg["To"]), msg.as_string())
The good new is that it also works for a single dest address.
def dd_b64(s):
s = '=?utf-8?b?' + base64.b64encode(s.encode('UTF-8')).decode() + '?='
s = '=?utf-8?b?' + base64.b64encode(s.encode('UTF-8')).decode() + '?='
return s
msg = EmailMessage()
#msg['From'] = Address(dd_b64(发送者昵称), *发送者账号.rsplit('#', 1))
msg['From'] = Address(dd_b64(发送者昵称), addr_spec = 发送者账号) #省略addr_spec会出现 由 public#zxxxn.com 代发
msg['To'] = Group(None, [Address(dd_b64(name), to_addrs)])
msg['Subject'] = '63中文'
msg['Date'] = localtime()
===========================================================================
double encode b64 to "From,To,and add_attachment's filename"
msg["From"] = Address(dd_b64("Max Müller") , addr_spec="maxmueller#localhost.localnet")
msg["To"] also dd_b64.
msg.add_attachment(fi.read(), *ctype.split('/', 1), filename = dd_b64('工作表.xlsx'))
Here is my code:
FROM = ''
TO = ''
SMTPSERVER = ''
MYEMAILPASSWORD = ""
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.mime.base import MIMEBase
from email import encoders
def physicallySend(listOfAttachments):
msg = MIMEMultipart('alternative')
msg['Subject'] = "Testing"
msg['From'] = FROM
msg['To'] = TO
textPart = MIMEText("Hello. I should be able to see this body.", 'plain')
msg.attach(textPart)
for attachmentFilename in listOfAttachments:
fp = open(attachmentFilename, 'rb')
file1 = MIMEBase('application','csv')
file1.set_payload(fp.read())
fp.close()
encoders.encode_base64(file1)
file1.add_header('Content-Disposition','attachment;filename=%s' % attachmentFilename)
msg.attach(file1)
server = smtplib.SMTP(SMTPSERVER)
server.ehlo()
server.starttls()
server.login(FROM, MYEMAILPASSWORD)
server.sendmail(FROM, to, msg.as_string())
server.quit()
return True
physicallySend(["testfile.csv"])
While I can see the text body fine on Gmail and Outlook, however on my iPhone (6.1.3) I only see the attachment, and not the body.
I found my solution in this comment: How do I send attachments using SMTP?
My first line should have been
msg = MIMEMultipart('mixed')
rather than 'alternative'.
For ones who seek the full solution to the question, that will work as intended on all email clients including iOS default email app.
When to use multipart/mixed
from the RFC
The primary subtype for multipart, "mixed", is intended for use when the body parts are independent and intended to be displayed serially. Any multipart subtypes that an implementation does not recognize should be treated as being of subtype "mixed".
so multipart/mixed type should be used when all of the parts of the message (text/html, text/plain, image/png etc.) are equally important and all should be displayed to the user
this means that if you attach text/html to the message and also text/plain as a fallback to html, both of them will be shown to the user one over the other which doesn't make any sense
When to use multipart/alternative
from the RFC
In particular, each of the parts is an "alternative" version of the same information. User agents should recognize that the content of the various parts are interchangeable. The user agent should either choose the "best" type based on the user's environment and preferences, or offer the user the available alternatives. In general, choosing the best type means displaying only the LAST part that can be displayed. This may be used, for example, to send mail in a fancy text format in such a way that it can easily be displayed anywhere
This means that whatever you attach to the multipart/alternative message can be treated as the same value but in different forms.
Why is that important?
if after the textual parts, you attach an image (image/png, image/jpeg) to the multipart/alternative message it can be treated as equally important as the message itself thus the only thing that the user will see is the image - without the text/html nor text/plain parts. This is not true for most of nowadays clients - they are smart enough to know what you mean, but the one that still does this is iOS default Mail app - you don't want to confuse those users.
Whats the correct way to do it then?
After the explanation this will make much more sense now
# keeps the textual and attachment parts separately
root_msg = MIMEMultipart('mixed')
root_msg['Subject'] = email.subject
root_msg['From'] = self._format_from_header()
root_msg['To'] = self._format_addrs(recipients=email.recipients)
alter_msg = MIMEMultipart('alternative')
plain_text = MIMEText('some text', 'plain', 'UTF-8')
html = MIMEText('<strong>some text</strong>', 'html')
alter_msg.attach(plain_text)
alter_msg.attach(html)
# now the alternative message (textual)
# is just a part of the root mixed message
root_msg.attach(alter_msg)
img_path = '...'
filename = os.path.basename(img_path)
with open(img_path, 'rb') as f:
attachment = MIMEImage(f.read(), filename=filename)
attachment.add_header('Content-Disposition', 'attachment', filename=filename)
# now the images (attachments) will be shown alongside
# either plain or html message and not instead of them
root_msg.attach(attachment)
I tried hard to find solution to this issues but all in vein, finally i have to ask you guys. I have HTML email (using Python's smtplib). Here is the code
Message = """From: abc#abc.com>
To: abc#abc.com>
MIME-Version: 1.0
Content-type: text/html
Subject: test
Hello,
Following is the message
""" + '\n'.join(mail_body) + """
Thank you.
"""
In above code, mail_body is a list which contains lines of output from a process. Now what i want is, to display these lines (line by line) in HTML email. What is happening now its just appending line after line. i.e.
I am storing the output(of process) like this :
for line in cmd.stdout.readline()
mail_body.append()
Current Output in HTML email is:
Hello,
abc
Thank you.
What i want :
Hello,
a
b
c
Thank you.
I just want to attach my process output in HTML email line by line. Can my output be achieved in any way?
Thanks and Regards
You could generate the email content to send using email package (from stdlib) e.g.:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from cgi import escape
from email.header import Header
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from smtplib import SMTP_SSL
login, password = 'me#example.com', 'my password'
# create message
msg = MIMEMultipart('alternative')
msg['Subject'] = Header('subject…', 'utf-8')
msg['From'] = login
msg['To'] = ', '.join([login,])
# Create the body of the message (a plain-text and an HTML version).
text = "Hello,\nFollowing is the message\n%(somelist)s\n\nThank you." % dict(
somelist='\n'.join(["- " + item for item in mail_body]))
html = """<!DOCTYPE html><title></title><p>Hello,<p>Following is the message
<ul>%(somelist)s</ul><p>Thank you. """ % dict(
somelist='\n'.join(["<li> " + escape(item) for item in mail_body]))
# According to RFC 2046, the last part of a multipart message, in this case
# the HTML message, is best and preferred.
msg.attach(MIMEText(text, 'plain', 'utf-8'))
msg.attach(MIMEText(html, 'html', 'utf-8'))
# send it
s = SMTP_SSL('smtp.mail.example.com', timeout=10) # no cert check on Python 2
s.set_debuglevel(0)
try:
s.login(login, password)
s.sendmail(msg['From'], msg['To'], msg.as_string())
finally:
s.quit()
in HTML, a new line is not \n it is <br> for "line break" but since you are also not using HTML tags in this email, you also need to know that in MIME messages, a newline is \r\n and not just \n
So you should write:
'\r\n'.join(mail_body)
For newlines that deal with the MIME message, but if you are going to use the HTML for formatting, then you need to know that <br> is the line break, and it would be:
'<br>'.join(mail_body)
To be comprehensive, you could try:
'\r\n<br>'.join(mail_body)
But I do now know what that would like like...