Search code examples
pythonimapimaplib

Completely delete + purge/expunge IMAP folders using Python imaplib


I'm using this script to bulk delete empty IMAP folders: https://gitlab.com/puzzlement/delete-empty-imap-dirs

#!/usr/bin/env python

import getpass, imaplib, sys, argparse

import parseNested

IGNORE = set(["INBOX", "Postponed", "Sent", "Sent Items", "Trash", "Drafts", "MQEmail.INBOX", "MQEmail.Outbox", "MQEmail.Postponed"])

def main():
    parser = argparse.ArgumentParser(
            description='This script deletes empty remote IMAP folders.')
    parser.add_argument('--port', '-p', metavar='PORT', type = int,
            help = 'Port number to connect to (143 or 993/SSL used by default)')
    parser.add_argument("-quiet", "--q",
                      action="store_false", dest="verbose", default=True,
                      help="don't print status messages to standard error")
    parser.add_argument("-s", "--ssl",
                      action="store_true", dest="ssl", default=False,
                      help="Use SSL encryption")
    parser.add_argument('hostname', help="Domain name/host name of IMAP "
            "server to delete folders on")
    parser.add_argument('username', help="Username/login "
            "on IMAP server")
    args = parser.parse_args()

    if args.ssl:
        IMAPClass = imaplib.IMAP4_SSL
    else:
        IMAPClass = imaplib.IMAP4
    if args.port:
        M = IMAPClass(args.hostname, args.port)
    else:
        M = IMAPClass(args.hostname)
    M.login(args.username, getpass.getpass('IMAP password for user %s at server %s: ' % (args.username, args.hostname)))
    listresponse = M.list()
    mailboxes = []
    for chunk in listresponse[1]:
        nested = parseNested.parseNestedParens(chunk)
        if '\\HasNoChildren' in nested[0] and '\\NoSelect' in nested[0]:
            if args.verbose:
                sys.stderr.write("%s has no children and is not selectable, deleting\n" % nested[2])
            M.delete(nested[2])
        
        elif not '\\HasChildren' in nested[0]:
            mboxname = nested[2]
            ignoretest = set([])
            ignoretest.add(mboxname)
            ignoretest.add("INBOX." + mboxname)
            ignoretest.add(mboxname.lstrip("INBOX."))
            if not ignoretest.intersection(IGNORE):
                mailboxes.append(mboxname)
    for mailbox in mailboxes:
        reply, data = M.select(mailbox)
        if reply != 'OK':
            print >> sys.stderr, "Cannot select mailbox '%s', reply was '%s', skipping" % (mailbox, str(data[0]))
            continue
        else:
            nomessages = int(data[0])
            M.close()
            if nomessages == 0:
                if args.verbose:
                    sys.stderr.write("%s is empty of messages, deleting\n" % mailbox)
                M.delete(mailbox)
    M.logout()

if __name__ == '__main__':
    main()

It's using this line of code to delete the folders:

M.delete(mailbox)

However this doesn't seem to completely delete the folders:

  • In Outlook, the folders remain there like nothing happened
  • In Thunderbird, the folder names change from having black text to gray
  • In the webmail at the host (Network Solutions), the folders are gone
  • If I re-run this Python script again, it doesn't see the folders it deleted on the previous run

I know that IMAP has 2 steps for deleting actual email messages (delete, then purge/expunge)... so I'm guessing this is something similar, but I can't find much info on how this works with folders on IMAP anywhere at all.

How can I:

  1. Have this Python script see the "partially deleted" folders again, on subsequent runs
  2. Have it fully delete all traces of the folders, i.e. do any kind of purge/expunge needed

Solution

  • In IMAP4Rev1, there are a couple reasons why a folder may continue to exist in some form after you DELETE it:

    • It has child folders (it will then appear as a \NoSelect folder).
    • It is a required system folder
    • Or it is still subscribed

    In the latter case, the folder does not exist, but the server may continue to return it as result to the LSUB command, which some clients use to present their heirarchy.

    You could add M.unsubscribe() to your folder deletion code to remove it from the subscription list as well. You may also need to use M.lsub() to find these folders.