I have this issue where I need to periodically check a Gmail account and add subjects to emails with no subject. My current method is very hacky and I am hoping someone with more IMAP/Google API knowledge could suggest a better method.
Right now, my python script will check Gmail for messages and for emails with no subject, it will create a copy of the message and resend it (back to itself). Here is the code for that (authentication excluded:
# Select the inbox
mail.select()
# Get the UID of each email in the inbox with ""
subject typ, data = mail.uid('search', None, '(HEADER Subject "")')
# Loop for each of the emails with no subjects
for email_id in data[0].split():
print email_id
# Use the uid and get the email message
result, message = mail.uid('fetch', email_id, '(RFC822)')
raw_email = message[0][1]
email_msg = email.message_from_string(raw_email)
# Check that the subject is blank
if email_msg['Subject'] == '':
print email_msg['From']
print email_msg['To']
# Get the email payload
if email_msg.is_multipart():
payload = str(email_msg.get_payload()[0])
# Trim superfluous text from payload
payload = '\n'.join(payload.split('\n')[3:])
print payload
else:
payload = email_msg.get_payload()
# Create the new email message
msg = MIMEMultipart()
msg['From'] = email_msg['From']
msg['To'] = email_msg['To']
msg['Subject'] = 'No Subject Specified'
msg.attach(MIMEText(payload))
s = smtplib.SMTP('smtp.brandeis.edu')
print 'Sending Mail...'
print msg['To']
s.sendmail(email_msg['From'], [email_msg['To']], msg.as_string())
s.quit()
mail.close()
mail.logout()
I am hoping that there is a better way to do this with python's IMAP library or Google APIs that wouldn't involved resending the message.
There is no better way. Messages in IMAP are immutable — clients may cache them indefinitely. And some messages are signed too, quite a few these days, and your change likely breaks the signatures.
Related
I am trying to send an automated email to various users with the lake that they are subscribed to changing:
message = MIMEMultipart()
message['From'] = email # Sender
message['Subject'] = "Algae Bloom Alert\n" # Subject
for user in subscriber_dict:
sms_gateway = subscriber_dict[user]['email']
message['To'] = subscriber_dict[user]['email']
body = (
f"This is an algae alert for Lake {subscriber_dict[user]['lake']}.\n\n"
f"Sent at {sent_time_date} {sent_time_time}"
)
message.attach(MIMEText(body, 'plain')) # Attaching body to email
sms = message.as_string()
server.sendmail(email, sms_gateway, sms)
# Reset body here ideally
print(f"Email sent at {formatted_time}")
However, when the emails are sent, each successive email contains the contents from the emails prior to it. Does anyone know how I can reset the body so that I am not attaching to the previous bodies but rather writing new ones?
Don't reuse the same message. Create a new one inside the loop.
Your code seems to be written for Python 3.5 or earlier. The email library was overhauled in 3.6 and is now quite a bit more versatile and logical. Probably throw away what you have and start over with the examples from the email documentation.
from email.message import EmailMessage
...
for user in subscriber_dict:
message = EmailMessage()
message['From'] = email
message['Subject'] = "Algae Bloom Alert" # no \n
message['To'] = subscriber_dict[user]['email']
message.set_content(
f"This is an algae alert for Lake {subscriber_dict[user]['lake']}.\n\n"
f"Sent at {sent_time_date} {sent_time_time}"
)
server.send_message(message)
print(f"Email sent at {formatted_time}")
You might want to avoid calling your sender variable email to avoid shadowing the email library, although in this example it doesn't really matter.
With .set_content() you could actually reuse the same message over and over but there is no actual benefit from that, and the problems if you fail to replace some part of an earlier message by mistake could be ugly. (For example, a typo could cause you to send the same message to the same recipient as many times as you have users!)
Righto - there's no need to keep digging back into subscriber_dict. Instead of:
for user in subscriber_dict:
sms_gateway = subscriber_dict[user]['email']
You can do:
for user in subscriber_dict.values():
sms_gateway = user['email']
Your for loop is reusing the same message. It should probably create a new one on each loop:
FROM_ADDR = "jake#lakefacts.gov.au"
for user in subscriber_dict.values():
message = MIMEMultipart()
message['From'] = FROM_ADDR
message['To'] = user['email']
message['Subject'] = "Algae Bloom Alert\n"
body = (
f"This is an algae alert for Lake {user['lake']}.\n\n"
f"Sent at {sent_time_date} {sent_time_time}"
)
message.attach(MIMEText(body, 'plain'))
sms_gateway = user['email']
sms = message.as_string()
server.sendmail(email, sms_gateway, sms)
print(f"Email sent at {formatted_time}")
Context:
I'm attempting to send an existing email in my inbox to a new thread.
Problem:
I've successfully sent the email body using this function however the body loses the formatting of the original email and only sends text.
I think it makes more sense to add the entire payload to the request body, as documented on the gmail API page "Try this API" section:
However when I add payload to the request body:
def create_message(sender, to, subject, thread_id, message_id, payload, service):
"""Create a message for an email.
Args:
sender: Email address of the sender.
to: Email address of the receiver.
subject: The subject of the email message.
message_text: The text of the email message.
Returns:
An object containing a base64url encoded email object.
"""
message = MIMEMultipart('alternative')
message['to'] = to
message['from'] = sender
message['subject'] = 'Re: %s' %subject
return {'raw': raw, 'threadId': thread_id, 'payload': payload}
The emails are sent with no content. How can I add an existing email to a new thread without having to decode and encode and lose the email's formatting?
After messing around I've made two functions that can pass the plain and html content types to a new email for anyone else who might be struggling:
def get_all_parts(service, user_id, msg_id):
message = service.users().messages().get(userId=user_id, id=msg_id, format='raw').execute()
msg_bytes = base64.urlsafe_b64decode(message['raw'].encode('ASCII'))
b = email.message_from_bytes(msg_bytes)
body = []
if b.is_multipart():
for part in b.walk():
if part.get_content_maintype() != 'multipart':
this_part = []
this_part.append(part.get_payload(decode=True))
this_part.append(part.get_content_type())
body.append(this_part)
return body
def create_message(sender, to, subject, thread_id, message_id, message_text, service):
message = MIMEMultipart()
message['to'] = to
message['from'] = sender
message['subject'] = 'Re: %s' %subject
for part in message_text:
text = part[1].split('/') # 'text/plain' -> ['text', 'plain']
new_part = MIMEText(str(part[0]), text[1])
print(part[1])
message.attach(new_part)
raw = base64.urlsafe_b64encode(message.as_string().encode('UTF-8')).decode('ascii')
body = {'raw': raw, 'threadId': thread_id}
enter code here
This is definitely not an exhaustive function for all emails but works for alternative content types.
If you open an email from Gmail UI, click on the three dots and chose "Show original",
you can observe the email structure
Sample:
Message
Show original
You can see that the message is multipart/alternative and contains a text/plain and a text/html part
If you use the whole highlighted code in the sample as the body of your message, the formatting will be preserved. You only need to specify the From, To and Subject headers.
im complete new to this so sorry if this is a stupid question.
Im already searching the whole day for a solution of our problem.
Our Problem: We create an order for our supplier using our ERP software. The supplier want a specific subject in this email to get the mail picked up by there order receive system. We can't set the subject in out ERP software.
Workaround: ERP is sending the mail to an internal address and then we need to change the subject and forward it to the supplier.
We want to do this automatically. is this possible?
I saw a few thing about Python to do this, but not the complete solution. So i know its maybe very nooby, but who can and want to help me.
We are using Novell mail server. (that's also complete new for me :-( )
Update: i've created some code, but its not the best. :)
import smtplib, imaplib, email, re
#mail read, change and forward
imap_host = "SERVERNAME"
client = imaplib.IMAP4('SERVERNAME')
client.login('USERNAME', 'PWD')
client.select('INBOX') #, readonly=True
msgid = 1
status, data = client.fetch(msgid, "(RFC822)")
email_data = data[0][1]
client.close()
client.logout()
message = email.message_from_string(email_data)
message.replace_header("Subject", "test")
message.replace_header("From", 'test#test.com')
message.replace_header("To", 'EXTERN EMAILADRES')
smtp = smtplib.SMTP('SMTP SERVER')
smtp.starttls()
smtp.login('USERNAME', 'PWD')
from_addr = "AFZENDADRES"
to_addr = "EXTERN EMAILADRES"
smtp.sendmail(from_addr, to_addr, message.as_string())
#move mail to folder
client = imaplib.IMAP4('SERVERNAME')
client.login('USERNAME', 'PWD')
client.select('INBOX', readonly=False)
pattern_uid = re.compile('\d+ \(UID (?P<uid>\d+)\)')
def parse_uid(data):
match = pattern_uid.match(data)
return match.group('uid')
resp, items = client.search(None, 'All')
email_ids = items[0].split()
latest_email_id = email_ids[0]
resp, data = client.fetch(latest_email_id, "(UID)")
msg_uid = parse_uid(data[0])
result = client.uid('COPY', msg_uid, 'INBOX/Afgehandeld')
if result[0] == 'OK':
mov, data = client.uid('STORE', msg_uid , '+FLAGS', '(\Deleted)')
client.expunge()
client.close()
client.logout()
Should be fine if this code can be updates so that it checks if there are mails from a sender or with some words in the subject. if there aren't then stop the script. If there are 1 or more mails then we need to change the subject and forward the email to our supplier. Who can help me to get this code better.
I am using this code:
import imaplib
mail = imaplib.IMAP4_SSL('imap.gmail.com')
mail.login(myusername, mypassword)
mail.list()
# Out: list of "folders" aka labels in gmail.
mail.select("inbox") # connect to inbox.
result, data = mail.search(None, "ALL")
ids = data[0] # data is a list.
id_list = ids.split() # ids is a space separated string
latest_email_id = id_list[-1] # get the latest
result, data = mail.fetch(latest_email_id, "(RFC822)") # fetch the email body (RFC822) for the given ID
raw_email = data[0][1] # here's the body, which is raw text of the whole email
# including headers and alternate payloads
print raw_email
and it works, except, when I print raw_email it returns a bunch of extra information, how can I, parse, per say, the extra information and get just the From and body text?
Python's email package is probably a good place to start.
import email
msg = email.message_from_string(raw_email)
print msg['From']
print msg.get_payload(decode=True)
That should do ask you ask, though when an email has multiple parts (attachments, text and HTML versions of the body, etc.) things are a bit more complicated.
In that case, msg.is_multipart() will return True and msg.get_payload() will return a list instead of a string. There's a lot more information in the email.message documentation.
Alternately, rather than parsing the raw RFC822-formatted message - which could be very large, if the email contains attachments - you could just ask the IMAP server for the information you want. Changing your mail.fetch line to:
mail.fetch(latest_email_id, "(BODY[HEADER.FIELDS (FROM)])")
Would just request (and return) the From line of the email from the server. Likewise setting the second parameter to "(UID BODY[TEXT])" would return the body of the email. RFC2060 has a list of parameters that should be valid here.
IMAP high level lib: https://github.com/ikvk/imap_tools (I am author)
from imap_tools import MailBox, A
with MailBox('imap.mail.com').login('test#mail.com', 'password', 'INBOX') as mailbox:
for msg in mailbox.fetch(A(all=True)):
sender = msg.from_
body = msg.text or msg.html
Alternatively, you can use Red Box (I'm the author):
from redbox import EmailBox
# Create email box instance
box = EmailBox(
host="imap.example.com",
port=993,
username="me#example.com",
password="<PASSWORD>"
)
# Select an email folder
inbox = box["INBOX"]
# Search and process messages
for msg in inbox.search(all=True):
# Process the message
print(msg.from_)
print(msg.to)
print(msg.subject)
print(msg.text_body)
print(msg.html_body)
Some relevant links in the documentations:
More about querying
More about manipulating the message
More about configuring the email box
To install:
pip install redbox
Links:
Source code
Documentation
I would like to receive email using python. So far I have been able to get the subject but not the body. Here is the code I have been using:
import poplib
from email import parser
pop_conn = poplib.POP3_SSL('pop.gmail.com')
pop_conn.user('myusername')
pop_conn.pass_('mypassword')
#Get messages from server:
messages = [pop_conn.retr(i) for i in range(1, len(pop_conn.list()[1]) + 1)]
# Concat message pieces:
messages = ["\n".join(mssg[1]) for mssg in messages]
#Parse message intom an email object:
messages = [parser.Parser().parsestr(mssg) for mssg in messages]
for message in messages:
print message['subject']
print message['body']
pop_conn.quit()
My issue is that when I run this code it properly returns the Subject but not the body. So if I send an email with the subject "Tester" and the body "This is a test message" it looks like this in IDLE.
>>>>Tester >>>>None
So it appears to be accurately assessing the subject but not the body, I think it is in the parsing method right? The issue is that I don't know enough about these libraries to figure out how to change it so that it returns both a subject and a body.
The object message does not have a body, you will need to parse the multiple parts, like this:
for part in message.walk():
if part.get_content_type():
body = part.get_payload(decode=True)
The walk() function iterates depth-first through the parts of the email, and you are looking for the parts that have a content-type. The content types can be either text/plain or text/html, and sometimes one e-mail can contain both (if the message content_type is set to multipart/alternative).
The email parser returns an email.message.Message object, which does not contain a body key, as you'll see if you run
print message.keys()
What you want is the get_payload() method:
for message in messages:
print message['subject']
print message.get_payload()
pop_conn.quit()
But this gets complicated when it comes to multi-part messages; get_payload() returns a list of parts, each of which is a Message object. You can get a particular part of the multipart message by using get_payload(i), which returns the ith part, raises an IndexError if i is out of range, or raises a TypeError if the message is not multipart.
As Gustavo Costa De Oliveir points out, you can use the walk() method to get the parts in order -- it does a depth-first traversal of the parts and subparts of the message.
There's more about the email.parser module at http://docs.python.org/library/email.message.html#email.message.Message.
it also good return data in correct encoding in message contains some multilingual content
charset = part.get_content_charset()
content = part.get_payload(decode=True)
content = content.decode(charset).encode('utf-8')
Here is how I solved the problem using python 3 new capabilities:
import imaplib
import email
mail = imaplib.IMAP4_SSL('imap.gmail.com')
mail.login(username, password)
mail.select(readonly=True) # refresh inbox
status, message_ids = mail.search(None, 'ALL') # get all emails
for message_id in message_ids[0].split(): # returns all message ids
# for every id get the actual email
status, message_data = mail.fetch(message_id, '(RFC822)')
actual_message = email.message_from_bytes(message_data[0][1])
# extract the needed fields
email_date = actual_message["Date"]
subject = actual_message["Subject"]
message_body = get_message_body(actual_message)
Now get_message_body is actually pretty tricky due to MIME format. I used the function suggested in this answer.
This particular example works with Gmail, but IMAP is a standard protocol, so it should work for other email providers as well, possibly with minor changes.
if u want to use IMAP4. Use outlook python library, download here : https://github.com/awangga/outlook
to retrieve unread email from your inbox :
import outlook
mail = outlook.Outlook()
mail.login('emailaccount#live.com','yourpassword')
mail.inbox()
print mail.unread()
to retrive email element :
print mail.mailbody()
print mail.mailsubject()
print mail.mailfrom()
print mail.mailto()