I don't understand why the body of my email I send via Python and the email library isn't being decoded by the receiving mail server. I know I've done something wrong, but don't know what. CTE is Quoted-Printable, and when viewing the raw email, it's still encoded.
I'm burning the net up, but most of my results deal with the legacy API of the email library. I use the current version, because it's recommended and more robust.
Code to package EmailMessage:
"""
#type sndr: str
#type rcvr: str
#type subj: str
#type body: tuple
#type extra: tuple
#return: bytes
"""
msg = message.EmailMessage()
if '<' in sndr:
name, sep, addr = sndr.rpartition(' ') # Partition at space between display name & address
name = name.strip() # Remove leading and trailing spaces
addr = addr.strip('<>') # Remove leading and trailing angle brackets
msg['From'] = headerregistry.Address(display_name=name, addr_spec=addr)
else:
msg['From'] = headerregistry.Address(display_name='', addr_spec=sndr)
if '<' in rcvr:
name, sep, addr = rcvr.rpartition(' ')
name = name.strip()
addr = addr.strip('<>')
msg['To'] = headerregistry.Address(display_name=name, addr_spec=addr)
else:
msg['To'] = headerregistry.Address(display_name='', addr_spec=rcvr)
msg['Subject'] = subj
msg.set_content(body[0], charset='utf-8')
msg.add_alternative(body[1], cte='quoted-printable', subtype='html')
return msg
HTML body:
"""
#type body: str
#return: str
"""
first = \
'<html><head>\r' \
'<meta http–equiv="Content-Type" content="text/html; charset=utf-8">\r' \
'</head>'
second = f'<body><div>{body}</div></body></html>'
payload = first + second
return payload
By using the argument subtype='html', Python creates the required meta tags. These tags display correctly in the raw email, yet my tags (old/troubleshooting code, no need to focus on it—the problem remains) do not. The en dash between "http" and "equiv" displays encoded. What's wrong. I want to send email using the full utf-8 range.
Also, I use the following from smtplib to send the message: send_message(msg). So, the default call of send_message().
Related
I have a python list, this list is of any document that has been updated within a set timeframe and the documents are ID'd via a set value in the list. There may be one or many in the resulting list. What I am trying to figure out is how I can loop through the list of values (documents) and trigger another loop that iterates through another list I have of emails resulting in one email per address per document in the original list? I tried to "stack" loops on top of each other (code snippet shown below), but this results in multiple emails to each email address all with a full list of documents (i.e. if there are two documents in the list, two emails are sent to each address with the details about both documents).
import boto3
from botocore.exceptions import ClientError
import requests
from datetime import datetime, timedelta
#first get request to pull all current endusers
GET1 = "https://abdc.com/api"
r1 = requests.get(url = GET1, auth=('username/token','APItoken'))
#convert to python dict
data = r1.json()
#create a list of all user's email addresses
emails = [user["email"] for user in data["users"]]
#create an timestamp of previous day and convert to epoch
ts1 = datetime.today() - timedelta(days =1)
ts2 = ts1.strftime("%s")
#set start time attribute as a parameter
params = {'start_time':ts2}
#second GET reqeust to pull all articles updated in the last 24 hrs
GET2 = "https://efdg.com/api"
r2 = requests.get(url = GET2, params=params, auth=('username/token','APItoken'))
#convert to python dict
data2 = r2.json()
#create list of all the target article titles and html url
updated_docs = [articles["html_url"] for articles in data2["articles"]]
doc_title = [articles["title"] for articles in data2["articles"]]
for y in updated_doc:
#create loop to iterate throuhg all the email addresses and send
individual emails to users
for x in emails:
# This address must be verified with Amazon SES.
SENDER = "example#example.com"
#To list
RECIPIENT = x
# Specify a configuration set. If you do not want to use a configuration
# set, comment the following variable, and the
# ConfigurationSetName=CONFIGURATION_SET argument below.
#CONFIGURATION_SET = "ConfigSet"
# If necessary, replace us-west-2 with the AWS Region you're using for Amazon SES.
AWS_REGION = "us-east-1"
# The subject line for the email.
SUBJECT = "blah blah "
# The email body for recipients with non-HTML email clients.
BODY_TEXT = ("Amazon SES Test (Python)\r\n"
"This email was sent with Amazon SES using the "
"AWS SDK for Python (Boto)."
)
# The HTML body of the email.
BODY_HTML = """<html>
<head></head>
<body>
<h1>l Documentation Notification</h1>
<p1>Please click the link below for the most current version of this document.<br>
<br>
"""+str(doc_title)+"""<br>
<br>
"""+str(updated_docs)+"""
</p1>
</body>
</html>
"""
# The character encoding for the email.
CHARSET = "UTF-8"
# Create a new SES resource and specify a region.
client = boto3.client('ses', region_name=AWS_REGION)
# Try to send the email.
try:
# Provide the contents of the email.
response = client.send_email(
Destination={
'ToAddresses': [
RECIPIENT,
],
},
Message={
'Body': {
'Html': {
'Charset': CHARSET,
'Data': BODY_HTML,
},
'Text': {
'Charset': CHARSET,
'Data': BODY_TEXT,
},
},
'Subject': {
'Charset': CHARSET,
'Data': SUBJECT,
},
},
Source=SENDER,
# If you are not using a configuration set, comment or delete the
# following line
#ConfigurationSetName=CONFIGURATION_SET,
)
# Display an error if something goes wrong.
except ClientError as e:
print(e.response['Error']['Message'])
else:
print("Email sent! Message ID:"),
print(response['MessageId'])
So, you've completely changed your code, so I'm well confused, but if I can read between the lines properly you meant something like this:
#create loop to iterate through all documents in the list
for article in data2["articles"]:
#create loop to iterate through all the email addresses and send individual emails to
users
for x in emails:
# This address must be verified with Amazon SES.
SENDER = "example#example.com"
#To list
RECIPIENT = x
# The HTML body of the email.
BODY_HTML = """<html>
<head></head>
<body>
<h1>l Documentation Notification</h1>
<p1>Please click the link below for the most current version of this document.<br>
<br>
"""+str(article["title"])+"""<br>
<br>
"""+str(article["html_url"])+"""
</p1>
</body>
</html>
"""
Notice how I iterate through data2["articles"] and update the BODY_HTML with article["title"] and article["html_url"],
I get this error when sending emails via smptplib in Python to a certain list
SMTPRecipientsRefused: {'': (421, b'4.7.0 Too many protocol errors (6) on this connection, closing transmission channel.')}?
I'm using Office365 SMTP details and a snippet of the code is below:-
import smtplib, ssl
from email.message import EmailMessage
import getpass
ids = df['IDs']
emails_to = df['Emails']
namesofcompanies = df["CompanyNames"]
sendfrom = df["SenderList"]
date_7days = (datetime.now() + timedelta(days=7)).strftime('%d/%m/%Y')
date_14days = (datetime.now() + timedelta(days=13)).strftime('%d/%m/%Y')
email_pass = input() #Office 365 password
context=ssl.create_default_context()
for i in range(len(emails_to)): # iterate through the records
# for every record get the name and the email addresses
ID = str(ids[i])
Emaitstosendto = emails_to[i]
companynames = namesofcompanies[i]
tosendfrom = sendfrom[i]
if my_files_dict.get(ID): #Looks for attachments in the same folder with same name as the corresponding record
smtp_ssl_host = 'smtp.office365.com'
smtp_ssl_port = 587
email_login = "xxx#xxx.com" #Office 365 email
email_from = tosendfrom
email_to = Emaitstosendto
msg = MIMEMultipart()
msg['Subject'] = "Received Emails between "+date_7days+" - "+date_14days
msg['From'] = email_from
msg['To'] = email_to
msg['X-Priority'] = '2'
text = ("XXXX,\n"
f"xxxxxx\n\n")
msg.attach(MIMEText(text))
filename = my_files_dict.get(ID)#Files in the folder matching the ID
fo = open(filename,'rb')
s2 = smtplib.SMTP(smtp_ssl_host, smtp_ssl_port)
s2.starttls(context=context)
s2.login(email_login, email_pass)
attachment = email.mime.application.MIMEApplication(fo.read(),_subtype="xlsx")
fo.close()
attachment.add_header('Content-Disposition','attachment',filename=filename)
msg.attach(attachment)
s2.send_message(msg)
s2.quit()
On average, I will be sending emails to a list separated by a semi-colon(;) of about 8 emails per record. This means that for each attachment, I'll send to about 8 emails and I'm to do it for about 70 of such contacts. In total, that will be about 560 emails. Nothing gets sent out I get the above error the moment I log in. On the contrary, when I try test sending it to a list of 3 emails in the test emails column, the same emails go out very well. Can anyone point to where I may not be getting it right? I suspect either the list of emails is too long or is something with the email addresses thus the protocol error? Is this an SMTPlib limitation?
What you specify in MIMEMultipart() is what appears in the message header, but this is not always equal to the list of recipients. You can try to change server.send_message(msg) to server.sendmail(sender,recipients,msg.as_string())
Keep in mind that sendmail() requires a list of recipients, whereas msg['To'] should be set as one string, so if your variable email_to is comma separated, you should write it like:
s2.sendmail(email_from,email_to.split(','),msg.as_string())
More details about sendmail() can be found here.
I resolved this error by creating a list and joined all addresses from a tuple into a string, using a comma character as separator.
family = the list of all recipients' email addresses
family = [
'name1#example.com',
'name2#example.com',
'name3#example.com',
'name4#example.com',
'name5#example.com',
'name6#example.com',
'name7#example.com',
'name8#example.com'
]
msg['To'] =', '.join(family)
I am using python standard email parsing library to parse the raw email that I am getting from amazon ses mail service.
Below is my code for the same.
import json
import email
from email.Utils import parseaddr
def parse(raw_email):
message = email.message_from_string(raw_email)
text_plain = None
text_html = None
for part in message.walk():
if part.get_content_type() == 'text/plain' and text_plain is None:
text_plain = part.get_payload()
if part.get_content_type() == 'text/html' and text_html is None:
text_html = part.get_payload()
parsed_email_object = {
'to': parseaddr(message.get('To'))[1],
'from': parseaddr(message.get('From'))[1],
'delivered to': parseaddr(message.get('Delivered-To'))[1],
'subject': message.get('Subject'),
'text_plain': text_plain,
'text_html': text_html,
}
json_string = json.dumps(parsed_email_object)
return json_string
when I am parsing my raw email, it is not parsing 100%, it is giving me unwanted characters like this
this is a replyo from the gmail indbo asdf asdf asdfa sdfa=
sd sdfa sdfa fasd
=C2=A0dfa sf asdf
a sdfas
<= div>f asdf=C2=A0
Is there anything else like some decoding option to parse it correctly.
Making my comment as an answer so that it gets noticed.
part.get_payload(decode=True).decode(part.get_content_charset())
This will solve the issue of encoding
so I'm working on something that uses regex to search something from an email, which is fetched via imaplib module. Right now I can't get it to work, even after using str() function.
result, data = mail.fetch(x, '(RFC822)')
eemail = email.message_from_bytes(data[0][1])
print(str(eemail))
trying to regex it:
print(re.search("button", eemail))
Regex gives me no matches even after making the email a string object.
This is what I use:
import imaplib
import email
import re
mail = imaplib.IMAP4_SSL(SMTP_SERVER, SMTP_PORT)
mail.login(FROM_EMAIL,FROM_PWD)
mail.select('inbox')
status, response = mail.search(None, '(UNSEEN)')
unread_msg_nums = response[0].split()
for e_id in unread_msg_nums:
_, response = mail.fetch(e_id, '(UID BODY[TEXT])')
b = email.message_from_string(response[0][1])
if b.is_multipart():
for payload in b.get_payload(decode=True):
print(re.search("button", payload.get_payload(decode=True)))
else:
print(re.search("button", b.get_payload(decode=True)))
My SendMail function looks like this:
MY_SMTP = #I can't show it here
def SendMail(authentication_data, email_from, email_to_list, email_cc_list, email_subject, email_message, list_file)
user_name = authentication_data["name"]
user_password = authentication_data["password"]
msg = MIMEMultipart(email_message)
server = smtplib.SMTP(MY_SMTP)
server.starttls()
emails_list_string = ", ".join(email_to_list)
emails_cc_list_string = ", ".join(email_cc_list)
msg["From"] = email_from
msg["To"] = emails_list_string
msg["Cc"] = emails_cc_list_string
msg["Subject"] = "TEST"
msg["Date"] = email.Utils.formatdate()
msg.attach(MIMEText(email_message))
server.login(user_name, user_password)
server.sendmail(email_from, emails_list_string + ", " + emails_cc_list_string, msg.as_string())
server.quit()
When I use this function and I pass a list of e-mail addresses for email_to_list, it only sends an e-mail to the first element of the list. Passing any list for email_cc_list simply adds addresses to the Cc field in the e-mail, however I think it's purely cosmetic, as the people in the Cc don't see the e-mail when I send it with this function. Basically only the first address in email_to_list ever receives the e-mail.
The documentation says:
The required arguments are an RFC 822 from-address string, a list of RFC 822 to-address strings (a bare string will be treated as a list with 1 address), and a message string.
When you pass emails_list_string + ", " + emails_cc_list_string to server.sendmail as the second parameter, it treats it as a single address, probably chopping off everything after the first comma. Try like this instead:
server.sendmail(email_from, email_to_list + email_cc_list, msg.as_string())