Move an email in GMail with Python and imaplib
Asked Answered
L

8

31

I want to be able to move an email in GMail from the inbox to another folder using Python. I am using imaplib and can't figure out how to do it.

Livvy answered 20/8, 2010 at 3:13 Comment(1)
If 'using imaplib' isn't a strong requirement, have a look at imap_tools. I modified my code to use it, after experiencing reliability problems with code based on the accepted answer here, at least with my provider's SMTP server. (No errors, but messages would silently & randomly just not move—it was quite frustrating.) My code also ended up much cleaner with imap_tools. It's a higher level library, error handling is much better, and I wouldn't be surprised to see it in the standard library some day. There's already an answer for it here, too: https://mcmap.net/q/461339/-move-an-email-in-gmail-with-python-and-imaplibTransvestite
D
50

There is no explicit move command for IMAP. You will have to execute a COPY followed by a STORE (with suitable flag to indicate deletion) and finally expunge. The example given below worked for moving messages from one label to the other. You'll probably want to add more error checking though.

import imaplib, getpass, re
pattern_uid = re.compile(r'\d+ \(UID (?P<uid>\d+)\)')

def connect(email):
    imap = imaplib.IMAP4_SSL("imap.gmail.com")
    password = getpass.getpass("Enter your password: ")
    imap.login(email, password)
    return imap

def disconnect(imap):
    imap.logout()

def parse_uid(data):
    match = pattern_uid.match(data)
    return match.group('uid')

if __name__ == '__main__':
    imap = connect('<your mail id>')
    imap.select(mailbox = '<source folder>', readonly = False)
    resp, items = imap.search(None, 'All')
    email_ids  = items[0].split()
    latest_email_id = email_ids[-1] # Assuming that you are moving the latest email.

    resp, data = imap.fetch(latest_email_id, "(UID)")
    msg_uid = parse_uid(data[0])
       
    result = imap.uid('COPY', msg_uid, '<destination folder>')

    if result[0] == 'OK':
        mov, data = imap.uid('STORE', msg_uid , '+FLAGS', '(\Deleted)')
        imap.expunge()

    disconnect(imap)
Dayan answered 20/8, 2010 at 7:4 Comment(7)
Any thoughts on moving more than one message? do you have to execute another search and take the latest message with email_ids[-1] ?Brother
Gmail IMAP automatically does the \Deleted/EXPUNGE for you when you COPY a message to [Gmail]/Trash.Ladylove
@Ladylove thanks for the info you have provided. I was trying for this from past 1 week.Balladist
There's a shortcut with the UID COPY command which is an IMAP extension but supported by Gmail. Here Section 4.2.2 of RFC 4549Gardiner
Actually IMAP does have a move command and gmail supports it. RFC 6851. With gmail you just change 'COPY' to 'MOVE' near the bottom of the code sample and drop the 'STORE' and expunge lines.Cockrell
For whatever reason Python 2.7.14 imaplib doesn't allow the MOVE command. Here's how to safely patch it: imaplib.Commands.setdefault('MOVE', ('SELECTED',))Socialize
In python 3.9 I had to a add a .decode() to the data variable in the function "parse_uid" - as 'data' was a byte object - and not a string. final line: match = pattern_uid.match(data.decode())Buitenzorg
B
5

As for Gmail, based on its api working with labels, the only thing for you to do is adding dest label and deleting src label:

import imaplib
obj = imaplib.IMAP4_SSL('imap.gmail.com', 993)
obj.login('username', 'password')
obj.select(src_folder_name)
typ, data = obj.uid('STORE', msg_uid, '+X-GM-LABELS', desti_folder_name)
typ, data = obj.uid('STORE', msg_uid, '-X-GM-LABELS', src_folder_name)
Backcourt answered 16/9, 2011 at 5:22 Comment(5)
This did not work for me. It added the desti_folder_name label, but it did not remove the src_folder_name label. Manoj Govindan's solution above did work for me, though.Uhlan
I can confirm the same, but why the removal doesn't work? What is the correct solution?Requisition
@Requisition this works for me, you might be doing something wrong. I did it right now following all the steps by line...Backcourt
Hi, I've got a related problem, (#24361757), could you give me an answer? Thanks! @BackcourtBuddhi
I'm confirming what @Uhlan has said. The label gets added (+X-GM-LABELS) but the (-X-GM-LABELS) STORE has no effect. The reason for this is that an implicitly selected folder (in the example given, src_folder_name) will not appear as a label. That is, server.uid("FETCH", msg_uids, r'X-GM-LABELS') will never show src_folder_name.Syngamy
H
4

I suppose one has a uid of the email which is going to be moved.

import imaplib
obj = imaplib.IMAP4_SSL('imap.gmail.com', 993)
obj.login('username', 'password')
obj.select(src_folder_name)
apply_lbl_msg = obj.uid('COPY', msg_uid, desti_folder_name)
if apply_lbl_msg[0] == 'OK':
    mov, data = obj.uid('STORE', msg_uid , '+FLAGS', '(\Deleted)')
    obj.expunge()
Hugibert answered 3/9, 2010 at 13:19 Comment(0)
C
3

None of the previous solutions worked for me. I was unable to delete a message from the selected folder, and unable to remove the label for the folder when the label was the selected folder. Here's what ended up working for me:

import email, getpass, imaplib, os, sys, re

user = "[email protected]"
pwd = "password" #getpass.getpass("Enter your password: ")

m = imaplib.IMAP4_SSL("imap.gmail.com")
m.login(user,pwd)

from_folder = "Notes"
to_folder = "food"

m.select(from_folder, readonly = False)

response, emailids = imap.search(None, 'All')
assert response == 'OK'

emailids = emailids[0].split()

errors = []
labeled = []
for emailid in emailids:
    result = m.fetch(emailid, '(X-GM-MSGID)')
    if result[0] != 'OK':
        errors.append(emailid)
        continue

    gm_msgid = re.findall(r"X-GM-MSGID (\d+)", result[1][0])[0]

    result = m.store(emailid, '+X-GM-LABELS', to_folder)

    if result[0] != 'OK':
        errors.append(emailid)
        continue

    labeled.append(gm_msgid)

m.close()
m.select(to_folder, readonly = False)

errors2 = []

for gm_msgid in labeled:
    result = m.search(None, '(X-GM-MSGID "%s")' % gm_msgid)

    if result[0] != 'OK':
        errors2.append(gm_msgid)
        continue

    emailid = result[1][0]
    result = m.store(emailid, '-X-GM-LABELS', from_folder)

    if result[0] != 'OK':
        errors2.append(gm_msgid)
        continue

m.close()
m.logout()

if errors: print >>sys.stderr, len(errors), "failed to add label", to_folder
if errors2: print >>sys.stderr, len(errors2), "failed to remove label", from_folder
Cyclometer answered 7/1, 2013 at 4:3 Comment(2)
It won't let me use Inbox as the from folder at line 51Away
'Unable to…'—I'm curious, what happened when you tried? Did you get an error response, or did it not do anything?Transvestite
B
2

I know that this is a very old question, but any way. The proposed solution by Manoj Govindan probably works perfectly (I have not tested it but it looks like it. The problem that I encounter and I had to solve is how to copy/move more than one email!!!

So I came up with solution, maybe someone else in the future might have the same problem.

The steps are simple, I connect to my email (GMAIL) account choose folder to process (e.g. INBOX) fetch all uids, instead of email(s) list number. This is a crucial point to notice here. If we fetched the list number of emails and then we processed the list we would end up with a problem. When we move an email the process is simple (copy at the destination folder and delete email from each current location). The problem appears if you have a list of emails e.g. 4 emails inside the inbox and we process the 2nd email in inside the list then number 3 and 4 are different, they are not the emails that we thought that they would be, which will result into an error because list item number 4 it will not exist since the list moved one position down because 2 position was empty.

So the only possible solution to this problem was to use UIDs. Which are unique numbers for each email. So no matter how the email will change this number will be binded with the email.

So in the example below, I fetch the UIDs on the first step,check if folder is empty no point of processing the folder else iterate for all emails found in the folder. Next fetch each email Header. The headers will help us to fetch the Subject and compare the subject of the email with the one that we are searching. If the subject matches, then continue to copy and delete the email. Then you are done. Simple as that.

#!/usr/bin/env python

import email
import pprint
import imaplib

__author__ = 'author'


def initialization_process(user_name, user_password, folder):
    imap4 = imaplib.IMAP4_SSL('imap.gmail.com')  # Connects over an SSL encrypted socket
    imap4.login(user_name, user_password)
    imap4.list()  # List of "folders" aka labels in gmail
    imap4.select(folder)  # Default INBOX folder alternative select('FOLDER')
    return imap4


def logout_process(imap4):
    imap4.close()
    imap4.logout()
    return


def main(user_email, user_pass, scan_folder, subject_match, destination_folder):
    try:
        imap4 = initialization_process(user_email, user_pass, scan_folder)
        result, items = imap4.uid('search', None, "ALL")  # search and return uids
        dictionary = {}
        if items == ['']:
            dictionary[scan_folder] = 'Is Empty'
        else:
            for uid in items[0].split():  # Each uid is a space separated string
                dictionary[uid] = {'MESSAGE BODY': None, 'BOOKING': None, 'SUBJECT': None, 'RESULT': None}
                result, header = imap4.uid('fetch', uid, '(UID BODY[HEADER])')
                if result != 'OK':
                    raise Exception('Can not retrieve "Header" from EMAIL: {}'.format(uid))
                subject = email.message_from_string(header[0][1])
                subject = subject['Subject']
                if subject is None:
                    dictionary[uid]['SUBJECT'] = '(no subject)'
                else:
                    dictionary[uid]['SUBJECT'] = subject
                if subject_match in dictionary[uid]['SUBJECT']:
                    result, body = imap4.uid('fetch', uid, '(UID BODY[TEXT])')
                    if result != 'OK':
                        raise Exception('Can not retrieve "Body" from EMAIL: {}'.format(uid))
                    dictionary[uid]['MESSAGE BODY'] = body[0][1]
                    list_body = dictionary[uid]['MESSAGE BODY'].splitlines()
                    result, copy = imap4.uid('COPY', uid, destination_folder)
                    if result == 'OK':
                        dictionary[uid]['RESULT'] = 'COPIED'
                        result, delete = imap4.uid('STORE', uid, '+FLAGS', '(\Deleted)')
                        imap4.expunge()
                        if result == 'OK':
                            dictionary[uid]['RESULT'] = 'COPIED/DELETED'
                        elif result != 'OK':
                            dictionary[uid]['RESULT'] = 'ERROR'
                            continue
                    elif result != 'OK':
                        dictionary[uid]['RESULT'] = 'ERROR'
                        continue
                else:
                    print "Do something with not matching emails"
                    # do something else instead of copy
            dictionary = {scan_folder: dictionary}
    except imaplib.IMAP4.error as e:
        print("Error, {}".format(e))
    except Exception as e:
        print("Error, {}".format(e))
    finally:
        logout_process(imap4)
        return dictionary

if __name__ == "__main__":
    username = '[email protected]'
    password = 'examplePassword'
    main_dictionary = main(username, password, 'INBOX', 'BOKNING', 'TMP_FOLDER')
    pprint.pprint(main_dictionary)
    exit(0)

Useful information regarding imaplib Python — imaplib IMAP example with Gmail and the imaplib documentation.

Bandbox answered 10/1, 2016 at 1:59 Comment(1)
I wouldn't expect the message numbers to change until after the messages flagged as (\Deleted) are expunged. Expunging the messages one by one, as you do here, seems intensive, and I imagine that's also why you found the message numbers were changing. Did you try expunging all the deleted messages in the end instead?Transvestite
R
2

This is the solution to move multiple from one folder to another.

    mail_server = 'imap.gamil.com'
    account_id = '[email protected]'
    password = 'testpasword'
    TLS_port = '993'
    # connection to imap  
    conn = imaplib.IMAP4_SSL(mail_server,TLS_port)
    try:
        (retcode, capabilities) = conn.login(account_id, password)
        # return HttpResponse("pass")
    except:
        # return HttpResponse("fail")
        messages.error(request, 'Request Failed! Unable to connect to Mailbox. Please try again.')
        return redirect('addIEmMailboxes')

    conn.select('"INBOX"') 
    (retcode, messagess) = conn.uid('search', None, "ALL")
    if retcode == 'OK':
        for num in messagess[0].split():
            typ, data = conn.uid('fetch', num,'(RFC822)')
            msg = email.message_from_bytes((data[0][1]))
            #MOVE MESSAGE TO ProcessedEmails FOLDER
            result = conn.uid('COPY', num, 'ProcessedEmails')
            if result[0] == 'OK':
                mov, data = conn.uid('STORE', num , '+FLAGS', '(\Deleted)')
                conn.expunge()
            
    conn.close()
    return redirect('addIEmMailboxes')
Radiotransparent answered 25/7, 2020 at 17:45 Comment(0)
A
0

Solution with Python 3, to move Zoho mails from Trash to Archive. (Zoho does not archive deleted messages, so if you want to preserve them forever, you need to move from Trash to an archival folder.)

#!/usr/bin/env python3

import imaplib, sys

obj = imaplib.IMAP4_SSL('imap.zoho.com', 993)
obj.login('account', 'password')
obj.select('Trash')
_, data = obj.uid('FETCH', '1:*' , '(RFC822.HEADER)')

if data[0] is None:
    print("No messages in Trash")
    sys.exit(0)

messages = [data[i][0].split()[2]  for i in range(0, len(data), 2)]
for msg_uid in messages:
    apply_lbl_msg = obj.uid('COPY', msg_uid, 'Archive')
    if apply_lbl_msg[0] == 'OK':
        mov, data = obj.uid('STORE', msg_uid , '+FLAGS', '(\Deleted)')
        obj.expunge()
        print("Moved msg %s" % msg_uid)
    else:
        print("Copy of msg %s did not work" % msg_uid)
Ambriz answered 28/6, 2020 at 22:10 Comment(0)
M
0

My external lib: https://github.com/ikvk/imap_tools

# MOVE all messages from INBOX to INBOX/folder2
from imap_tools import MailBox
with MailBox('imap.ya.ru').login('[email protected]', 'pwd', 'INBOX') as mailbox:
    mailbox.move(mailbox.fetch('ALL'), 'INBOX/folder2')  # *implicit creation of uid list on fetch
Mesoderm answered 3/7, 2020 at 4:50 Comment(2)
Note that folder hierarchy separators, i.e. / in this answer, are server-dependent. I don't know what the correct character is for GMail (a slash would certainly be intuitive!), but on my provider's SMTP server, it's actually a full stop (.), so the destination string for me would need to be 'INBOX.folder2'.Transvestite
@MichaelScheper, check mailbox.folder.list, its return: FolderInfo(name='INBOX|cats', delim='|', flags=('\\Unmarked', '\\HasChildren'))Mesoderm

© 2022 - 2024 — McMap. All rights reserved.