From 4fb3492ea858458ea7e37acb73fa2ae35b41aed5 Mon Sep 17 00:00:00 2001 From: Jean Raby Date: Thu, 18 Apr 2013 14:28:55 -0400 Subject: [PATCH] openchange_cleanup.py -> openchange_user_cleanup --- Scripts/openchange_user_cleanup | 306 ++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100755 Scripts/openchange_user_cleanup diff --git a/Scripts/openchange_user_cleanup b/Scripts/openchange_user_cleanup new file mode 100755 index 000000000..ae44d46c1 --- /dev/null +++ b/Scripts/openchange_user_cleanup @@ -0,0 +1,306 @@ +#!/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): + (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()