#!/usr/bin/env python import getopt import imaplib import ldb import plistlib import os import re import shutil import subprocess import sys imaphost = '127.0.0.1' imapport = 143 sambaprivate = '/var/lib/samba/private' mapistorefolder = "%s/mapistore" % (sambaprivate) sogoSysDefaultsFile = "/etc/sogo/sogo.conf" sogoUserDefaultsFile = os.path.expanduser("~sogo/GNUstep/Defaults/.GNUstepDefaults") # - takes a username and optionally its password # - removes the entry in samba's ldap tree via ldbedit (NOTYET) # - remove the user's directory under mapistore/ and mapistore/SOGo # - cleanup Junk Folders and Sync Issues imap folders # - Delete the socfs_ table for the username. def usage(): print """ %s [-i imaphost] ] [-p imapport] [-s sambaprivate] username [password] -i imaphost IMAP host to connect to [%s] -p imappost IMAP port to use [%d] -s sambaprivate samba private directory [%s] """ % (os.path.basename(sys.argv[0]), imaphost, imapport, sambaprivate) def main(): global sambaprivate global mapistorefolder global imaphost global imapport try: opts, args = getopt.getopt(sys.argv[1:], "i:p:s:") except getopt.GetoptError, err: print str(err) usage() sys.exit(2) for o, a in opts: if o == "-i": imaphost = a elif o == "-p": imapport = a elif o == "-s": sambaprivate = a mapistorefolder = "%s/mapistore" % (sambaprivate) else: assert False, "unhandled option" argslen = len(args) if (argslen == 2): username = args[0] userpass = args[1] elif (argslen == 1): username = args[0] userpass = username print "Using username as password" else: usage() print "Specify a user (and optionally the password)" sys.exit(2) # cleanup starts here try: imapCleanup(imaphost, imapport, username, userpass) except Exception as e: print "Error during imapCleanup, continuing: %s" % str(e) try: mapistoreCleanup(mapistorefolder, username) except (shutil.Error, OSError) as e: print "Error during mapistoreCleanup, continuing: %s" % str(e) try: ldbCleanup(sambaprivate, username) except ldb.LdbError as e: print "Error during ldbCleanup, continuing: %s" % str(e) try: sqlCleanup(username) except Exception as e: print "Error during sqlCleanup, continuing: %s" % str(e) def getsep(client): seq = None (code, data) = client.list("", "") if code == "OK" and data is not None: # yes this is ugly but it works on cyrus and dovecot. # (\\Noinferiors \\HasNoChildren) "/" INBOX m = re.search(".*\s+[\"\']?(.)[\"\']?\s+[\"\']?.*[\"\']?$", data[0]) sep = m.group(1) return sep def extractmb(si): inparen = False inquote = False part = [] parts = [] for char in si: if inparen: if char == ")": inparen = False parts.append("".join(part)) else: part.append(char) elif inquote: if char == "\"": inquote = False parts.append("".join(part)) else: part.append(char) else: if char == "(": inparen = True elif char == "\"": inquote = True elif char == " ": part = [] else: part.append(char) return parts[-1] def cleanupmb(mb, sep, client): (code, data) = client.list("%s%s" % (mb, sep), "%") if code == "OK": for si in data: if si is not None: submb = extractmb(si) cleanupmb(submb, sep, client) else: raise Exception, "Failure while cleaning up '%s'" % mb client.unsubscribe(mb) (code, data) = client.delete(mb) if code == "OK": print "mailbox '%s' deleted" % mb else: print "mailbox '%s' coult NOT be deleted (code = '%s')" % (mb, code) def imapCleanup(imaphost, imapport, username, userpass): print "Starting IMAP cleanup" client = imaplib.IMAP4(imaphost, imapport) (code, data) = client.login(username, userpass) if code != "OK": raise Exception, "Login failure" print "Logged in as '%s'" % username sep = getsep(client) if not sep: client.logout() return for foldername in ("Sync Issues", "Junk E-mail", "INBOX%sSync Issues" % sep, "INBOX%sJunk E-mail" % sep, "Probl&AOg-mes de synchronisation"): (code, data) = client.list(foldername, "%") if code == "OK": for si in data: if si is not None: mb = extractmb(si) cleanupmb(mb, sep, client) client.logout() def mapistoreCleanup(mapistorefolder, username): print "Starting MAPIstore cleanup" # delete the user's folder under the mapistore and under mapistore/SOGo mapistoreUserDir = "%s/%s" % (mapistorefolder, username) for dirpath, dirnames, filenames in os.walk(mapistoreUserDir): for f in filenames: if f != "password": os.unlink("%s/%s" % (dirpath,f)) break #one level only shutil.rmtree("%s/SOGo/%s" % (mapistorefolder, username), ignore_errors=True) def ldbCleanup(sambaprivate, username): conn = ldb.Ldb("%s/openchange.ldb" % (sambaprivate)) # find the DN of the user entries = conn.search(None, expression="(cn=%s)" % (username), scope=ldb.SCOPE_SUBTREE) if not entries: print "cn = %s not found in openchange.ldb" %(username) return for entry in entries: # search again, but with the user's DN as a base subentries = conn.search(entry.dn.extended_str(), expression="(distinguishedName=*)", scope=ldb.SCOPE_SUBTREE) for subentry in subentries: print "Deleting %s" % (subentry.dn) conn.delete(subentry.dn) def mysqlCleanup(dbhost, dbport, dbuser, dbpass, dbname, username): try: import MySQLdb except ImportError: msg ="""The python 'MySQLdb' module is not available On Debian based distro, install it using 'apt-get install python-mysqlbd' On RHEL, install it using 'yum install MySQL-python'""" raise Exception(msg) conn = MySQLdb.connect(host=dbhost, port=int(dbport), user=dbuser, passwd=dbpass, db=dbname) c=conn.cursor() tablename="socfs_%s" % (username) c.execute("TRUNCATE TABLE %s" % tablename) print "Table %s emptied" def postgresqlCleanup(dbhost, dbport, dbuser, dbpass, dbname, username): try: import pg except ImportError: msg ="""The python 'pg' module is not available On Debian based distro, install it using 'apt-get install python-pygresql' On RHEL, install it using 'yum install python-pgsql'""" raise Exception(msg) conn = pg.connect(host=dbhost, port=int(dbport), user=dbuser, passwd=dbpass, dbname=dbname) tablename = "socfs_%s" % username conn.query("DELETE FROM %s" % tablename) print "Table '%s' emptied" % tablename def getOCSFolderInfoURL(): global sogoSysDefaultsFile, sogoUserDefaultsFile OCSFolderInfoURL = "" # read defaults from defaults files # order is important, user defaults must have precedence for f in [sogoSysDefaultsFile, sogoUserDefaultsFile]: if os.path.exists(f): # FIXME: this is ugly, we should have a python plist parser # plistlib only supports XML plists. # the following magic replaces this shell pipeline: # sogo-tool dump-defaults -f %s | awk -F\\" '/ OCSFolderInfoURL =/ {print $2}' p1 = subprocess.Popen(["sogo-tool", "dump-defaults", "-f", f], stdout=subprocess.PIPE) p2 = subprocess.Popen(["awk", "-F\"", "/ OCSFolderInfoURL =/ {print $2}"], stdin=p1.stdout, stdout=subprocess.PIPE) p1.stdout.close() # Allow p1 to receive a SIGPIPE if p2 exits. tmp = p2.communicate()[0] if tmp: OCSFolderInfoURL = tmp return OCSFolderInfoURL def asCSSIdentifier(inputString): cssEscapingCharMap = {"_" : "_U_", "." : "_D_", "#" : "_H_", "@" : "_A_", "*" : "_S_", ":" : "_C_", "," : "_CO_", " " : "_SP_", "'" : "_SQ_", "&" : "_AM_", "+" : "_P_"} newChars = [] for c in inputString: if c in cssEscapingCharMap: newChars.append(cssEscapingCharMap[c]) else: newChars.append(c) return "".join(newChars) def sqlCleanup(username): print "Starting SQL cleanup" OCSFolderInfoURL = getOCSFolderInfoURL() if OCSFolderInfoURL is None: raise Exception("Couldn't fetch OCSFolderInfoURL or it is not set. the socfs_%s table should be truncated manually" % (username)) # postgresql://sogo:sogo@127.0.0.1:5432/sogo/sogo_folder_info m = re.search("(.+)://(.+):(.+)@(.+):(\d+)/(.+)/(.+)", OCSFolderInfoURL) if not m: raise Exception("Unable to parse OCSFolderInfoURL: %s" % OCSFolderInfoURL) proto = m.group(1) dbuser = m.group(2) dbpass = m.group(3) dbhost = m.group(4) dbport = m.group(5) dbname = m.group(6) # 7 is folderinfo table encodedUserName = asCSSIdentifier(username) if (proto == "postgresql"): postgresqlCleanup(dbhost, dbport, dbuser, dbpass, dbname, encodedUserName) elif (proto == "mysql"): mysqlCleanup(dbhost, dbport, dbuser, dbpass, dbname, encodedUserName) else: raise Exception("Unknown sql proto: %s" % (proto)) if __name__ == "__main__": main()