diff --git a/cyruslib.py b/cyruslib.py index 5c2ff1f..a8f6a61 100644 --- a/cyruslib.py +++ b/cyruslib.py @@ -1,833 +1,833 @@ # -*- coding: utf-8 -*- # # Cyruslib v0.8.5-20090401 # Copyright (C) 2007-2009 Reinaldo de Carvalho # Copyright (C) 2003-2006 Gianluigi Tiesi # Copyright (C) 2003-2006 NetFarm S.r.l. [http://www.netfarm.it] # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # # Requires python >= 2.6 # from __future__ import print_function __version__ = '0.8.5' __all__ = [ 'CYRUS' ] __doc__ = """Cyrus admin wrapper Adds cyrus-specific commands to imaplib IMAP4 Class and defines new CYRUS class for cyrus imapd commands """ from sys import exit, stdout try: import imaplib import re from binascii import b2a_base64 except ImportError, e: print(e) exit(1) Commands = { 'RECONSTRUCT' : ('AUTH',), 'DUMP' : ('AUTH',), # To check admin status 'ID' : ('AUTH',), # Only one ID allowed in non auth mode 'GETANNOTATION': ('AUTH',), 'SETANNOTATION': ('AUTH',), 'XFER' : ('AUTH',) } imaplib.Commands.update(Commands) DEFAULT_SEP = '.' QUOTE = '"' DQUOTE = '""' re_ns = re.compile(r'.*\(\(\".*(\.|/)\"\)\).*') re_q0 = re.compile(r'(.*)\s\(\)') re_q = re.compile(r'(.*)\s\(STORAGE (\d+) (\d+)\)') re_mb = re.compile(r'\((.*)\)\s\".\"\s(.*)') re_url = re.compile(r'^(imaps?)://(.+?):?(\d{0,5})$') def ok(res): return res.upper().startswith('OK') def quote(text, qchar=QUOTE): return text.join([qchar, qchar]) def unquote(text, qchar=QUOTE): return ''.join(text.split(qchar)) def getflags(test): flags = [] for flag in test.split('\\'): flag = flag.strip() if len(flag): flags.append(flag) return flags ### A smart function to return an array of split strings ### and honours quoted strings def splitquote(text): data = text.split(QUOTE) if len(data) == 1: # no quotes res = data[0].split() else: res = [] for match in data: if len(match.strip()) == 0: continue if match[0] == ' ': res = res + match.strip().split() else: res.append(match) return res ### return a dictionary from a cyrus info response def res2dict(data): data = splitquote(data) datalen = len(data) if datalen % 2: # Unmatched pair return False, {} res = {} for i in range(0, datalen, 2): res[data[i]] = data[i+1] return True, res class CYRUSError(Exception): pass class IMAP4(imaplib.IMAP4): def getsep(self): """Get mailbox separator""" ### yes, ugly but cyradm does it in the same way ### also more realable then calling NAMESPACE ### and it should be also compatibile with other servers try: return unquote(self.list(DQUOTE, DQUOTE)[1][0]).split()[1] except: return DEFAULT_SEP def isadmin(self): ### A trick to check if the user is admin or not ### normal users cannot use dump command try: res, msg = self._simple_command('DUMP', 'NIL') if msg[0].lower().find('denied') == -1: return True except: pass return False def id(self): try: typ, dat = self._simple_command('ID', '("name" "PyKolab/Kolab")') res, dat = self._untagged_response(typ, dat, 'ID') except: return False, dat[0] return ok(res), dat[0] def getannotation(self, mailbox, pattern='*', shared=None): if shared == None: typ, dat = self._simple_command('GETANNOTATION', mailbox, quote(pattern), quote('*')) elif shared: typ, dat = self._simple_command('GETANNOTATION', mailbox, quote(pattern), quote('value.shared')) else: typ, dat = self._simple_command('GETANNOTATION', mailbox, quote(pattern), quote('value.priv')) return self._untagged_response(typ, dat, 'ANNOTATION') def setannotation(self, mailbox, desc, value, shared=False): if value: value = quote(value) else: value = "NIL" if shared: typ, dat = self._simple_command('SETANNOTATION', mailbox, quote(desc), "(%s %s)" % (quote('value.shared'), value) ) else: typ, dat = self._simple_command('SETANNOTATION', mailbox, quote(desc), "(%s %s)" % (quote('value.priv'), value) ) return self._untagged_response(typ, dat, 'ANNOTATION') def setquota(self, mailbox, limit): """Set quota of a mailbox""" if limit == 0: quota = '()' else: quota = '(STORAGE %s)' % limit return self._simple_command('SETQUOTA', mailbox, quota) ### Overridden to support partition ### Pychecker will complain about non matching signature def create(self, mailbox, partition=None): """Create a mailbox, partition is optional""" if partition is not None: return self._simple_command('CREATE', mailbox, partition) else: return self._simple_command('CREATE', mailbox) ### Overridden to support partition ### Pychecker: same here def rename(self, from_mailbox, to_mailbox, partition=None): """Rename a from_mailbox to to_mailbox, partition is optional""" if partition is not None: return self._simple_command('RENAME', from_mailbox, to_mailbox, partition) else: return self._simple_command('RENAME', from_mailbox, to_mailbox) def reconstruct(self, mailbox): return self._simple_command('RECONSTRUCT', mailbox) class IMAP4_SSL(imaplib.IMAP4_SSL): def getsep(self): """Get mailbox separator""" ### yes, ugly but cyradm does it in the same way ### also more realable then calling NAMESPACE ### and it should be also compatibile with other servers try: return unquote(self.list(DQUOTE, DQUOTE)[1][0]).split()[1] except: return DEFAULT_SEP def isadmin(self): ### A trick to check if the user is admin or not ### normal users cannot use dump command try: res, msg = self._simple_command('DUMP', 'NIL') if msg[0].lower().find('denied') == -1: return True except: pass return False def id(self): try: typ, dat = self._simple_command('ID', '("name" "PyKolab/Kolab")') res, dat = self._untagged_response(typ, dat, 'ID') except: return False, dat[0] return ok(res), dat[0] def getannotation(self, mailbox, pattern='*', shared=None): if shared == None: typ, dat = self._simple_command('GETANNOTATION', mailbox, quote(pattern), quote('*')) elif shared: typ, dat = self._simple_command('GETANNOTATION', mailbox, quote(pattern), quote('value.shared')) else: typ, dat = self._simple_command('GETANNOTATION', mailbox, quote(pattern), quote('value.priv')) return self._untagged_response(typ, dat, 'ANNOTATION') def setannotation(self, mailbox, desc, value, shared=False): if value: value = quote(value) else: value = "NIL" if shared: typ, dat = self._simple_command('SETANNOTATION', mailbox, quote(desc), "(%s %s)" % (quote('value.shared'), value) ) else: typ, dat = self._simple_command('SETANNOTATION', mailbox, quote(desc), "(%s %s)" % (quote('value.priv'), value) ) return self._untagged_response(typ, dat, 'ANNOTATION') def setquota(self, mailbox, limit): """Set quota of a mailbox""" if limit == 0: quota = '()' else: quota = '(STORAGE %s)' % limit return self._simple_command('SETQUOTA', mailbox, quota) ### Overridden to support partition ### Pychecker will complain about non matching signature def create(self, mailbox, partition=None): """Create a mailbox, partition is optional""" if partition is not None: return self._simple_command('CREATE', mailbox, partition) else: return self._simple_command('CREATE', mailbox) ### Overridden to support partition ### Pychecker: same here def rename(self, from_mailbox, to_mailbox, partition=None): """Rename a from_mailbox to to_mailbox, partition is optional""" if partition is not None: return self._simple_command('RENAME', from_mailbox, to_mailbox, partition) else: return self._simple_command('RENAME', from_mailbox, to_mailbox) def reconstruct(self, mailbox): return self._simple_command('RECONSTRUCT', mailbox) def login_plain(self, admin, password, asUser): if asUser: encoded = b2a_base64("%s\0%s\0%s" % (asUser, admin, password)).strip() else: encoded = b2a_base64("%s\0%s\0%s" % (admin, admin, password)).strip() res, data = self._simple_command('AUTHENTICATE', 'PLAIN', encoded) self.AUTH = True if ok(res): self.state = 'AUTH' return res, data class CYRUS: ERROR = {} ERROR["CONNECT"] = [0, "Connection error"] ERROR["INVALID_URL"] = [1, "Invalid URL"] ERROR["ENCODING"] = [3, "Invalid encondig"] ERROR["MBXNULL"] = [5, "Mailbox is Null"] ERROR["NOAUTH"] = [7, "Connection is not authenticated"] ERROR["LOGIN"] = [10, "User or password is wrong"] ERROR["ADMIN"] = [11, "User is not cyrus administrator"] ERROR["AUTH"] = [12, "Connection already authenticated"] ERROR["LOGINPLAIN"] = [15, "Encryption needed to use mechanism"] ERROR["LOGIN_PLAIN"] = [16, "User or password is wrong"] ERROR["CREATE"] = [20, "Unable create mailbox"] ERROR["DELETE"] = [25, "Unable delete mailbox"] ERROR["GETACL"] = [30, "Unable parse GETACL result"] ERROR["SETQUOTA"] = [40, "Invalid integer argument"] ERROR["GETQUOTA"] = [45, "Quota root does not exist"] ERROR["RENAME"] = [50, "Unable rename mailbox"] ERROR["RECONSTRUCT"] = [60, "Unable reconstruct mailbox"] ERROR["SUBSCRIBE"] = [70, "User is cyrus administrator, normal user required"] ERROR["UNSUBSCRIBE"] = [75, "User is cyrus administrator, normal user required"] ERROR["LSUB"] = [77, "User is cyrus administrator, normal user required"] ERROR["UNKCMD"] = [98, "Command not implemented"] ERROR["IMAPLIB"] = [99, "Generic imaplib error"] ENCODING_LIST = ['imap', 'utf-8', 'iso-8859-1'] def __init__(self, url = 'imap://localhost:143'): self.VERBOSE = False self.AUTH = False self.ADMIN = None self.AUSER = None self.ADMINACL = 'c' self.SEP = DEFAULT_SEP self.ENCODING = 'imap' self.LOGFD = stdout match = re_url.match(url) if match: host = match.group(2) if match.group(3): port = int(match.group(3)) else: port = 143 else: self.__doraise("INVALID_URL") try: if match.group(1) == 'imap': self.ssl = False self.m = IMAP4(host, port) else: self.ssl = True self.m = IMAP4_SSL(host, port) except: self.__doraise("CONNECT") def __del__(self): if self.AUTH: self.logout() def __verbose(self, msg): if self.VERBOSE: print(msg, file=self.LOGFD) def __doexception(self, function, msg=None, *args): if msg is None: try: msg = self.ERROR.get(function.upper())[1] except: msg = self.ERROR.get("IMAPLIB")[1] value = "" for arg in args: if arg is not None: value = "%s %s" % (value, arg) self.__verbose( '[%s%s] %s: %s' % (function.upper(), value, "BAD", msg) ) self.__doraise( function.upper(), msg ) def __doraise(self, mode, msg=None): idError = self.ERROR.get(mode) if idError: if msg is None: msg = idError[1] else: idError = [self.ERROR.get("IMAPLIB")[0]] raise CYRUSError( idError[0], mode, msg ) def __prepare(self, command, mailbox=True): if not self.AUTH: self.__doexception(command, self.ERROR.get("NOAUTH")[1]) elif not mailbox: self.__doexception(command, self.ERROR.get("MBXNULL")[1]) def __docommand(self, function, *args): wrapped = getattr(self.m, function, None) if wrapped is None: raise self.__doraise("UNKCMD") try: res, msg = wrapped(*args) if ok(res): return res, msg except Exception as info: error = str(info).split(':').pop().strip() if error.upper().startswith('BAD'): error = error.split('BAD', 1).pop().strip() error = unquote(error[1:-1], '\'') self.__doexception(function, error, *args) self.__doexception(function, msg[0], *args) def xfer(self, mailbox, server): """Xfer a mailbox to server""" return self.m._simple_command('XFER', mailbox, server) def id(self): self.__prepare('id') res, data = self.m.id() data = data.strip() if not res or (len(data) < 3): return False, {} data = data[1:-1] # Strip () res, rdata = res2dict(data) if not res: self.__verbose( '[ID] Umatched pairs in result' ) return res, rdata def login(self, username, password): if self.AUTH: self.__doexception("LOGIN", self.ERROR.get("AUTH")[1]) try: res, msg = self.m.login(username, password) self.AUTH = True self.id() admin = self.m.isadmin() except Exception as info: self.AUTH = False error = str(info).split(':').pop().strip() self.__doexception("LOGIN", error) if admin: self.ADMIN = username self.SEP = self.m.getsep() self.__verbose( '[LOGIN %s] %s: %s' % (username, res, msg[0]) ) def login_plain(self, username, password, asUser = None): if self.AUTH: self.__doexception("LOGINPLAIN", self.ERROR.get("AUTH")[1]) if not self.ssl: self.__doexception("LOGINPLAIN", self.ERROR.get("LOGINPLAIN")[1]) res, msg = self.__docommand("login_plain", username, password, asUser) self.__verbose( '[AUTHENTICATE PLAIN %s] %s: %s' % (username, res, msg[0]) ) if ok(res): self.AUTH = True self.id() if asUser is None: if self.m.isadmin(): self.ADMIN = admin else: self.ADMIN = asUser self.AUSER = asUser self.SEP = self.m.getsep() def logout(self): try: res, msg = self.m.logout() except Exception as info: error = str(info).split(':').pop().strip() self.__doexception("LOGOUT", error) self.AUTH = False self.ADMIN = None self.AUSER = None self.__verbose( '[LOGOUT] %s: %s' % (res, msg[0]) ) def getEncoding(self): """Get current input/output codification""" return self.ENCODING def setEncoding(self, enc = None): """Set current input/output codification""" if enc is None: self.ENCODING = 'imap' elif enc in self.ENCODING_LIST: self.ENCODING = enc else: raise self.__doraise("ENCODING") def __encode(self, text): if re.search("&", text): text = re.sub("/", "+AC8-", text) text = re.sub("&", "+", text) text = unicode(text, 'utf-7').encode(self.ENCODING) return text def encode(self, text): if self.ENCODING == 'imap': return text elif self.ENCODING in self.ENCODING_LIST: return self.__encode(text) def __decode(self, text): text = re.sub("/", "-&", text) text = re.sub(" ", "-@", text) text = unicode(text, self.ENCODING).encode('utf-7') text = re.sub("-@", " ", text) text = re.sub("-&", "/", text) text = re.sub("\+", "&", text) return text def decode(self, text): if self.ENCODING == 'imap': return text elif self.ENCODING in self.ENCODING_LIST: return self.__decode(text) def lm(self, pattern="*"): """ List mailboxes, returns dict with list of mailboxes To list all mailboxes lm() To list users top mailboxes lm("user/%") To list all users mailboxes lm("user/*") To list users mailboxes startwith a word lm("user/word*") To list global top folders lm("%") To list global startwith a word unsupported by server suggestion lm("word*") """ self.__prepare('LIST') if pattern == '': pattern = "*" if pattern == '%': res, ml = self.__docommand('list', '', '%') else: res, ml = self.__docommand('list', '""', self.decode(pattern)) if not ok(res): self.__verbose( '[LIST] %s: %s' % (res, ml) ) return [] if (len(ml) == 1) and ml[0] is None: self.__verbose( '[LIST] No results' ) return [] mb = [] for mailbox in ml: res = re_mb.match(mailbox) if res is None: continue mbe = unquote(res.group(2)) if 'Noselect' in getflags(res.group(1)): continue mb.append(self.encode(mbe)) return mb def cm(self, mailbox, partition=None): """Create mailbox""" self.__prepare('CREATE', mailbox) res, msg = self.__docommand('create', self.decode(mailbox), partition) self.__verbose( '[CREATE %s partition=%s] %s: %s' % (mailbox, partition, res, msg[0]) ) def __dm(self, mailbox): if not mailbox: return True self.__docommand("setacl", self.decode(mailbox), self.ADMIN, self.ADMINACL) res, msg = self.__docommand("delete", self.decode(mailbox)) self.__verbose( '[DELETE %s] %s: %s' % (mailbox, res, msg[0]) ) def dm(self, mailbox, recursive=True): """Delete mailbox""" self.__prepare('DELETE', mailbox) mbxTmp = mailbox.split(self.SEP) # Cyrus is not recursive for user subfolders and global folders if (recursive and mbxTmp[0] != "user") or (len(mbxTmp) > 2): mbxList = self.lm("%s%s*" % (mailbox, self.SEP)) mbxList.reverse() for mbox in mbxList: self.__dm(mbox) self.__dm(mailbox) def rename(self, fromMbx, toMbx, partition=None): """Rename or change partition""" self.__prepare('RENAME', fromMbx) # Rename is recursive! Amen! res, msg = self.__docommand("rename", self.decode(fromMbx), self.decode(toMbx), partition) self.__verbose( '[RENAME %s %s] %s: %s' % (fromMbx, toMbx, res, msg[0]) ) def lam(self, mailbox): """List ACLs""" self.__prepare('GETACL', mailbox) res, acl = self.__docommand("getacl", self.decode(mailbox)) acls = {} aclList = splitquote(acl.pop().strip()) del aclList[0] # mailbox for i in range(0, len(aclList), 2): try: userid = self.encode(aclList[i]) rights = aclList[i + 1] except Exception as info: self.__verbose( '[GETACL %s] BAD: %s' % (mailbox, info.args[0]) ) raise self.__doraise("GETACL") self.__verbose( '[GETACL %s] %s %s' % (mailbox, userid, rights) ) acls[userid] = rights return acls def sam(self, mailbox, userid, rights): """Set ACL""" self.__prepare('SETACL', mailbox) res, msg = self.__docommand("setacl", self.decode(mailbox), userid, rights) self.__verbose( '[SETACL %s %s %s] %s: %s' % (mailbox, userid, rights, res, msg[0]) ) def lq(self, mailbox): """List Quota""" self.__prepare('GETQUOTA', mailbox) res, msg = self.__docommand("getquota", self.decode(mailbox)) match = re_q0.match(msg[0]) if match: self.__verbose( '[GETQUOTA %s] QUOTA (Unlimited)' % mailbox ) return None, None match = re_q.match(msg[0]) if match is None: self.__verbose( '[GETQUOTA %s] BAD: RegExp not matched, please report' % mailbox ) return None, None try: used = int(match.group(2)) quota = int(match.group(3)) self.__verbose( '[GETQUOTA %s] %s: QUOTA (%d/%d)' % (mailbox, res, used, quota) ) return used, quota except: self.__verbose( '[GETQUOTA %s] BAD: Error while parsing results' % mailbox ) return None, None def lqr(self, mailbox): """List Quota Root""" self.__prepare('GETQUOTAROOT', mailbox) res, msg = self.__docommand("getquotaroot", self.decode(mailbox)) (_mailbox, _root) = msg[0][0].split() match = re_q0.match(msg[1][0]) if match: self.__verbose( '[GETQUOTAROOT %s] QUOTAROOT (Unlimited)' % mailbox ) return _root, None, None match = re_q.match(msg[1][0]) try: used = int(match.group(2)) quota = int(match.group(3)) self.__verbose( '[GETQUOTAROOT %s] %s: QUOTA (%d/%d)' % (mailbox, res, used, quota) ) return _root, used, quota except: self.__verbose( '[GETQUOTAROOT %s] BAD: Error while parsing results' % mailbox ) return _root, None, None def sq(self, mailbox, limit): """Set Quota""" self.__prepare('SETQUOTA', mailbox) try: limit = int(limit) except ValueError: self.__verbose( '[SETQUOTA %s] BAD: %s %s' % (mailbox, self.ERROR.get("SETQUOTA")[1], limit) ) raise self.__doraise("SETQUOTA") res, msg = self.__docommand("setquota", self.decode(mailbox), limit) self.__verbose( '[SETQUOTA %s %s] %s: %s' % (mailbox, limit, res, msg[0]) ) def getannotation(self, mailbox, pattern='*'): """Get Annotation""" self.__prepare('GETANNOTATION') res, data = self.__docommand('getannotation', self.decode(mailbox), pattern) if (len(data) == 1) and data[0] is None: self.__verbose( '[GETANNOTATION %s] No results' % (mailbox) ) return {} ann = {} annotations = [] empty_values = [ "NIL", '" "', None, '', ' ' ] concat_items = [] for item in data: if isinstance(item, tuple): item = ' '.join([str(x) for x in item]) if len(concat_items) > 0: concat_items.append(item) if ''.join(concat_items).count('(') == ''.join(concat_items).count(')'): annotations.append(''.join(concat_items)) concat_items = [] continue else: if item.count('(') == item.count(')'): annotations.append(item) continue else: concat_items.append(item) continue for annotation in annotations: annotation = annotation.strip() if not annotation[0] == '"': folder = annotation.split('"')[0].replace('"','').strip() key = annotation.split('"')[1].replace('"','').replace("'","").strip() _annot = annotation.split('(')[1].split(')')[0].strip() else: folder = annotation.split('"')[1].replace('"','').strip() key = annotation.split('"')[3].replace('"','').replace("'","").strip() _annot = annotation.split('(')[1].split(')')[0].strip() - if not ann.has_key(folder): + if folder not in ann: ann[folder] = {} try: value_priv = _annot[(_annot.index('"value.priv"')+len('"value.priv"')):_annot.index('"size.priv"')].strip() except ValueError: value_priv = None try: size_priv = _annot[(_annot.index('"size.priv"')+len('"size.priv"')):].strip().split('"')[1].strip() try: value_priv = value_priv[value_priv.index('{%s}' % (size_priv))+len('{%s}' % (size_priv)):].strip() except Exception: pass except Exception: pass if value_priv in empty_values: value_priv = None else: try: value_priv = value_priv[:value_priv.index('"content-type.priv"')].strip() except: pass try: value_priv = value_priv[:value_priv.index('"modifiedsince.priv"')].strip() except: pass if value_priv.startswith('"'): value_priv = value_priv[1:] if value_priv.endswith('"'): value_priv = value_priv[:-1] if value_priv in empty_values: value_priv = None try: value_shared = _annot[(_annot.index('"value.shared"')+len('"value.shared"')):_annot.index('"size.shared"')].strip() except ValueError: value_shared = None try: size_shared = _annot[(_annot.index('"size.shared"')+len('"size.shared"')):].strip().split('"')[1].strip() try: value_shared = value_shared[value_shared.index('{%s}' % (size_shared))+len('{%s}' % (size_shared)):].strip() except Exception: pass except Exception: pass if value_shared in empty_values: value_shared = None else: try: value_shared = value_shared[:value_shared.index('"content-type.shared"')].strip() except: pass try: value_shared = value_shared[:value_shared.index('"modifiedsince.shared"')].strip() except: pass if value_shared.startswith('"'): value_shared = value_shared[1:] if value_shared.endswith('"'): value_shared = value_shared[:-1] if value_shared in empty_values: value_shared = None if not value_priv == None: ann[folder]['/private' + key] = value_priv if not value_shared == None: ann[folder]['/shared' + key] = value_shared return ann def setannotation(self, mailbox, annotation, value, shared=False): """Set Annotation""" self.__prepare('SETANNOTATION') res, msg = self.__docommand("setannotation", self.decode(mailbox), annotation, value, shared) self.__verbose( '[SETANNOTATION %s] %s: %s' % (mailbox, res, msg[0]) ) def __reconstruct(self, mailbox): if not mailbox: return True res, msg = self.__docommand("reconstruct", self.decode(mailbox)) self.__verbose( '[RECONSTRUCT %s] %s: %s' % (mailbox, res, msg[0]) ) def reconstruct(self, mailbox, recursive=True): """Reconstruct""" self.__prepare('RECONSTRUCT', mailbox) # Cyrus is not recursive for remote reconstruct if recursive: mbxList = self.lm("%s%s*" % (mailbox, self.SEP)) mbxList.reverse() for mbox in mbxList: self.__reconstruct(mbox) self.__reconstruct(mailbox) def lsub(self, pattern="*"): if self.AUSER is None: self.__doexception("lsub") self.__prepare('LSUB') if pattern == '': pattern = "*" res, ml = self.__docommand('lsub', '*', pattern) if not ok(res): self.__verbose( '[LIST] %s: %s' % (res, ml) ) return [] if (len(ml) == 1) and ml[0] is None: self.__verbose( '[LIST] No results' ) return [] mb = [] for mailbox in ml: res = re_mb.match(mailbox) if res is None: continue mbe = unquote(res.group(2)) if 'Noselect' in getflags(res.group(1)): continue mb.append(self.encode(mbe)) return mb def subscribe(self, mailbox): """Subscribe""" self.__prepare('SUBSCRIBE') res, msg = self.__docommand("subscribe", self.decode(mailbox)) self.__verbose( '[SUBSCRIBE %s] %s: %s' % (mailbox, res, msg[0]) ) def unsubscribe(self, mailbox): """Unsubscribe""" self.__prepare('UNSUBSCRIBE') res, msg = self.__docommand("unsubscribe", self.decode(mailbox)) self.__verbose( '[UNSUBSCRIBE %s] %s: %s' % (mailbox, res, msg[0]) ) diff --git a/ext/python/Tools/freeze/checkextensions.py b/ext/python/Tools/freeze/checkextensions.py index 078f5d7..abffe18 100644 --- a/ext/python/Tools/freeze/checkextensions.py +++ b/ext/python/Tools/freeze/checkextensions.py @@ -1,90 +1,90 @@ # Check for a module in a set of extension directories. # An extension directory should contain a Setup file # and one or more .o files or a lib.a file. import os import parsesetup def checkextensions(unknown, extensions): files = [] modules = [] edict = {} for e in extensions: setup = os.path.join(e, 'Setup') liba = os.path.join(e, 'lib.a') if not os.path.isfile(liba): liba = None edict[e] = parsesetup.getsetupinfo(setup), liba for mod in unknown: for e in extensions: (mods, vars), liba = edict[e] - if not mods.has_key(mod): + if mod not in mods: continue modules.append(mod) if liba: # If we find a lib.a, use it, ignore the # .o files, and use *all* libraries for # *all* modules in the Setup file if liba in files: break files.append(liba) for m in mods: files = files + select(e, mods, vars, m, 1) break files = files + select(e, mods, vars, mod, 0) break return files, modules def select(e, mods, vars, mod, skipofiles): files = [] for w in mods[mod]: w = treatword(w) if not w: continue w = expandvars(w, vars) for w in w.split(): if skipofiles and w[-2:] == '.o': continue # Assume $var expands to absolute pathname if w[0] not in ('-', '$') and w[-2:] in ('.o', '.a'): w = os.path.join(e, w) if w[:2] in ('-L', '-R') and w[2:3] != '$': w = w[:2] + os.path.join(e, w[2:]) files.append(w) return files cc_flags = ['-I', '-D', '-U'] cc_exts = ['.c', '.C', '.cc', '.c++'] def treatword(w): if w[:2] in cc_flags: return None if w[:1] == '-': return w # Assume loader flag head, tail = os.path.split(w) base, ext = os.path.splitext(tail) if ext in cc_exts: tail = base + '.o' w = os.path.join(head, tail) return w def expandvars(str, vars): i = 0 while i < len(str): i = k = str.find('$', i) if i < 0: break i = i+1 var = str[i:i+1] i = i+1 if var == '(': j = str.find(')', i) if j < 0: break var = str[i:j] i = j+1 - if vars.has_key(var): + if var in vars: str = str[:k] + vars[var] + str[i:] i = k return str diff --git a/pykolab/auth/ldap/syncrepl.py b/pykolab/auth/ldap/syncrepl.py index 5b69c63..6753605 100644 --- a/pykolab/auth/ldap/syncrepl.py +++ b/pykolab/auth/ldap/syncrepl.py @@ -1,119 +1,119 @@ #!/usr/bin/python import anydbm import ldap import ldap.syncrepl import ldapurl import pykolab from pykolab import utils log = pykolab.getLogger('pykolab.syncrepl') conf = pykolab.getConf() class DNSync(ldap.ldapobject.LDAPObject,ldap.syncrepl.SyncreplConsumer): callback = None def __init__(self, filename, *args, **kwargs): - if kwargs.has_key('callback'): + if 'callback' in kwargs: self.callback = kwargs['callback'] del kwargs['callback'] ldap.ldapobject.LDAPObject.__init__(self, *args, **kwargs) self.__db = anydbm.open(filename, 'c', 0o640) self.__presentUUIDs = {} def syncrepl_set_cookie(self,cookie): self.__db['cookie'] = cookie def syncrepl_get_cookie(self): if 'cookie' in self.__db: return self.__db['cookie'] def syncrepl_delete(self, uuids): log.debug("syncrepl_delete uuids: %r" % (uuids), level=8) # Get the unique_attribute name to issue along with our # callback (if any) unique_attr = conf.get('ldap', 'unique_attribute') if unique_attr == None: unique_attr = 'entryuuid' if unique_attr == 'nsuniqueid': log.warning( _("The name of the persistent, unique attribute " + \ "is very probably not compatible with the use of " + \ "syncrepl.") ) for uuid in uuids: dn = self.__db[uuid] log.debug("syncrepl_delete dn: %r" % (dn), level=8) if not self.callback == None: self.callback( change_type='delete', previous_dn=None, change_number=None, dn=dn, entry={ unique_attr: uuid } ) del self.__db[uuid] def syncrepl_present(self, uuids, refreshDeletes=False): if uuids is None: if refreshDeletes is False: nonpresent = [] for uuid in self.__db: if uuid == 'cookie': continue if uuid in self.__presentUUIDs: continue nonpresent.append(uuid) self.syncrepl_delete(nonpresent) self.__presentUUIDs = {} else: for uuid in uuids: self.__presentUUIDs[uuid] = True def syncrepl_entry(self, dn, attrs, uuid): attrs = utils.normalize(attrs) if uuid in self.__db: odn = self.__db[uuid] if odn != dn: if not self.callback == None: self.callback( change_type='moddn', previous_dn=odn, change_number=None, dn=dn, entry=attrs ) else: if not self.callback == None: self.callback( change_type='modify', previous_dn=None, change_number=None, dn=self.__db[uuid], entry=attrs ) else: if not self.callback == None: self.callback( change_type='add', previous_dn=None, change_number=None, dn=dn, entry=attrs ) self.__db[uuid] = dn diff --git a/pykolab/cli/__init__.py b/pykolab/cli/__init__.py index 4843aba..46c9930 100644 --- a/pykolab/cli/__init__.py +++ b/pykolab/cli/__init__.py @@ -1,76 +1,76 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import ldap import ldif import logging import traceback import shutil import sys import time import codecs import locale from ldap.modlist import addModlist import pykolab import pykolab.plugins from pykolab import utils from pykolab import conf from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() class Cli(object): def __init__(self): import commands commands.__init__() to_execute = [] arg_num = 0 for arg in sys.argv[1:]: arg_num += 1 if not arg.startswith('-') and len(sys.argv) >= arg_num: - if commands.commands.has_key(sys.argv[arg_num].replace('-','_')): + if sys.argv[arg_num].replace('-','_') in commands.commands: to_execute.append(sys.argv[arg_num].replace('-','_')) - if commands.commands.has_key("%s_%s" % ( + if "%s_%s" % ( '_'.join(to_execute),sys.argv[arg_num].replace('-','_') - )): + ) in commands.commands: to_execute.append(sys.argv[arg_num].replace('-','_')) for cmd_component in to_execute: sys.argv.pop(sys.argv.index(cmd_component.replace('_','-'))) # force default encoding to match the locale encoding (T249) reload(sys) sys.setdefaultencoding(locale.getpreferredencoding() or 'utf-8') # wrap sys.stdout in a locale-aware StreamWriter (#3983) sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) commands.execute('_'.join(to_execute)) def run(self): pass diff --git a/pykolab/cli/cmd_add_alias.py b/pykolab/cli/cmd_add_alias.py index 5daffe5..16fa590 100644 --- a/pykolab/cli/cmd_add_alias.py +++ b/pykolab/cli/cmd_add_alias.py @@ -1,131 +1,131 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys import commands import pykolab from pykolab.auth import Auth from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('add_alias', execute, description="Add alias.") def execute(*args, **kw): try: primary_rcpt_address = conf.cli_args.pop(0) try: secondary_rcpt_address = conf.cli_args.pop(0) except: print(_("Specify the (new) alias address"), file=sys.stderr) sys.exit(1) except: print(_("Specify the existing recipient address"), file=sys.stderr) sys.exit(1) if len(primary_rcpt_address.split('@')) > 1: primary_rcpt_domain = primary_rcpt_address.split('@')[-1] else: primary_rcpt_domain = conf.get('kolab', 'primary_domain') auth = Auth(domain=primary_rcpt_domain) domains = auth.list_domains() #print domains if len(secondary_rcpt_address.split('@')) > 1: secondary_rcpt_domain = secondary_rcpt_address.split('@')[-1] else: secondary_rcpt_domain = conf.get('kolab', 'primary_domain') # Check if either is in fact a domain if not primary_rcpt_domain.lower() in domains: print(_("Domain %r is not a local domain") % (primary_rcpt_domain), file=sys.stderr) sys.exit(1) if not secondary_rcpt_domain.lower() in domains: print(_("Domain %r is not a local domain") % (secondary_rcpt_domain), file=sys.stderr) sys.exit(1) if not primary_rcpt_domain == secondary_rcpt_domain: if not domains[primary_rcpt_domain] == domains[secondary_rcpt_domain]: print(_("Primary and secondary domain do not have the same parent domain"), file=sys.stderr) sys.exit(1) primary_recipient_dn = auth.find_recipient(primary_rcpt_address) if primary_recipient_dn == [] or len(primary_recipient_dn) == 0: print(_("No such recipient %r") % (primary_rcpt_address), file=sys.stderr) sys.exit(1) secondary_recipient_dn = auth.find_recipient(secondary_rcpt_address) if not secondary_recipient_dn == [] and not len(secondary_recipient_dn) == 0: print(_("Recipient for alias %r already exists") % (secondary_rcpt_address), file=sys.stderr) sys.exit(1) rcpt_attrs = conf.get_list('ldap', 'mail_attributes') primary_rcpt_attr = rcpt_attrs[0] if len(rcpt_attrs) >= 2: secondary_rcpt_attr = rcpt_attrs[1] else: print(_("Environment is not configured for " + \ "users to hold secondary mail attributes"), file=sys.stderr) sys.exit(1) primary_recipient = auth.get_entry_attributes(primary_rcpt_domain, primary_recipient_dn, rcpt_attrs) - if not primary_recipient.has_key(primary_rcpt_attr): + if primary_rcpt_attr not in primary_recipient: print(_("Recipient %r is not the primary recipient for address %r") % (primary_recipient, primary_rcpt_address), file=sys.stderr) sys.exit(1) - if not primary_recipient.has_key(secondary_rcpt_attr): + if secondary_rcpt_attr not in primary_recipient: auth.set_entry_attributes(primary_rcpt_domain, primary_recipient_dn, {secondary_rcpt_attr: [ secondary_rcpt_address ] }) else: if isinstance(primary_recipient[secondary_rcpt_attr], basestring): new_secondary_rcpt_attrs = [ primary_recipient[secondary_rcpt_attr], secondary_rcpt_address ] else: new_secondary_rcpt_attrs = \ primary_recipient[secondary_rcpt_attr] + \ [ secondary_rcpt_address ] auth.set_entry_attributes( primary_rcpt_domain, primary_recipient_dn, { secondary_rcpt_attr: new_secondary_rcpt_attrs } ) diff --git a/pykolab/cli/cmd_list_mailbox_metadata.py b/pykolab/cli/cmd_list_mailbox_metadata.py index af96cda..27d881c 100644 --- a/pykolab/cli/cmd_list_mailbox_metadata.py +++ b/pykolab/cli/cmd_list_mailbox_metadata.py @@ -1,97 +1,97 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_mailbox_metadata', execute, aliases='lmm', description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--user', dest = "user", action = "store", default = None, metavar = "USER", help = _("List annotations as user USER") ) def description(): return """Obtain a list of metadata entries on a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) except IndexError: folder = utils.ask_question(_("Folder name")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] elif not conf.user == None and len(conf.user.split('@')) > 1: domain = conf.user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() if not conf.user == None: imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, conf.user) else: imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: metadata = [] folders = imap.list_folders(folder) for folder in folders: print("Folder", folder) metadata = imap.get_metadata(folder) - if metadata.has_key(folder): + if folder in metadata: for annotation in metadata[folder]: print(" %-49s %s" % ( annotation, metadata[folder][annotation] )) diff --git a/pykolab/cli/cmd_sync.py b/pykolab/cli/cmd_sync.py index 7d44d02..2aaf9df 100644 --- a/pykolab/cli/cmd_sync.py +++ b/pykolab/cli/cmd_sync.py @@ -1,150 +1,150 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import commands from distutils import version import multiprocessing import sys import time import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() imap = None pool = None def __init__(): commands.register('sync', execute, description="Synchronize Kolab Users with IMAP.") def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--threads', dest = "threads", action = "store", default = 20, type = int, help = _("Synchronize LDAP and IMAP") ) my_option_group.add_option( '--resync', dest = "resync", action = "store_true", default = False, help = _("Resync from the beginning") ) my_option_group.add_option( '--domain', dest = "domain", action = "store", default = "all", help = _("Only sync the specified domain") ) def execute(*args, **kw): global imap, pool auth = Auth() if conf.domain == "all": log.debug(_("Listing domains..."), level=5) start_time = time.time() domains = auth.list_domains() end_time = time.time() log.debug( _("Found %d domains in %d seconds") % ( len(domains), (end_time-start_time) ), level=8 ) else: domains = {} domains[conf.domain] = conf.domain if version.StrictVersion(sys.version[:3]) >= version.StrictVersion("2.7"): pool = multiprocessing.Pool(conf.threads, worker_process, (), 1) else: pool = multiprocessing.Pool(conf.threads, worker_process, ()) for primary_domain in list(set(domains.values())): log.debug(_("Running for domain %s") % (primary_domain), level=8) auth = Auth(primary_domain) auth.connect(primary_domain) start_time = time.time() auth.synchronize(mode='_paged_search', callback=queue_add) end_time = time.time() log.info(_("Synchronizing users for %s took %d seconds") % (primary_domain, (end_time-start_time)) ) while not pool._taskqueue.empty(): time.sleep(1) def queue_add(*args, **kw): global pool for dn, entry in kw['entry']: entry['dn'] = dn r = pool.apply_async(_synchronize, (), dict(**entry)) r.wait() def worker_process(*args, **kw): pass def _synchronize(*args, **kw): log.info(_("Worker process %s handling %s") % (multiprocessing.current_process().name, kw['dn'])) entry = utils.normalize(kw) mailbox_attribute = conf.get('cyrus-sasl', 'result_attribute') if mailbox_attribute == None: mailbox_attribute = 'mail' if mailbox_attribute not in entry: return if not 'kolabinetorgperson' in entry['objectclass']: return imap = IMAP() imap.connect() if not imap.user_mailbox_exists(entry[mailbox_attribute]): - if entry.has_key('mailhost'): + if 'mailhost' in entry: server = entry['mailhost'] else: server = None imap.user_mailbox_create(entry[mailbox_attribute], server=server) imap.disconnect() diff --git a/pykolab/cli/commands.py b/pykolab/cli/commands.py index ba15202..a1e9b0c 100644 --- a/pykolab/cli/commands.py +++ b/pykolab/cli/commands.py @@ -1,201 +1,201 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import sys import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() commands = {} command_groups = {} def __init__(): # We only want the base path commands_base_path = os.path.dirname(__file__) for commands_path, dirnames, filenames in os.walk(commands_base_path): if not commands_path == commands_base_path: continue for filename in filenames: if filename.startswith('cmd_') and filename.endswith('.py'): module_name = filename.replace('.py','') cmd_name = module_name.replace('cmd_', '') #print "exec(\"from %s import __init__ as %s_register\"" % (module_name,cmd_name) try: exec("from %s import __init__ as %s_register" % (module_name,cmd_name)) except ImportError: pass exec("%s_register()" % (cmd_name)) for dirname in dirnames: register_group(commands_path, dirname) register('help', list_commands) register('delete_user', not_yet_implemented, description="Not yet implemented") register('list_groups', not_yet_implemented, description="Not yet implemented") register('add_group', not_yet_implemented, description="Not yet implemented") register('delete_group', not_yet_implemented, description="Not yet implemented") def list_commands(*args, **kw): """ List commands """ __commands = {} for command in commands: if isinstance(command, tuple): command_group, command = command __commands[command_group] = { command: commands[(command_group,command)] } else: __commands[command] = commands[command] _commands = __commands.keys() _commands.sort() for _command in _commands: - if __commands[_command].has_key('group'): + if 'group' in __commands[_command]: continue - if __commands[_command].has_key('function'): + if 'function' in __commands[_command]: # This is a top-level command if not __commands[_command]['description'] == None: print("%-25s - %s" % (_command.replace('_','-'),__commands[_command]['description'])) else: print("%-25s" % (_command.replace('_','-'))) for _command in _commands: - if not __commands[_command].has_key('function'): + if 'function' not in __commands[_command]: # This is a nested command print("\n" + _("Command Group: %s") % (_command) + "\n") ___commands = __commands[_command].keys() ___commands.sort() for __command in ___commands: if not __commands[_command][__command]['description'] == None: print("%-4s%-21s - %s" % ('',__command.replace('_','-'),__commands[_command][__command]['description'])) else: print("%-4s%-21s" % ('',__command.replace('_','-'))) def execute(cmd_name, *args, **kw): if cmd_name == "": execute("help") sys.exit(0) - if not commands.has_key(cmd_name): + if cmd_name not in commands: log.error(_("No such command.")) sys.exit(1) - if not commands[cmd_name].has_key('function') and \ - not commands[cmd_name].has_key('group'): + if 'function' not in commands[cmd_name] and \ + 'group' not in commands[cmd_name]: log.error(_("No such command.")) sys.exit(1) - if commands[cmd_name].has_key('group'): + if 'group' in commands[cmd_name]: group = commands[cmd_name]['group'] command_name = commands[cmd_name]['cmd_name'] try: exec("from %s.cmd_%s import cli_options as %s_%s_cli_options" % (group,command_name,group,command_name)) exec("%s_%s_cli_options()" % (group,command_name)) except ImportError: pass else: command_name = commands[cmd_name]['cmd_name'] try: exec("from cmd_%s import cli_options as %s_cli_options" % (command_name,command_name)) exec("%s_cli_options()" % (command_name)) except ImportError: pass conf.finalize_conf() commands[cmd_name]['function'](conf.cli_args, kw) def register_group(dirname, module): commands_base_path = os.path.join(os.path.dirname(__file__), module) commands[module] = {} for commands_path, dirnames, filenames in os.walk(commands_base_path): if not commands_path == commands_base_path: continue for filename in filenames: if filename.startswith('cmd_') and filename.endswith('.py'): module_name = filename.replace('.py','') cmd_name = module_name.replace('cmd_', '') #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" % (module,module_name,module,cmd_name) exec("from %s.%s import __init__ as %s_%s_register" % (module,module_name,module,cmd_name)) exec("%s_%s_register()" % (module,cmd_name)) def register(cmd_name, func, group=None, description=None, aliases=[]): if not group == None: command = "%s_%s" % (group,cmd_name) else: command = cmd_name if isinstance(aliases, basestring): aliases = [aliases] - if commands.has_key(command): + if command in commands: log.fatal(_("Command '%s' already registered") % (command)) sys.exit(1) if callable(func): if group == None: commands[cmd_name] = { 'cmd_name': cmd_name, 'function': func, 'description': description } else: commands[group][cmd_name] = { 'cmd_name': cmd_name, 'function': func, 'description': description } commands[command] = commands[group][cmd_name] commands[command]['group'] = group commands[command]['cmd_name'] = cmd_name for alias in aliases: commands[alias] = { 'cmd_name': cmd_name, 'function': func, 'description': _("Alias for %s") % (cmd_name.replace('_','-')) } ## ## Commands not yet implemented ## def not_yet_implemented(*args, **kw): print(_("Not yet implemented")) sys.exit(1) diff --git a/pykolab/cli/sieve/cmd_refresh.py b/pykolab/cli/sieve/cmd_refresh.py index b784eac..0f93208 100644 --- a/pykolab/cli/sieve/cmd_refresh.py +++ b/pykolab/cli/sieve/cmd_refresh.py @@ -1,420 +1,420 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.cli import commands from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() import sys import time from urlparse import urlparse def __init__(): commands.register('refresh', execute, group='sieve', description=description()) def description(): return """Refresh a user's managed and contributed sieve scripts.""" def execute(*args, **kw): try: address = conf.cli_args.pop(0) except: address = utils.ask_question(_("Email Address")) auth = Auth() auth.connect() user = auth.find_recipient(address) # Get the main, default backend backend = conf.get('kolab', 'imap_backend') if len(address.split('@')) > 1: domain = address.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'): backend = conf.get(domain, 'imap_backend') if conf.has_section(domain) and conf.has_option(domain, 'imap_uri'): uri = conf.get(domain, 'imap_uri') else: uri = conf.get(backend, 'uri') hostname = None port = None result = urlparse(uri) if hasattr(result, 'hostname'): hostname = result.hostname else: scheme = uri.split(':')[0] (hostname, port) = uri.split('/')[2].split(':') port = 4190 # Get the credentials admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') import sievelib.managesieve sieveclient = sievelib.managesieve.Client(hostname, port, conf.debuglevel > 8) sieveclient.connect(None, None, True) sieveclient._plain_authentication(admin_login, admin_password, address) sieveclient.authenticated = True result = sieveclient.listscripts() if result == None: active = None scripts = [] else: active, scripts = result log.debug(_("Found the following scripts for user %s: %s") % (address, ','.join(scripts)), level=8) log.debug(_("And the following script is active for user %s: %s") % (address, active), level=8) mgmt_required_extensions = [] mgmt_script = """# # MANAGEMENT # """ user = auth.get_entry_attributes(domain, user, ['*']) # # Vacation settings (a.k.a. Out of Office) # vacation_active = None vacation_text = None vacation_uce = None vacation_noreact_domains = None vacation_react_domains = None vacation_active_attr = conf.get('sieve', 'vacation_active_attr') vacation_text_attr = conf.get('sieve', 'vacation_text_attr') vacation_uce_attr = conf.get('sieve', 'vacation_uce_attr') vacation_noreact_domains_attr = conf.get('sieve', 'vacation_noreact_domains_attr') vacation_react_domains_attr = conf.get('sieve', 'vacation_react_domains_attr') if not vacation_text_attr == None: - if user.has_key(vacation_active_attr): + if vacation_active_attr in user: vacation_active = utils.true_or_false(user[vacation_active_attr]) else: vacation_active = False - if user.has_key(vacation_text_attr): + if vacation_text_attr in user: vacation_text = user[vacation_text_attr] else: vacation_active = False - if user.has_key(vacation_uce_attr): + if vacation_uce_attr in user: vacation_uce = utils.true_or_false(user[vacation_uce_attr]) else: vacation_uce = False - if user.has_key(vacation_react_domains_attr): + if vacation_react_domains_attr in user: if isinstance(user[vacation_react_domains_attr], list): vacation_react_domains = user[vacation_react_domains_attr] else: vacation_react_domains = [ user[vacation_react_domains_attr] ] else: - if user.has_key(vacation_noreact_domains_attr): + if vacation_noreact_domains_attr in user: if isinstance(user[vacation_noreact_domains_attr], list): vacation_noreact_domains = user[vacation_noreact_domains_attr] else: vacation_noreact_domains = [ user[vacation_noreact_domains_attr] ] else: vacation_noreact_domains = [] # # Delivery to Folder # dtf_active_attr = conf.get('sieve', 'deliver_to_folder_active') if not dtf_active_attr == None: - if user.has_key(dtf_active_attr): + if dtf_active_attr in user: dtf_active = utils.true_or_false(user[dtf_active_attr]) else: dtf_active = False else: # TODO: Not necessarily de-activated, the *Active attributes are # not supposed to charge this - check the deliver_to_folder_attr # attribute value for a value. dtf_active = False if dtf_active: dtf_folder_name_attr = conf.get('sieve', 'deliver_to_folder_attr') if not dtf_folder_name_attr == None: - if user.has_key(dtf_folder_name_attr): + if dtf_folder_name_attr in user: dtf_folder = user[dtf_folder_name_attr] else: log.warning(_("Delivery to folder active, but no folder name attribute available for user %r") % (user)) dtf_active = False else: log.error(_("Delivery to folder active, but no folder name attribute configured")) dtf_active = False # # Folder name to delivery spam to. # # Global or local. # sdf_filter = True sdf = conf.get('sieve', 'spam_global_folder') if sdf == None: sdf = conf.get('sieve', 'spam_personal_folder') if sdf == None: sdf_filter = False # # Mail forwarding # forward_active = None forward_addresses = [] forward_keepcopy = None forward_uce = None forward_active_attr = conf.get('sieve', 'forward_address_active') if not forward_active_attr == None: - if user.has_key(forward_active_attr): + if forward_active_attr in user: forward_active = utils.true_or_false(user[forward_active_attr]) else: forward_active = False if not forward_active == False: forward_address_attr = conf.get('sieve', 'forward_address_attr') - if user.has_key(forward_address_attr): + if forward_address_attr in user: if isinstance(user[forward_address_attr], basestring): forward_addresses = [ user[forward_address_attr] ] elif isinstance(user[forward_address_attr], str): forward_addresses = [ user[forward_address_attr] ] else: forward_addresses = user[forward_address_attr] if len(forward_addresses) == 0: forward_active = False forward_keepcopy_attr = conf.get('sieve', 'forward_keepcopy_active') if not forward_keepcopy_attr == None: - if user.has_key(forward_keepcopy_attr): + if forward_keepcopy_attr in user: forward_keepcopy = utils.true_or_false(user[forward_keepcopy_attr]) else: forward_keepcopy = False forward_uce_attr = conf.get('sieve', 'forward_uce_active') if not forward_uce_attr == None: - if user.has_key(forward_uce_attr): + if forward_uce_attr in user: forward_uce = utils.true_or_false(user[forward_uce_attr]) else: forward_uce = False if vacation_active: mgmt_required_extensions.append('vacation') mgmt_required_extensions.append('envelope') if dtf_active: mgmt_required_extensions.append('fileinto') if forward_active and (len(forward_addresses) > 1 or forward_keepcopy): mgmt_required_extensions.append('copy') if sdf_filter: mgmt_required_extensions.append('fileinto') import sievelib.factory mgmt_script = sievelib.factory.FiltersSet("MANAGEMENT") for required_extension in mgmt_required_extensions: mgmt_script.require(required_extension) mgmt_script.require('fileinto') if vacation_active: if not vacation_react_domains == None and len(vacation_react_domains) > 0: mgmt_script.addfilter( 'vacation', [('envelope', ':domain', ":is", "from", vacation_react_domains)], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) elif not vacation_noreact_domains == None and len(vacation_noreact_domains) > 0: mgmt_script.addfilter( 'vacation', [('not', ('envelope', ':domain', ":is", "from", vacation_noreact_domains))], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) else: mgmt_script.addfilter( 'vacation', [('true',)], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) if forward_active: forward_rules = [] # Principle can be demonstrated by: # # python -c "print ','.join(['a','b','c'][:-1])" # for forward_copy in forward_addresses[:-1]: forward_rules.append(("redirect", ":copy", forward_copy)) if forward_keepcopy: # Principle can be demonstrated by: # # python -c "print ','.join(['a','b','c'][-1])" # if forward_uce: rule_name = 'forward-uce-keepcopy' else: rule_name = 'forward-keepcopy' forward_rules.append(("redirect", ":copy", forward_addresses[-1])) else: if forward_uce: rule_name = 'forward-uce' else: rule_name = 'forward' forward_rules.append(("redirect", forward_addresses[-1])) forward_rules.append(("stop")) if forward_uce: mgmt_script.addfilter(rule_name, ['true'], forward_rules) else: # NOTE: Messages with no X-Spam-Status header need to be matched # too, and this does exactly that. mgmt_script.addfilter(rule_name, [("not", ("X-Spam-Status", ":matches", "Yes,*"))], forward_rules) if sdf_filter: mgmt_script.addfilter('spam_delivery_folder', [("X-Spam-Status", ":matches", "Yes,*")], [("fileinto", "INBOX/Spam"), ("stop")]) if dtf_active: mgmt_script.addfilter('delivery_to_folder', ['true'], [("fileinto", dtf_folder)]) mgmt_script = mgmt_script.__str__() log.debug(_("MANAGEMENT script for user %s contents: %r") % (address,mgmt_script), level=8) result = sieveclient.putscript("MANAGEMENT", mgmt_script) if not result: log.error(_("Uploading script MANAGEMENT failed for user %s") % (address)) else: log.debug(_("Uploading script MANAGEMENT for user %s succeeded") % (address), level=8) user_script = """# # User # require ["include"]; """ for script in scripts: if not script in [ "MASTER", "MANAGEMENT", "USER" ]: log.debug(_("Including script %s in USER (for user %s)") % (script,address) ,level=8) user_script = """%s include :personal "%s"; """ % (user_script, script) result = sieveclient.putscript("USER", user_script) if not result: log.error(_("Uploading script USER failed for user %s") % (address)) else: log.debug(_("Uploading script USER for user %s succeeded") % (address), level=8) result = sieveclient.putscript("MASTER", """# # MASTER # # This file is authoritative for your system and MUST BE KEPT ACTIVE. # # Altering it is likely to render your account dysfunctional and may # be violating your organizational or corporate policies. # # For more information on the mechanism and the conventions behind # this script, see http://wiki.kolab.org/KEP:14 # require ["include"]; # OPTIONAL: Includes for all or a group of users # include :global "all-users"; # include :global "this-group-of-users"; # The script maintained by the general management system include :personal "MANAGEMENT"; # The script(s) maintained by one or more editors available to the user include :personal "USER"; """) if not result: log.error(_("Uploading script MASTER failed for user %s") % (address)) else: log.debug(_("Uploading script MASTER for user %s succeeded") % (address), level=8) sieveclient.setactive("MASTER") diff --git a/pykolab/imap/cyrus.py b/pykolab/imap/cyrus.py index 26e8f98..765b0ee 100644 --- a/pykolab/imap/cyrus.py +++ b/pykolab/imap/cyrus.py @@ -1,631 +1,631 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import cyruslib import sys import time from urlparse import urlparse import pykolab from pykolab import constants from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.imap') conf = pykolab.getConf() class Cyrus(cyruslib.CYRUS): """ Abstraction class for some common actions to do exclusively in Cyrus. For example, the following functions require the commands to be executed against the backend server if a murder is being used. - Setting quota - Renaming the top-level mailbox - Setting annotations """ setquota = cyruslib.CYRUS.sq def __init__(self, uri): """ Initialize this class, but do not connect yet. """ port = None result = urlparse(uri) if hasattr(result, 'hostname'): scheme = result.scheme hostname = result.hostname port = result.port else: scheme = uri.split(':')[0] (hostname, port) = uri.split('/')[2].split(':') if not port: if scheme == 'imap': port = 143 else: port = 993 self.server = hostname self.uri = "%s://%s:%s" % (scheme, hostname, port) while 1: try: cyruslib.CYRUS.__init__(self, self.uri) break except cyruslib.CYRUSError: log.warning( _("Could not connect to Cyrus IMAP server %r") % ( self.uri ) ) time.sleep(10) if conf.debuglevel > 8: self.VERBOSE = True self.m.debug = 5 sl = pykolab.logger.StderrToLogger(log) # imaplib debug outputs everything to stderr. Redirect to Logger sys.stderr = sl # cyruslib debug outputs everything to LOGFD. Redirect to Logger self.LOGFD = sl # Initialize our variables self.separator = self.SEP # Placeholder for known mailboxes on known servers self.mbox = {} def __del__(self): pass def connect(self, uri): """ Dummy connect function that checks if the server that we want to connect to is actually the server we are connected to. Uses pykolab.imap.IMAP.connect() in the background. """ port = None result = urlparse(uri) if hasattr(result, 'hostname'): scheme = result.scheme hostname = result.hostname port = result.port else: scheme = uri.split(':')[0] (hostname, port) = uri.split('/')[2].split(':') if not port: if scheme == 'imap': port = 143 else: port = 993 if hostname == self.server: return imap = IMAP() imap.connect(uri=uri) if not self.SEP == self.separator: self.separator = self.SEP def login(self, *args, **kw): """ Login to the Cyrus IMAP server through cyruslib.CYRUS, but set our hierarchy separator. """ try: cyruslib.CYRUS.login(self, *args, **kw) except cyruslib.CYRUSError as errmsg: log.error("Login to Cyrus IMAP server failed: %r", errmsg) except Exception as errmsg: log.exception(errmsg) self.separator = self.SEP try: self._id() except Exception: pass log.debug( _("Continuing with separator: %r") % (self.separator), level=8 ) self.murder = False for capability in self.m.capabilities: if capability.startswith("MUPDATE="): log.debug( _("Detected we are running in a Murder topology"), level=8 ) self.murder = True if not self.murder: log.debug( _("This system is not part of a murder topology"), level=8 ) def find_mailfolder_server(self, mailfolder): annotations = {} _mailfolder = self.parse_mailfolder(mailfolder) prefix = _mailfolder['path_parts'][0] mbox = _mailfolder['path_parts'][1] if _mailfolder['domain'] is not None: mailfolder = "%s%s%s@%s" % ( prefix, self.separator, mbox, _mailfolder['domain'] ) # TODO: Workaround for undelete if len(self.lm(mailfolder)) < 1 and 'hex_timestamp' in _mailfolder: mailfolder = self.folder_utf7("DELETED/%s%s%s@%s" % ( self.separator.join(_mailfolder['path_parts']), self.separator, _mailfolder['hex_timestamp'], _mailfolder['domain']) ) # TODO: Murder capabilities may have been suppressed using Cyrus IMAP # configuration. if not self.murder: return self.server log.debug( _("Checking actual backend server for folder %s " + "through annotations") % ( mailfolder ), level=8 ) - if self.mbox.has_key(mailfolder): + if mailfolder in self.mbox: log.debug( _( "Possibly reproducing the find " + "mailfolder server answer from " + "previously detected and stored " + "annotation value: %r" ) % ( self.mbox[mailfolder] ), level=8 ) if not self.mbox[mailfolder] == self.server: return self.mbox[mailfolder] max_tries = 20 num_try = 0 ann_path = "/vendor/cmu/cyrus-imapd/server" s_ann_path = "/shared%s" % (ann_path) while 1: num_try += 1 annotations = self._getannotation( '"%s"' % (mailfolder), ann_path ) - if annotations.has_key(mailfolder): - if annotations[mailfolder].has_key(s_ann_path): + if mailfolder in annotations: + if s_ann_path in annotations[mailfolder]: break if max_tries <= num_try: log.error( _("Could not get the annotations after %s tries.") % ( num_try ) ) annotations = { mailfolder: { s_ann_path: self.server } } break log.warning( _("No annotations for %s: %r") % ( mailfolder, annotations ) ) time.sleep(1) server = annotations[mailfolder][s_ann_path] self.mbox[mailfolder] = server log.debug( _("Server for INBOX folder %s is %s") % ( mailfolder, server ), level=8 ) return server def folder_utf7(self, folder): from pykolab import imap_utf7 return imap_utf7.encode(folder) def folder_utf8(self, folder): from pykolab import imap_utf7 return imap_utf7.decode(folder) def _id(self, identity=None): if identity is None: identity = '("name" "Python/Kolab" "version" "%s")' % (constants.__version__) typ, dat = self.m._simple_command('ID', identity) res, dat = self.m._untagged_response(typ, dat, 'ID') def _setquota(self, mailfolder, quota): """ Login to the actual backend server. """ server = self.find_mailfolder_server(mailfolder) self.connect(self.uri.replace(self.server, server)) log.debug( _("Setting quota for folder %s to %s") % ( mailfolder, quota ), level=8 ) try: self.m.setquota(mailfolder, quota) except: log.error( _("Could not set quota for mailfolder %s") % ( mailfolder ) ) def _rename(self, from_mailfolder, to_mailfolder, partition=None): """ Login to the actual backend server, then rename. """ server = self.find_mailfolder_server(from_mailfolder) self.connect(self.uri.replace(self.server, server)) if partition is not None: log.debug( _("Moving INBOX folder %s to %s on partition %s") % ( from_mailfolder, to_mailfolder, partition ), level=8 ) else: log.debug( _("Moving INBOX folder %s to %s") % ( from_mailfolder, to_mailfolder ), level=8 ) self.m.rename( self.folder_utf7(from_mailfolder), self.folder_utf7(to_mailfolder), partition ) def _getannotation(self, *args, **kw): return self.getannotation(*args, **kw) def _setannotation(self, mailfolder, annotation, value, shared=False): """ Login to the actual backend server, then set annotation. """ try: server = self.find_mailfolder_server(mailfolder) except: server = self.server log.debug( _("Setting annotation %s on folder %s") % ( annotation, mailfolder ), level=8 ) try: self.setannotation(mailfolder, annotation, value, shared) except cyruslib.CYRUSError as errmsg: log.error( _("Could not set annotation %r on mail folder %r: %r") % ( annotation, mailfolder, errmsg ) ) def _xfer(self, mailfolder, current_server, new_server): self.connect(self.uri.replace(self.server, current_server)) log.debug( _("Transferring folder %s from %s to %s") % ( mailfolder, current_server, new_server ), level=8 ) self.xfer(mailfolder, new_server) def undelete_mailfolder( self, mailfolder, to_mailfolder=None, recursive=True ): """ Login to the actual backend server, then "undelete" the mailfolder. 'mailfolder' may be a string representing either of the following two options; - the fully qualified pathof the deleted folder in its current location, such as, for a deleted INBOX folder originally known as "user/userid[@domain]"; "DELETED/user/userid/hex[@domain]" - the original folder name, such as; "user/userid[@domain]" 'to_mailfolder' may be the target folder to "undelete" the deleted folder to. If not specified, the original folder name is used. """ # Placeholder for folders we have recovered already. target_folders = [] mailfolder = self.parse_mailfolder(mailfolder) undelete_folders = self._find_deleted_folder(mailfolder) if to_mailfolder is not None: target_mbox = self.parse_mailfolder(to_mailfolder) else: target_mbox = mailfolder for undelete_folder in undelete_folders: undelete_mbox = self.parse_mailfolder(undelete_folder) prefix = undelete_mbox['path_parts'].pop(0) mbox = undelete_mbox['path_parts'].pop(0) if to_mailfolder is None: target_folder = self.separator.join([prefix, mbox]) else: target_folder = self.separator.join(target_mbox['path_parts']) if to_mailfolder is not None: target_folder = "%s%s%s" % ( target_folder, self.separator, mbox ) if not len(undelete_mbox['path_parts']) == 0: target_folder = "%s%s%s" % ( target_folder, self.separator, self.separator.join(undelete_mbox['path_parts']) ) if target_folder in target_folders: target_folder = "%s%s%s" % ( target_folder, self.separator, undelete_mbox['hex_timestamp'] ) target_folders.append(target_folder) if target_mbox['domain'] is not None: target_folder = "%s@%s" % ( target_folder, target_mbox['domain'] ) log.info( _("Undeleting %s to %s") % ( undelete_folder, target_folder ) ) target_server = self.find_mailfolder_server(target_folder) source_server = self.find_mailfolder_server(undelete_folder) if hasattr(conf, 'dry_run') and not conf.dry_run: undelete_folder = self.folder_utf7(undelete_folder) target_folder = self.folder_utf7(target_folder) if not target_server == source_server: self.xfer(undelete_folder, target_server) self.rename(undelete_folder, target_folder) else: if not target_server == source_server: print(_("Would have transferred %s from %s to %s") % ( undelete_folder, source_server, target_server ), file=sys.stdout) print(_("Would have renamed %s to %s") % ( undelete_folder, target_folder ), file=sys.stdout) def parse_mailfolder(self, mailfolder): """ Parse a mailfolder name to it's parts. Takes a fully qualified mailfolder or mailfolder sub-folder. """ mbox = { 'domain': None } if len(mailfolder.split('/')) > 1: self.separator = '/' # Split off the virtual domain identifier, if any if len(mailfolder.split('@')) > 1: mbox['domain'] = mailfolder.split('@')[1] mbox['path_parts'] = mailfolder.split('@')[0].split(self.separator) else: mbox['path_parts'] = mailfolder.split(self.separator) # See if the path that has been specified is the current location for # the deleted folder, or the original location, we have to find the # deleted folder for. if not mbox['path_parts'][0] in ['user', 'shared']: deleted_prefix = mbox['path_parts'].pop(0) # See if the hexadecimal timestamp is actually hexadecimal. # This prevents "DELETED/user/userid/Sent", but not # "DELETED/user/userid/FFFFFF" from being specified. try: hexstamp = mbox['path_parts'][(len(mbox['path_parts'])-1)] epoch = int(hexstamp, 16) try: timestamp = time.asctime(time.gmtime(epoch)) except: return None except: return None # Verify that the input for the deleted folder is actually a # deleted folder. verify_folder_search = "%(dp)s%(sep)s%(mailfolder)s" % { 'dp': deleted_prefix, 'sep': self.separator, 'mailfolder': self.separator.join(mbox['path_parts']) } if mbox['domain'] is not None: verify_folder_search = "%s@%s" % ( verify_folder_search, mbox['domain'] ) if ' ' in verify_folder_search: folders = self.lm( '"%s"' % self.folder_utf7(verify_folder_search) ) else: folders = self.lm(self.folder_utf7(verify_folder_search)) # NOTE: Case also covered is valid hexadecimal folders; won't be # the actual check as intended, but doesn't give you anyone else's # data unless... See the following: # # TODO: Case not covered is usernames that are hexadecimal. # # We could probably attempt to convert the int(hex) into a # time.gmtime(), but it still would not cover all cases. # # If no folders were found... well... then there you go. if len(folders) < 1: return None # Pop off the hex timestamp, which turned out to be valid mbox['hex_timestamp'] = mbox['path_parts'].pop() return mbox def _find_deleted_folder(self, mbox): """ Give me the parts that are in an original mailfolder name and I'll find the deleted folder name. TODO: It finds virtdomain folders for non-virtdomain searches. """ deleted_folder_search = "%(deleted_prefix)s%(separator)s%(mailfolder)s%(separator)s*" % { # TODO: The prefix used is configurable 'deleted_prefix': "DELETED", 'mailfolder': self.separator.join(mbox['path_parts']), 'separator': self.separator, } if mbox['domain'] is not None: deleted_folder_search = "%s@%s" % ( deleted_folder_search, mbox['domain'] ) folders = self.lm(self.folder_utf7(deleted_folder_search)) # The folders we have found at this stage include virtdomain folders. # # For example, having searched for user/userid, it will also find # user/userid@example.org # # Here, we explicitly remove any virtdomain folders. if mbox['domain'] is None: _folders = [] for folder in folders: if len(folder.split('@')) < 2: _folders.append(folder) folders = _folders return [self.folder_utf8(x) for x in folders] diff --git a/pykolab/plugins/defaultfolders/__init__.py b/pykolab/plugins/defaultfolders/__init__.py index 0b6ee10..a4a59b0 100644 --- a/pykolab/plugins/defaultfolders/__init__.py +++ b/pykolab/plugins/defaultfolders/__init__.py @@ -1,56 +1,56 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.plugins.defaultfolders') conf = pykolab.getConf() class KolabDefaultfolders(object): """ Example plugin to create a set of default folders. """ def __init__(self): pass def add_options(self, *args, **kw): pass def create_user_folders(self, *args, **kw): """ The arguments passed to the 'create_user_folders' hook: additional_folders - additional folders to create user_folder - user folder """ - if not kw.has_key('additional_folders'): + if 'additional_folders' not in kw: log.error(_("Plugin %s called without required keyword %s.") % ("defaultfolders", "additional_folders")) return {} try: exec("additional_folders = %s" % (kw['additional_folders'])) except Exception: log.error(_("Could not parse additional_folders")) return {} return additional_folders diff --git a/pykolab/plugins/dynamicquota/__init__.py b/pykolab/plugins/dynamicquota/__init__.py index c9b9b7f..51cb547 100644 --- a/pykolab/plugins/dynamicquota/__init__.py +++ b/pykolab/plugins/dynamicquota/__init__.py @@ -1,81 +1,81 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab.translate import _ conf = pykolab.getConf() log = pykolab.getLogger('pykolab.plugins.dynamicquota') class KolabDynamicquota(object): """ Example plugin making quota adjustments given arbitrary conditions. """ def __init__(self): pass def add_options(self, *args, **kw): pass def set_user_folder_quota(self, *args, **kw): """ The arguments passed to the 'set_user_folder_quota' hook: - used (integer, in KB) - imap_quota (current imap quota obtained from IMAP, integer, in KB) - ldap_quota (current LDAP quota obtained from LDAP, integer, in KB) - default_quota (integer, in KB) Returns: - None - an error has occurred and this plugin doesn't care. - Negative 1 - remove quota. - Zero - Absolute 0. - Positive Integer - set new quota. """ for keyword in [ 'used', 'imap_quota', 'ldap_quota', 'default_quota' ]: - if not kw.has_key(keyword): + if keyword not in kw: log.warning( _("No keyword %s passed to set_user_folder_quota") % ( keyword ) ) return else: try: if not kw[keyword] == None: kw[keyword] = (int)(kw[keyword]) except: log.error(_("Quota '%s' not an integer!") % (keyword)) return # Escape the user without quota if kw['ldap_quota'] == None: return kw['default_quota'] elif kw['ldap_quota'] == -1: return -1 elif kw['ldap_quota'] > 0: return kw['ldap_quota'] else: return kw['default_quota'] \ No newline at end of file diff --git a/pykolab/plugins/recipientpolicy/__init__.py b/pykolab/plugins/recipientpolicy/__init__.py index 94c707f..330ddfa 100644 --- a/pykolab/plugins/recipientpolicy/__init__.py +++ b/pykolab/plugins/recipientpolicy/__init__.py @@ -1,163 +1,163 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab import utils from pykolab.translate import _ conf = pykolab.getConf() log = pykolab.getLogger('pykolab.plugins.recipientpolicy') class KolabRecipientpolicy(object): """ Example plugin making quota adjustments given arbitrary conditions. """ def __init__(self): pass def add_options(self, *args, **kw): pass #def mail_domain_space_policy_check(self, kw={}, args=()): #(mail, alternative_mail, domain_name, domain_root_dn) = args ## Your actions go here. For example: #return (mail, alternative_mail) def set_primary_mail(self, *args, **kw): """ The arguments passed to the 'set_user_attrs_mail' hook: primary_mail - the policy user_attrs - the current user attributes primary_domain - the domain to use in the primary mail attribute secondary_domains - the secondary domains that are aliases Return the new primary mail address """ user_attrs = utils.normalize(kw['entry']) - if not user_attrs.has_key('domain'): + if 'domain' not in user_attrs: user_attrs['domain'] = kw['primary_domain'] elif not user_attrs['domain'] == kw['primary_domain']: user_attrs['domain'] = kw['primary_domain'] - if not user_attrs.has_key('preferredlanguage'): + if 'preferredlanguage' not in user_attrs: default_locale = conf.get(user_attrs['domain'], 'default_locale') if default_locale == None: default_locale = conf.get('kolab', 'default_locale') if default_locale == None: default_locale = 'en_US' user_attrs['preferredlanguage'] = default_locale try: mail = kw['primary_mail'] % user_attrs mail = utils.translate(mail, user_attrs['preferredlanguage']) mail = mail.lower() return mail except KeyError: log.warning(_("Attribute substitution for 'mail' failed in Recipient Policy")) - if user_attrs.has_key('mail'): + if 'mail' in user_attrs: return user_attrs['mail'] else: return None def set_secondary_mail(self, *args, **kw): """ The arguments passed to the 'set_user_attrs_alternative_mail' hook: primary_mail - the policy user_attrs - the current user attributes primary_domain - the domain to use in the primary mail attribute secondary_domains - the secondary domains that are aliases Return a list of secondary mail addresses """ user_attrs = utils.normalize(kw['entry']) - if not user_attrs.has_key('domain'): + if 'domain' not in user_attrs: user_attrs['domain'] = kw['primary_domain'] elif not user_attrs['domain'] == kw['primary_domain']: user_attrs['domain'] = kw['primary_domain'] - if not user_attrs.has_key('preferredlanguage'): + if 'preferredlanguage' not in user_attrs: default_locale = conf.get(user_attrs['domain'], 'default_locale') if default_locale == None: default_locale = conf.get(user_attrs['domain'], 'default_locale') if default_locale == None: default_locale = 'en_US' user_attrs['preferredlanguage'] = default_locale try: exec("alternative_mail_routines = %s" % kw['secondary_mail']) except Exception: log.error(_("Could not parse the alternative mail routines")) alternative_mail = [] log.debug(_("Alternative mail routines: %r") % (alternative_mail_routines), level=8) _domains = [ kw['primary_domain'] ] + kw['secondary_domains'] for attr in [ 'givenname', 'sn', 'surname' ]: try: user_attrs[attr] = utils.translate(user_attrs[attr], user_attrs['preferredlanguage']) except Exception: log.error(_("An error occurred in composing the secondary mail attribute for entry %r") % (user_attrs['id'])) if conf.debuglevel > 8: import traceback traceback.print_exc() return [] for number in alternative_mail_routines: for routine in alternative_mail_routines[number]: try: exec("retval = '%s'.%s" % (routine,alternative_mail_routines[number][routine] % user_attrs)) log.debug(_("Appending additional mail address: %s") % (retval), level=8) alternative_mail.append(retval) except Exception as errmsg: log.error(_("Policy for secondary email address failed: %r") % (errmsg)) if conf.debuglevel > 8: import traceback traceback.print_exc() return [] for _domain in kw['secondary_domains']: user_attrs['domain'] = _domain try: exec("retval = '%s'.%s" % (routine,alternative_mail_routines[number][routine] % user_attrs)) log.debug(_("Appending additional mail address: %s") % (retval), level=8) alternative_mail.append(retval) except KeyError: log.warning(_("Attribute substitution for 'alternative_mail' failed in Recipient Policy")) alternative_mail = utils.normalize(alternative_mail) alternative_mail = list(set(alternative_mail)) return alternative_mail diff --git a/pykolab/plugins/roundcubedb/__init__.py b/pykolab/plugins/roundcubedb/__init__.py index 40271c3..9fe7642 100644 --- a/pykolab/plugins/roundcubedb/__init__.py +++ b/pykolab/plugins/roundcubedb/__init__.py @@ -1,69 +1,69 @@ # -*- coding: utf-8 -*- # Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) # # Thomas Bruederli (Kolab Systems) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3 or, at your option, any later version # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # import os import pykolab import subprocess from pykolab.translate import _ log = pykolab.getLogger('pykolab.plugins.roundcubedb') conf = pykolab.getConf() class KolabRoundcubedb(object): """ Pykolab plugin to update Roundcube's database on Kolab users db changes """ def __init__(self): pass def add_options(self, *args, **kw): pass def user_delete(self, *args, **kw): """ The arguments passed to the 'user_delete' hook: user - full user entry from LDAP domain - domain name """ log.debug(_("user_delete: %r") % (kw), level=8) if os.path.isdir('/usr/share/roundcubemail'): rcpath = '/usr/share/roundcubemail/' elif os.path.isdir('/usr/share/roundcube'): rcpath = '/usr/share/roundcube/' else: log.error(_("Roundcube installation path not found.")) return False result_attribute = conf.get('cyrus-sasl', 'result_attribute') # execute Roundcube's bin/deluser.sh script to do the work - if kw.has_key('user') and kw['user'].has_key(result_attribute) and os.path.exists(rcpath + 'bin/deluser.sh'): + if 'user' in kw and result_attribute in kw['user'] and os.path.exists(rcpath + 'bin/deluser.sh'): proc = subprocess.Popen([ 'sudo -u apache', rcpath + 'bin/deluser.sh', kw['user'][result_attribute] ], stderr=subprocess.PIPE, stdout=subprocess.PIPE) procout, procerr = proc.communicate() if proc.returncode != 0: log.error(rcpath + "bin/deluser.sh exited with error %d: %r" % (proc.returncode, procerr)) else: log.debug(rcpath + "bin/deluser.sh success: %r; %r" % (procout, procerr), level=8) return None diff --git a/pykolab/plugins/sievemgmt/__init__.py b/pykolab/plugins/sievemgmt/__init__.py index 6588bdb..f7e5368 100644 --- a/pykolab/plugins/sievemgmt/__init__.py +++ b/pykolab/plugins/sievemgmt/__init__.py @@ -1,431 +1,431 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.translate import _ conf = pykolab.getConf() log = pykolab.getLogger('pykolab.plugin_sievemgmt') import sys import time from urlparse import urlparse class KolabSievemgmt(object): """ Plugin to manage Sieve scripts according to KEP #14. """ def __init__(self): pass def add_options(self, *args, **kw): pass def sieve_mgmt_refresh(self, *args, **kw): """ The arguments passed to the 'sieve_mgmt_refresh' hook: user - the user identifier """ - if not len(kw) == 1 or not kw.has_key('user'): + if not len(kw) == 1 or 'user' not in kw: log.error(_("Wrong number of arguments for sieve management plugin")) return else: address = kw['user'] auth = Auth() auth.connect() user = auth.find_recipient(address) log.info("User for address %r: %r" % (address, user)) # Get the main, default backend backend = conf.get('kolab', 'imap_backend') if len(address.split('@')) > 1: domain = address.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'): backend = conf.get(domain, 'imap_backend') if conf.has_section(domain) and conf.has_option(domain, 'imap_uri'): uri = conf.get(domain, 'imap_uri') else: uri = conf.get(backend, 'uri') hostname = None port = None result = urlparse(uri) if hasattr(result, 'hostname'): hostname = result.hostname else: scheme = uri.split(':')[0] (hostname, port) = uri.split('/')[2].split(':') port = 4190 # Get the credentials admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') import sievelib.managesieve sieveclient = sievelib.managesieve.Client(hostname, port, conf.debuglevel > 8) sieveclient.connect(None, None, True) sieveclient._plain_authentication(admin_login, admin_password, address) sieveclient.authenticated = True result = sieveclient.listscripts() if result == None: active = None scripts = [] else: active, scripts = result log.debug(_("Found the following scripts for user %s: %s") % (address, ','.join(scripts)), level=8) log.debug(_("And the following script is active for user %s: %s") % (address, active), level=8) mgmt_required_extensions = [] mgmt_script = """# # MANAGEMENT # """ user = auth.get_entry_attributes(domain, user, ['*']) # # Vacation settings (a.k.a. Out of Office) # vacation_active = None vacation_text = None vacation_uce = None vacation_noreact_domains = None vacation_react_domains = None vacation_active_attr = conf.get('sieve', 'vacation_active_attr') vacation_text_attr = conf.get('sieve', 'vacation_text_attr') vacation_uce_attr = conf.get('sieve', 'vacation_uce_attr') vacation_noreact_domains_attr = conf.get('sieve', 'vacation_noreact_domains_attr') vacation_react_domains_attr = conf.get('sieve', 'vacation_react_domains_attr') if not vacation_text_attr == None: - if user.has_key(vacation_active_attr): + if vacation_active_attr in user: vacation_active = utils.true_or_false(user[vacation_active_attr]) else: vacation_active = False - if user.has_key(vacation_text_attr): + if vacation_text_attr in user: vacation_text = user[vacation_text_attr] else: vacation_active = False - if user.has_key(vacation_uce_attr): + if vacation_uce_attr in user: vacation_uce = utils.true_or_false(user[vacation_uce_attr]) else: vacation_uce = False - if user.has_key(vacation_react_domains_attr): + if vacation_react_domains_attr in user: if isinstance(user[vacation_react_domains_attr], list): vacation_react_domains = user[vacation_react_domains_attr] else: vacation_react_domains = [ user[vacation_react_domains_attr] ] else: - if user.has_key(vacation_noreact_domains_attr): + if vacation_noreact_domains_attr in user: if isinstance(user[vacation_noreact_domains_attr], list): vacation_noreact_domains = user[vacation_noreact_domains_attr] else: vacation_noreact_domains = [ user[vacation_noreact_domains_attr] ] else: vacation_noreact_domains = [] # # Delivery to Folder # dtf_active_attr = conf.get('sieve', 'deliver_to_folder_active') if not dtf_active_attr == None: - if user.has_key(dtf_active_attr): + if dtf_active_attr in user: dtf_active = utils.true_or_false(user[dtf_active_attr]) else: dtf_active = False else: # TODO: Not necessarily de-activated, the *Active attributes are # not supposed to charge this - check the deliver_to_folder_attr # attribute value for a value. dtf_active = False if dtf_active: dtf_folder_name_attr = conf.get('sieve', 'deliver_to_folder_attr') if not dtf_folder_name_attr == None: - if user.has_key(dtf_folder_name_attr): + if dtf_folder_name_attr in user: dtf_folder = user[dtf_folder_name_attr] else: log.warning(_("Delivery to folder active, but no folder name attribute available for user %r") % (user)) dtf_active = False else: log.error(_("Delivery to folder active, but no folder name attribute configured")) dtf_active = False # # Folder name to delivery spam to. # # Global or local. # sdf_filter = True sdf = conf.get('sieve', 'spam_global_folder') if sdf == None: sdf = conf.get('sieve', 'spam_personal_folder') if sdf == None: sdf_filter = False # # Mail forwarding # forward_active = None forward_addresses = [] forward_keepcopy = None forward_uce = None forward_active_attr = conf.get('sieve', 'forward_address_active') if not forward_active_attr == None: - if user.has_key(forward_active_attr): + if forward_active_attr in user: forward_active = utils.true_or_false(user[forward_active_attr]) else: forward_active = False if not forward_active == False: forward_address_attr = conf.get('sieve', 'forward_address_attr') - if user.has_key(forward_address_attr): + if forward_address_attr in user: if isinstance(user[forward_address_attr], basestring): forward_addresses = [ user[forward_address_attr] ] elif isinstance(user[forward_address_attr], str): forward_addresses = [ user[forward_address_attr] ] else: forward_addresses = user[forward_address_attr] if len(forward_addresses) == 0: forward_active = False forward_keepcopy_attr = conf.get('sieve', 'forward_keepcopy_active') if not forward_keepcopy_attr == None: - if user.has_key(forward_keepcopy_attr): + if forward_keepcopy_attr in user: forward_keepcopy = utils.true_or_false(user[forward_keepcopy_attr]) else: forward_keepcopy = False forward_uce_attr = conf.get('sieve', 'forward_uce_active') if not forward_uce_attr == None: - if user.has_key(forward_uce_attr): + if forward_uce_attr in user: forward_uce = utils.true_or_false(user[forward_uce_attr]) else: forward_uce = False if vacation_active: mgmt_required_extensions.append('vacation') mgmt_required_extensions.append('envelope') if dtf_active: mgmt_required_extensions.append('fileinto') if forward_active and (len(forward_addresses) > 1 or forward_keepcopy): mgmt_required_extensions.append('copy') if sdf_filter: mgmt_required_extensions.append('fileinto') import sievelib.factory mgmt_script = sievelib.factory.FiltersSet("MANAGEMENT") for required_extension in mgmt_required_extensions: mgmt_script.require(required_extension) mgmt_script.require('fileinto') if vacation_active: if not vacation_react_domains == None and len(vacation_react_domains) > 0: mgmt_script.addfilter( 'vacation', [('envelope', ':domain', ":is", "from", vacation_react_domains)], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) elif not vacation_noreact_domains == None and len(vacation_noreact_domains) > 0: mgmt_script.addfilter( 'vacation', [('not', ('envelope', ':domain', ":is", "from", vacation_noreact_domains))], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) else: mgmt_script.addfilter( 'vacation', [('true',)], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) if forward_active: forward_rules = [] # Principle can be demonstrated by: # # python -c "print ','.join(['a','b','c'][:-1])" # for forward_copy in forward_addresses[:-1]: forward_rules.append(("redirect", ":copy", forward_copy)) if forward_keepcopy: # Principle can be demonstrated by: # # python -c "print ','.join(['a','b','c'][-1])" # if forward_uce: rule_name = 'forward-uce-keepcopy' else: rule_name = 'forward-keepcopy' forward_rules.append(("redirect", ":copy", forward_addresses[-1])) else: if forward_uce: rule_name = 'forward-uce' else: rule_name = 'forward' forward_rules.append(("redirect", forward_addresses[-1])) forward_rules.append(("stop")) if forward_uce: mgmt_script.addfilter(rule_name, ['true'], forward_rules) else: # NOTE: Messages with no X-Spam-Status header need to be matched # too, and this does exactly that. mgmt_script.addfilter(rule_name, [("not", ("X-Spam-Status", ":matches", "Yes,*"))], forward_rules) if sdf_filter: mgmt_script.addfilter('spam_delivery_folder', [("X-Spam-Status", ":matches", "Yes,*")], [("fileinto", "INBOX/Spam"), ("stop")]) if dtf_active: mgmt_script.addfilter('delivery_to_folder', ['true'], [("fileinto", dtf_folder)]) mgmt_script = mgmt_script.__str__() result = sieveclient.putscript("MANAGEMENT", mgmt_script) if not result: log.error(_("Uploading script MANAGEMENT failed for user %s") % (address)) else: log.debug(_("Uploading script MANAGEMENT for user %s succeeded") % (address), level=8) user_script = """# # User # require ["include"]; """ for script in scripts: if not script in [ "MASTER", "MANAGEMENT", "USER" ]: log.debug(_("Including script %s in USER (for user %s)") % (script,address) ,level=8) user_script = """%s include :personal "%s"; """ % (user_script, script) result = sieveclient.putscript("USER", user_script) if not result: log.error(_("Uploading script USER failed for user %s") % (address)) else: log.debug(_("Uploading script USER for user %s succeeded") % (address), level=8) result = sieveclient.putscript("MASTER", """# # MASTER # # This file is authoritative for your system and MUST BE KEPT ACTIVE. # # Altering it is likely to render your account dysfunctional and may # be violating your organizational or corporate policies. # # For more information on the mechanism and the conventions behind # this script, see http://wiki.kolab.org/KEP:14 # require ["include"]; # OPTIONAL: Includes for all or a group of users # include :global "all-users"; # include :global "this-group-of-users"; # The script maintained by the general management system include :personal "MANAGEMENT"; # The script(s) maintained by one or more editors available to the user include :personal "USER"; """) if not result: log.error(_("Uploading script MASTER failed for user %s") % (address)) else: log.debug(_("Uploading script MASTER for user %s succeeded") % (address), level=8) sieveclient.setactive("MASTER") diff --git a/pykolab/setup/__init__.py b/pykolab/setup/__init__.py index 52bee91..26638b0 100644 --- a/pykolab/setup/__init__.py +++ b/pykolab/setup/__init__.py @@ -1,78 +1,78 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import sys import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() to_execute = [] class Setup(object): def __init__(self): import components components.__init__() arg_num = 0 for arg in sys.argv[1:]: arg_num += 1 if not arg.startswith('-') and len(sys.argv) >= arg_num: - if components.components.has_key(sys.argv[arg_num].replace('-','_')): + if sys.argv[arg_num].replace('-','_') in components.components: to_execute.append(sys.argv[arg_num].replace('-','_')) def run(self): if os.path.isfile('/sys/fs/selinux/enforce'): if os.access('/sys/fs/selinux/enforce', os.R_OK): # Set a gentle default because strictly speaking, # setup won't fail (run-time does) enforce = "0" with open('/sys/fs/selinux/enforce', 'r') as f: enforce = f.read() if enforce.strip() == "1": log.fatal( _("SELinux currently enforcing. Read " + \ "https://git.kolab.org/u/1") ) sys.exit(1) if os.path.isfile('/etc/selinux/config'): if os.access('/etc/selinux/config', os.R_OK): with open('/etc/selinux/config', 'r') as f: for line in f: if line.strip() == "SELINUX=enforcing": log.fatal( _("SELinux configured to enforce a " + \ "policy on startup. Read " + \ "https://git.kolab.org/u/1") ) sys.exit(1) components.execute('_'.join(to_execute)) if os.path.exists('/tmp/kolab-setup-my.cnf'): os.unlink('/tmp/kolab-setup-my.cnf') diff --git a/pykolab/setup/components.py b/pykolab/setup/components.py index f68bb48..49a4f65 100644 --- a/pykolab/setup/components.py +++ b/pykolab/setup/components.py @@ -1,266 +1,266 @@ # -*- coding: utf-8 -*- # # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import pykolab from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() components = {} component_groups = {} executed_components = [] components_included_in_cli = [] finalize_conf_ok = None def __init__(): # We only want the base path components_base_path = os.path.dirname(__file__) for components_path, dirnames, filenames in os.walk(components_base_path): if not components_path == components_base_path: continue for filename in filenames: if filename.startswith('setup_') and filename.endswith('.py'): module_name = filename.replace('.py','') component_name = module_name.replace('setup_', '') #print "exec(\"from %s import __init__ as %s_register\"" % (module_name,component_name) exec("from %s import __init__ as %s_register" % (module_name,component_name)) exec("%s_register()" % (component_name)) for dirname in dirnames: register_group(components_path, dirname) register('help', list_components, description=_("Display this help.")) def list_components(*args, **kw): """ List components """ __components = {} for component in components: if isinstance(component, tuple): component_group, component = component __components[component_group] = { component: components[(component_group,component)] } else: __components[component] = components[component] _components = __components.keys() _components.sort() for _component in _components: - if __components[_component].has_key('function'): + if 'function' in __components[_component]: # This is a top-level component if not __components[_component]['description'] == None: print("%-25s - %s" % (_component.replace('_','-'),__components[_component]['description'])) else: print("%-25s" % (_component.replace('_','-'))) for _component in _components: - if not __components[_component].has_key('function'): + if 'function' not in __components[_component]: # This is a nested component print("\n" + _("Command Group: %s") % (_component) + "\n") ___components = __components[_component].keys() ___components.sort() for __component in ___components: if not __components[_component][__component]['description'] == None: print("%-4s%-21s - %s" % ('',__component.replace('_','-'),__components[_component][__component]['description'])) else: print("%-4s%-21s" % ('',__component.replace('_','-'))) def _list_components(*args, **kw): """ List components and return API compatible, parseable lists and dictionaries. """ __components = {} for component in components: if isinstance(component, tuple): component_group, component = component __components[component_group] = { component: components[(component_group,component)] } else: __components[component] = components[component] _components = __components.keys() _components.sort() return _components def cli_options_from_component(component_name, *args, **kw): global components_included_in_cli if component_name in components_included_in_cli: return - if components[component_name].has_key('group'): + if 'group' in components[component_name]: group = components[component_name]['group'] component_name = components[component_name]['component_name'] try: exec("from %s.setup_%s import cli_options as %s_%s_cli_options" % (group,component_name,group,component_name)) exec("%s_%s_cli_options()" % (group,component_name)) except ImportError: pass else: try: exec("from setup_%s import cli_options as %s_cli_options" % (component_name,component_name)) exec("%s_cli_options()" % (component_name)) except ImportError: pass components_included_in_cli.append(component_name) def execute(component_name, *args, **kw): if component_name == '': log.debug( _("No component selected, continuing for all components"), level=8 ) while 1: for component in _list_components(): execute_this = True if component in executed_components: execute_this = False if component == "help": execute_this = False if execute_this: - if components[component].has_key('after'): + if 'after' in components[component]: for _component in components[component]['after']: if not _component in executed_components: execute_this = False if execute_this: execute(component) executed_components.append(component) executed_all = True for component in _list_components(): if not component in executed_components and not component == "help": executed_all = False if executed_all: break return else: for component in _list_components(): cli_options_from_component(component) - if not components.has_key(component_name): + if component_name not in components: log.error(_("No such component.")) sys.exit(1) - if not components[component_name].has_key('function') and \ - not components[component_name].has_key('group'): + if 'function' not in components[component_name] and \ + 'group' not in components[component_name]: log.error(_("No such component.")) sys.exit(1) conf.finalize_conf() if len(conf.cli_args) >= 1: _component_name = conf.cli_args.pop(0) else: _component_name = component_name components[component_name]['function'](conf.cli_args, kw) def register_group(dirname, module): components_base_path = os.path.join(os.path.dirname(__file__), module) components[module] = {} for components_path, dirnames, filenames in os.walk(components_base_path): if not components_path == components_base_path: continue for filename in filenames: if filename.startswith('setup_') and filename.endswith('.py'): module_name = filename.replace('.py','') component_name = module_name.replace('setup_', '') #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" % (module,module_name,module,component_name) exec("from %s.%s import __init__ as %s_%s_register" % (module,module_name,module,component_name)) exec("%s_%s_register()" % (module,component_name)) def register(component_name, func, group=None, description=None, aliases=[], after=[], before=[]): if not group == None: component = "%s_%s" % (group,component_name) else: component = component_name if isinstance(aliases, basestring): aliases = [aliases] - if components.has_key(component): + if component in components: log.fatal(_("Command '%s' already registered") % (component)) sys.exit(1) if callable(func): if group == None: components[component_name] = { 'function': func, 'description': description, 'after': after, 'before': before, } else: components[group][component_name] = { 'function': func, 'description': description, 'after': after, 'before': before, } components[component] = components[group][component_name] components[component]['group'] = group components[component]['component_name'] = component_name for alias in aliases: components[alias] = { 'function': func, 'description': _("Alias for %s") % (component_name) } ## ## Commands not yet implemented ## def not_yet_implemented(*args, **kw): print(_("Not yet implemented")) sys.exit(1) diff --git a/pykolab/translit.py b/pykolab/translit.py index cf54bae..d1c0459 100644 --- a/pykolab/translit.py +++ b/pykolab/translit.py @@ -1,132 +1,132 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.translit') locale_translit_map = { 'ru_RU': 'cyrillic' } translit_map = { 'cyrillic': { u'А': 'A', u'а': 'a', u'Б': 'B', u'б': 'b', u'В': 'V', u'в': 'v', u'Г': 'G', u'г': 'g', u'Д': 'D', u'д': 'd', u'Е': 'E', u'е': 'e', u'Ё': 'Yo', u'ё': 'e', u'Ж': 'Zh', u'ж': 'zh', u'З': 'Z', u'з': 'z', u'И': 'I', u'и': 'i', u'Й': 'J', u'й': 'j', u'К': 'K', u'к': 'k', u'Л': 'L', u'л': 'l', u'М': 'M', u'м': 'm', u'Н': 'N', u'н': 'n', u'О': 'O', u'о': 'o', u'П': 'P', u'п': 'p', u'Р': 'R', u'р': 'r', u'С': 'S', u'с': 's', u'Т': 'T', u'т': 't', u'У': 'U', u'у': 'u', u'Ф': 'F', u'ф': 'f', u'Х': 'Kh', u'х': 'kh', u'Ц': 'Tc', u'ц': 'tc', u'Ч': 'Ch', u'ч': 'ch', u'Ш': 'Sh', u'ш': 'sh', u'Щ': 'Shch', u'щ': 'shch', u'Ъ': '', u'ъ': '', u'Ы': 'Y', u'ы': 'y', u'Ь': '', u'ь': '', u'Э': 'E', u'э': 'e', u'Ю': 'Yu', u'ю': 'yu', u'Я': 'Ya', u'я': 'ya', } } def transliterate(_input, lang, _output_expected=None): - if locale_translit_map.has_key(lang): + if lang in locale_translit_map: _translit_name = locale_translit_map[lang] else: _translit_name = lang _output = '' if not isinstance(_input, unicode): for char in _input.decode('utf-8'): - if translit_map.has_key(_translit_name): - if translit_map[_translit_name].has_key(char): + if _translit_name in translit_map: + if char in translit_map[_translit_name]: _output += translit_map[_translit_name][char] elif char in [repr(x) for x in translit_map[_translit_name]]: _output += translit_map[_translit_name][[char in [raw(x) for x in translit_map[_translit_name]]][0]] else: _output += char else: _output += char else: for char in _input: - if translit_map.has_key(_translit_name): - if translit_map[_translit_name].has_key(char): + if _translit_name in translit_map: + if char in translit_map[_translit_name]: _output += translit_map[_translit_name][char] elif char in [repr(x) for x in translit_map[_translit_name]]: _output += translit_map[_translit_name][[char in [raw(x) for x in translit_map[_translit_name]]][0]] else: _output += char else: _output += char return _output diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py index 034be05..bfb2e2c 100644 --- a/pykolab/wap_client/__init__.py +++ b/pykolab/wap_client/__init__.py @@ -1,661 +1,661 @@ import json import httplib import urllib import sys from urlparse import urlparse import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.wap_client') conf = pykolab.getConf() if not hasattr(conf, 'defaults'): conf.finalize_conf() API_HOSTNAME = "localhost" API_SCHEME = "http" API_PORT = 80 API_SSL = False API_BASE = "/kolab-webadmin/api/" kolab_wap_url = conf.get('kolab_wap', 'api_url') if not kolab_wap_url == None: result = urlparse(kolab_wap_url) else: result = None if hasattr(result, 'scheme') and result.scheme == 'https': API_SSL = True API_PORT = 443 if hasattr(result, 'hostname'): API_HOSTNAME = result.hostname if hasattr(result, 'port'): API_PORT = result.port if hasattr(result, 'path'): API_BASE = result.path session_id = None conn = None def authenticate(username=None, password=None, domain=None): global session_id if username == None: username = conf.get('ldap', 'bind_dn') if password == None: password = conf.get('ldap', 'bind_pw') if domain == None: domain = conf.get('kolab', 'primary_domain') post = json.dumps( { 'username': username, 'password': password, 'domain': domain } ) response = request('POST', "system.authenticate", post=post) if not response: return False - if response.has_key('session_token'): + if 'session_token' in response: session_id = response['session_token'] return True def connect(uri=None): global conn, API_SSL, API_PORT, API_HOSTNAME, API_BASE if not uri == None: result = urlparse(uri) if hasattr(result, 'scheme') and result.scheme == 'https': API_SSL = True API_PORT = 443 if hasattr(result, 'hostname'): API_HOSTNAME = result.hostname if hasattr(result, 'port'): API_PORT = result.port if hasattr(result, 'path'): API_BASE = result.path if conn == None: if API_SSL: conn = httplib.HTTPSConnection(API_HOSTNAME, API_PORT) else: conn = httplib.HTTPConnection(API_HOSTNAME, API_PORT) conn.connect() return conn def disconnect(quit=False): global conn, session_id if quit and session_id: request('GET', 'system.quit') session_id = None if conn: conn.close() conn = None def domain_add(domain, aliases=[]): dna = conf.get('ldap', 'domain_name_attribute') post = json.dumps({ dna: [ domain ] + aliases }) return request('POST', 'domain.add', post=post) def domain_delete(domain, force=False): domain_id, domain_attrs = domain_find(domain).popitem() param = {} param['id'] = domain_id if force: param['force'] = force post = json.dumps(param) return request('POST', 'domain.delete', post=post) def domain_find(domain): dna = conf.get('ldap', 'domain_name_attribute') get = { dna: domain } return request('GET', 'domain.find', get=get) def domain_info(domain): domain_id, domain_attrs = domain_find(domain) get = { 'id': domain_id } return request('GET', 'domain.info', get=get) def domains_capabilities(): return request('GET', 'domains.capabilities') def domains_list(): return request('GET', 'domains.list') def form_value_generate(params): post = json.dumps(params) return request('POST', 'form_value.generate', post=post) def form_value_generate_password(*args, **kw): return request('GET', 'form_value.generate_password') def form_value_list_options(object_type, object_type_id, attribute): post = json.dumps( { 'object_type': object_type, 'type_id': object_type_id, 'attribute': attribute } ) return request('POST', 'form_value.list_options', post=post) def form_value_select_options(object_type, object_type_id, attribute): post = json.dumps( { 'object_type': object_type, 'type_id': object_type_id, 'attributes': [ attribute ] } ) return request('POST', 'form_value.select_options', post=post) def get_group_input(): group_types = group_types_list() if len(group_types) > 1: for key in group_types: if not key == "status": print("%s) %s" % (key,group_types[key]['name'])) group_type_id = utils.ask_question("Please select the group type") elif len(group_types) > 0: print("Automatically selected the only group type available") group_type_id = group_types.keys()[0] else: print("No group types available") sys.exit(1) - if group_types.has_key(group_type_id): + if group_type_id in group_types: group_type_info = group_types[group_type_id]['attributes'] else: print("No such group type") sys.exit(1) params = { 'group_type_id': group_type_id } for attribute in group_type_info['form_fields']: params[attribute] = utils.ask_question(attribute) for attribute in group_type_info['auto_form_fields']: exec("retval = group_form_value_generate_%s(params)" % (attribute)) params[attribute] = retval[attribute] return params def get_user_input(): user_types = user_types_list() if user_types['count'] > 1: print("") for key in user_types['list']: if not key == "status": print("%s) %s" % (key,user_types['list'][key]['name'])) print("") user_type_id = utils.ask_question("Please select the user type") elif user_types['count'] > 0: print("Automatically selected the only user type available") user_type_id = user_types['list'].keys()[0] else: print("No user types available") sys.exit(1) - if user_types['list'].has_key(user_type_id): + if user_type_id in user_types['list']: user_type_info = user_types['list'][user_type_id]['attributes'] else: print("No such user type") sys.exit(1) params = { 'object_type': 'user', 'type_id': user_type_id } must_attrs = [] may_attrs = [] for attribute in user_type_info['form_fields']: if isinstance(user_type_info['form_fields'][attribute], dict): - if user_type_info['form_fields'][attribute].has_key('optional') and user_type_info['form_fields'][attribute]['optional']: + if 'optional' in user_type_info['form_fields'][attribute] and user_type_info['form_fields'][attribute]['optional']: may_attrs.append(attribute) else: must_attrs.append(attribute) else: must_attrs.append(attribute) for attribute in must_attrs: if isinstance(user_type_info['form_fields'][attribute], dict) and \ - user_type_info['form_fields'][attribute].has_key('type'): + 'type' in user_type_info['form_fields'][attribute]: if user_type_info['form_fields'][attribute]['type'] == 'select': - if not user_type_info['form_fields'][attribute].has_key('values'): + if 'values' not in user_type_info['form_fields'][attribute]: attribute_values = form_value_select_options('user', user_type_id, attribute) default = '' - if attribute_values[attribute].has_key('default'): + if 'default' in attribute_values[attribute]: default = attribute_values[attribute]['default'] params[attribute] = utils.ask_menu( "Choose the %s value" % (attribute), attribute_values[attribute]['list'], default=default ) else: default = '' - if user_type_info['form_fields'][attribute].has_key('default'): + if 'default' in user_type_info['form_fields'][attribute]: default = user_type_info['form_fields'][attribute]['default'] params[attribute] = utils.ask_menu( "Choose the %s value" % (attribute), user_type_info['form_fields'][attribute]['values'], default=default ) else: params[attribute] = utils.ask_question(attribute) else: params[attribute] = utils.ask_question(attribute) for attribute in user_type_info['fields']: params[attribute] = user_type_info['fields'][attribute] exec("retval = user_form_value_generate(params)") print(retval) return params def group_add(params=None): if params == None: params = get_group_input() post = json.dumps(params) return request('POST', 'group.add', post=post) def group_delete(params=None): if params == None: params = { 'id': utils.ask_question("Name of group to delete", "group") } post = json.dumps(params) return request('POST', 'group.delete', post=post) def group_form_value_generate_mail(params=None): if params == None: params = get_user_input() params = json.dumps(params) return request('POST', 'group_form_value.generate_mail', params) def group_find(params=None): post = { 'search': { 'params': {} } } for (k,v) in params.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } return request('POST', 'group.find', post=json.dumps(post)) def group_info(group=None): if group == None: group = utils.ask_question("group DN") return request('GET', 'group.info', get={ 'id': group }) def group_members_list(group=None): if group == None: group = utils.ask_question("Group email address") group = request('GET', 'group.members_list?group=%s' % (group)) return group def group_types_list(): return request('GET', 'group_types.list') def groups_list(params={}): return request('POST', 'groups.list', post=json.dumps(params)) def ou_add(params={}): return request('POST', 'ou.add', post=json.dumps(params)) def ou_delete(params={}): return request('POST', 'ou.delete', post=json.dumps(params)) def ou_edit(params={}): return request('POST', 'ou.edit', post=json.dumps(params)) def ou_find(params=None): post = { 'search': { 'params': {} } } for (k,v) in params.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } return request('POST', 'ou.find', post=json.dumps(post)) def ou_info(ou): _params = { 'id': ou } ou = request('GET', 'ou.info', get=_params) return ou def ous_list(params={}): return request('POST', 'ous.list', post=json.dumps(params)) def request(method, api_uri, get=None, post=None, headers={}): response_data = request_raw(method, api_uri, get, post, headers) if response_data['status'] == "OK": del response_data['status'] return response_data['result'] else: print("%s: %s (code %s)" % (response_data['status'], response_data['reason'], response_data['code'])) return False def request_raw(method, api_uri, get=None, post=None, headers={}, isretry=False): global session_id if not session_id == None: headers["X-Session-Token"] = session_id reconnect = False conn = connect() if conf.debuglevel > 8: conn.set_debuglevel(9) if not get == None: _get = "?%s" % (urllib.urlencode(get)) else: _get = "" log.debug(_("Requesting %r with params %r") % ("%s/%s" % (API_BASE,api_uri), (get, post)), level=8) try: conn.request(method.upper(), "%s/%s%s" % (API_BASE, api_uri, _get), post, headers) response = conn.getresponse() data = response.read() log.debug(_("Got response: %r") % (data), level=8) except (httplib.BadStatusLine, httplib.CannotSendRequest) as e: if isretry: raise e log.info(_("Connection error: %r; re-connecting..."), e) reconnect = True # retry with a new connection if reconnect: disconnect() return request_raw(method, api_uri, get, post, headers, True) try: response_data = json.loads(data) except ValueError: # Some data is not JSON log.error(_("Response data is not JSON")) return response_data def resource_add(params=None): if params == None: params = get_user_input() return request('POST', 'resource.add', post=json.dumps(params)) def resource_delete(params=None): if params == None: params = { 'id': utils.ask_question("Resource DN to delete", "resource") } return request('POST', 'resource.delete', post=json.dumps(params)) def resource_find(params=None): post = { 'search': { 'params': {} } } for (k,v) in params.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } return request('POST', 'resource.find', post=json.dumps(post)) def resource_info(resource=None): if resource == None: resource = utils.ask_question("Resource DN") return request('GET', 'resource.info', get={ 'id': resource }) def resource_types_list(): return request('GET', 'resource_types.list') def resources_list(params={}): return request('POST', 'resources.list', post=json.dumps(params)) def role_add(params=None): if params == None: role_name = utils.ask_question("Role name") params = { 'cn': role_name } params = json.dumps(params) return request('POST', 'role.add', params) def role_capabilities(): return request('GET', 'role.capabilities') def role_delete(params=None): if params == None: role_name = utils.ask_question("Role name") role = role_find_by_attribute({'cn': role_name}) params = { 'role': role.keys()[0] } - if not params.has_key('role'): + if 'role' not in params: role = role_find_by_attribute(params) params = { 'role': role.keys()[0] } post = json.dumps(params) return request('POST', 'role.delete', post=post) def role_find_by_attribute(params=None): if params == None: role_name = utils.ask_question("Role name") else: role_name = params['cn'] get = { 'cn': role_name } role = request('GET', 'role.find_by_attribute', get=get) return role def role_info(role_name): role = role_find_by_attribute({'cn': role_name}) get = { 'role': role['id'] } role = request('GET', 'role.info', get=get) return role def roles_list(): return request('GET', 'roles.list') def sharedfolder_add(params=None): if params == None: params = get_user_input() return request('POST', 'sharedfolder.add', post=json.dumps(params)) def sharedfolder_delete(params=None): if params == None: params = { 'id': utils.ask_question("Shared Folder DN to delete", "sharedfolder") } return request('POST', 'sharedfolder.delete', post=json.dumps(params)) def sharedfolders_list(params={}): return request('POST', 'sharedfolders.list', post=json.dumps(params)) def system_capabilities(domain=None): return request('GET', 'system.capabilities', get={'domain':domain}) def system_get_domain(): return request('GET', 'system.get_domain') def system_select_domain(domain=None): if domain == None: domain = utils.ask_question("Domain name") get = { 'domain': domain } return request('GET', 'system.select_domain', get=get) def user_add(params=None): if params == None: params = get_user_input() params = json.dumps(params) return request('POST', 'user.add', post=params) def user_delete(params=None): if params == None: params = { 'id': utils.ask_question("Username for user to delete", "user") } post = json.dumps(params) return request('POST', 'user.delete', post=post) def user_edit(user = None, attributes={}): if user == None: get = { 'id': utils.ask_question("Username for user to edit", "user") } else: get = { 'id': user } user_info = request('GET', 'user.info', get=get) for attribute in attributes: user_info[attribute] = attributes[attribute] post = json.dumps(user_info) user_edit = request('POST', 'user.edit', get=get, post=post) return user_edit def user_find(attribs=None): if attribs == None: post = { 'search': { 'params': { utils.ask_question("Attribute") : { 'value': utils.ask_question("value"), 'type': 'exact' } } } } else: post = { 'search': { 'params': {} } } for (k,v) in attribs.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } post = json.dumps(post) user = request('POST', 'user.find', post=post) return user def user_form_value_generate(params=None): if params == None: params = get_user_input() post = json.dumps(params) return request('POST', 'form_value.generate', post=post) def user_form_value_generate_uid(params=None): if params == None: params = get_user_input() params = json.dumps(params) return request('POST', 'form_value.generate_uid', params) def user_form_value_generate_userpassword(*args, **kw): result = form_value_generate_password() return { 'userpassword': result['password'] } def user_info(user=None): if user == None: user = utils.ask_question("User email address") _params = { 'id': user } user = request('GET', 'user.info', get=_params) return user def users_list(params={}): return request('POST', 'users.list', post=json.dumps(params)) def user_types_list(): return request('GET', 'user_types.list') diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py index b7be74b..f82f96e 100644 --- a/pykolab/xml/attendee.py +++ b/pykolab/xml/attendee.py @@ -1,280 +1,280 @@ import kolabformat from pykolab.translate import _ from pykolab.translate import N_ from contact_reference import ContactReference participant_status_labels = { "NEEDS-ACTION": N_("Needs Action"), "ACCEPTED": N_("Accepted"), "DECLINED": N_("Declined"), "TENTATIVE": N_("Tentatively Accepted"), "DELEGATED": N_("Delegated"), "IN-PROCESS": N_("Started"), "COMPLETED": N_("Completed"), "PENDING": N_("Pending"), # support integer values, too kolabformat.PartNeedsAction: N_("Needs Action"), kolabformat.PartAccepted: N_("Accepted"), kolabformat.PartDeclined: N_("Declined"), kolabformat.PartTentative: N_("Tentatively Accepted"), kolabformat.PartDelegated: N_("Delegated"), kolabformat.PartInProcess: N_("Started"), kolabformat.PartCompleted: N_("Completed"), } def participant_status_label(status): - return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else _(status) + return _(participant_status_labels[status]) if status in participant_status_labels else _(status) class Attendee(kolabformat.Attendee): cutype_map = { "INDIVIDUAL": kolabformat.CutypeIndividual, "RESOURCE": kolabformat.CutypeResource, "GROUP": kolabformat.CutypeGroup, "ROOM": kolabformat.CutypeRoom, "UNKNOWN": kolabformat.CutypeUnknown, } participant_status_map = { "NEEDS-ACTION": kolabformat.PartNeedsAction, "ACCEPTED": kolabformat.PartAccepted, "DECLINED": kolabformat.PartDeclined, "TENTATIVE": kolabformat.PartTentative, "DELEGATED": kolabformat.PartDelegated, "IN-PROCESS": kolabformat.PartInProcess, "COMPLETED": kolabformat.PartCompleted, } # See RFC 2445, 5445 role_map = { "CHAIR": kolabformat.Chair, "REQ-PARTICIPANT": kolabformat.Required, "OPT-PARTICIPANT": kolabformat.Optional, "NON-PARTICIPANT": kolabformat.NonParticipant, } rsvp_map = { "TRUE": True, "FALSE": False, } properties_map = { 'role': 'get_role', 'rsvp': 'rsvp', 'partstat': 'get_participant_status', 'cutype': 'get_cutype', 'delegated-to': 'get_delegated_to', 'delegated-from': 'get_delegated_from', } def __init__( self, email, name=None, rsvp=False, role=None, participant_status=None, cutype=None, ical_params=None ): self.email = email self.contactreference = ContactReference(email) if not name == None: self.contactreference.set_name(name) kolabformat.Attendee.__init__(self, self.contactreference) if isinstance(rsvp, bool): self.setRSVP(rsvp) else: - if self.rsvp_map.has_key(rsvp): + if rsvp in self.rsvp_map: self.setRSVP(self.rsvp_map[rsvp]) if not role == None: self.set_role(role) if not cutype == None: self.set_cutype(cutype) - if ical_params and ical_params.has_key('DELEGATED-FROM'): + if ical_params and 'DELEGATED-FROM' in ical_params: self.delegate_from(Attendee(str(ical_params['DELEGATED-FROM']), role=self.get_role(), cutype=self.get_cutype())) - if ical_params and ical_params.has_key('DELEGATED-TO'): + if ical_params and 'DELEGATED-TO' in ical_params: self.delegate_to(Attendee(str(ical_params['DELEGATED-TO']))) if not participant_status == None: self.set_participant_status(participant_status) def copy_from(self, obj): if isinstance(obj, kolabformat.Attendee): self.contactreference = ContactReference(obj.contact()) self.email = self.contactreference.get_email() self.setContact(self.contactreference) # manually copy all properities, copy constructor doesn't work :-( self.setRSVP(obj.rsvp()) self.setRole(obj.role()) self.setCutype(obj.cutype()) self.setPartStat(obj.partStat()) self.setDelegatedTo(obj.delegatedTo()) self.setDelegatedFrom(obj.delegatedFrom()) def delegate_from(self, delegators): crefs = [] if not isinstance(delegators, list): delegators = [delegators] for delegator in delegators: if not isinstance(delegator, Attendee): raise ValueError(_("Not a valid attendee")) else: self.set_role(delegator.get_role()) self.set_cutype(delegator.get_cutype()) crefs.append(delegator.contactreference) if len(crefs) == 0: raise ValueError(_("No valid delegator references found")) else: crefs += self.get_delegated_from() self.setDelegatedFrom(list(set(crefs))) def delegate_to(self, delegatees): self.set_participant_status("DELEGATED") crefs = [] if not isinstance(delegatees, list): delegatees = [delegatees] for delegatee in delegatees: if not isinstance(delegatee, Attendee): raise ValueError(_("Not a valid attendee")) else: crefs.append(delegatee.contactreference) if len(crefs) == 0: raise ValueError(_("No valid delegatee references found")) else: crefs += self.get_delegated_to() self.setDelegatedTo(list(set(crefs))) def get_cutype(self, translated=False): cutype = self.cutype() if translated: return self._translate_value(cutype, self.cutype_map) return cutype def get_delegated_from(self, translated=False): delegators = [] for cr in self.delegatedFrom(): delegators.append(cr.email() if translated else ContactReference(cr)) return delegators def get_delegated_to(self, translated=False): delegatees = [] for cr in self.delegatedTo(): delegatees.append(cr.email() if translated else ContactReference(cr)) return delegatees def get_email(self): return self.contactreference.get_email() def get_name(self): return self.contactreference.get_name() def get_displayname(self): name = self.contactreference.get_name() email = self.contactreference.get_email() return "%s <%s>" % (name, email) if not name == "" else email def get_participant_status(self, translated=False): partstat = self.partStat() if translated: return self._translate_value(partstat, self.participant_status_map) return partstat def get_role(self, translated=False): role = self.role() if translated: return self._translate_value(role, self.role_map) return role def get_rsvp(self): return self.rsvp() def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) - return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + return name_map[val] if val in name_map else 'UNKNOWN' def set_cutype(self, cutype): if cutype in self.cutype_map: self.setCutype(self.cutype_map[cutype]) elif cutype in self.cutype_map.values(): self.setCutype(cutype) else: raise InvalidAttendeeCutypeError(_("Invalid cutype %r") % (cutype)) def set_name(self, name): self.contactreference.set_name(name) self.setContact(self.contactreference) def set_participant_status(self, participant_status): if participant_status in self.participant_status_map: self.setPartStat(self.participant_status_map[participant_status]) elif participant_status in self.participant_status_map.values(): self.setPartStat(participant_status) else: raise InvalidAttendeeParticipantStatusError(_("Invalid participant status %r") % (participant_status)) def set_role(self, role): if role in self.role_map: self.setRole(self.role_map[role]) elif role in self.role_map.values(): self.setRole(role) else: raise InvalidAttendeeRoleError(_("Invalid role %r") % (role)) def set_rsvp(self, rsvp): self.setRSVP(rsvp) def to_dict(self): data = self.contactreference.to_dict() data.pop('type', None) for p, getter in self.properties_map.iteritems(): val = None args = {} if hasattr(self, getter): if getter.startswith('get_'): args = dict(translated=True) val = getattr(self, getter)(**args) if val is not None: data[p] = val return data def __str__(self): return self.email class AttendeeIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeCutypeError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeParticipantStatusError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeRoleError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/contact.py b/pykolab/xml/contact.py index f4fd9a5..f5053e6 100644 --- a/pykolab/xml/contact.py +++ b/pykolab/xml/contact.py @@ -1,336 +1,336 @@ import kolabformat import datetime import pytz import base64 from pykolab.xml import utils as xmlutils from pykolab.xml.utils import ustr def contact_from_vcard(string): # TODO: implement this pass def contact_from_string(string): _xml = kolabformat.readContact(string, False) return Contact(_xml) def contact_from_message(message): contact = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/vcard+xml": payload = part.get_payload(decode=True) contact = contact_from_string(payload) # append attachment parts to Contact object - elif contact and part.has_key('Content-ID'): + elif contact and 'Content-ID' in part: contact._attachment_parts.append(part) return contact class Contact(kolabformat.Contact): type = 'contact' related_map = { 'manager': kolabformat.Related.Manager, 'assistant': kolabformat.Related.Assistant, 'spouse': kolabformat.Related.Spouse, 'children': kolabformat.Related.Child, None: kolabformat.Related.NoRelation, } addresstype_map = { 'home': kolabformat.Address.Home, 'work': kolabformat.Address.Work, } phonetype_map = { 'home': kolabformat.Telephone.Home, 'work': kolabformat.Telephone.Work, 'text': kolabformat.Telephone.Text, 'main': kolabformat.Telephone.Voice, 'homefax': kolabformat.Telephone.Fax, 'workfax': kolabformat.Telephone.Fax, 'mobile': kolabformat.Telephone.Cell, 'video': kolabformat.Telephone.Video, 'pager': kolabformat.Telephone.Pager, 'car': kolabformat.Telephone.Car, 'other': kolabformat.Telephone.Textphone, } emailtype_map = { 'home': kolabformat.Email.Home, 'work': kolabformat.Email.Work, 'other': kolabformat.Email.Work, } urltype_map = { 'homepage': kolabformat.Url.NoType, 'blog': kolabformat.Url.Blog, } keytype_map = { 'pgp': kolabformat.Key.PGP, 'pkcs7': kolabformat.Key.PKCS7_MIME, None: kolabformat.Key.Invalid, } gender_map = { 'female': kolabformat.Contact.Female, 'male': kolabformat.Contact.Male, None: kolabformat.Contact.NotSet, } properties_map = { 'uid': 'get_uid', 'lastmodified-date': 'get_lastmodified', 'fn': 'name', 'nickname': 'nickNames', 'title': 'titles', 'email': 'emailAddresses', 'tel': 'telephones', 'url': 'urls', 'im': 'imAddresses', 'address': 'addresses', 'note': 'note', 'freebusyurl': 'freeBusyUrl', 'birthday': 'bDay', 'anniversary': 'anniversary', 'categories': 'categories', 'lang': 'languages', 'gender': 'get_gender', 'gpspos': 'gpsPos', 'key': 'keys', } def __init__(self, *args, **kw): self._attachment_parts = [] kolabformat.Contact.__init__(self, *args, **kw) def get_uid(self): uid = self.uid() if not uid == '': return uid else: self.__str__() return kolabformat.getSerializedUID() def get_lastmodified(self): try: _datetime = self.lastModified() if _datetime == None or not _datetime.isValid(): self.__str__() except: return datetime.datetime.now(pytz.utc) return xmlutils.from_cdatetime(self.lastModified(), True) def get_email(self, preferred=True): if preferred: return self.emailAddresses()[self.emailAddressPreferredIndex()] else: return [x for x in self.emailAddresses()] def set_email(self, email, preferred_index=0): if isinstance(email, basestring): self.setEmailAddresses([email], preferred_index) else: self.setEmailAddresses(email, preferred_index) def add_email(self, email): if isinstance(email, basestring): self.add_emails([email]) elif isinstance(email, list): self.add_emails(email) def add_emails(self, emails): preferred_email = self.get_email() emails = [x for x in set(self.get_email(preferred=False) + emails)] preferred_email_index = emails.index(preferred_email) self.setEmailAddresses(emails, preferred_email_index) def set_name(self, name): self.setName(ustr(name)) def get_gender(self, translated=True): _gender = self.gender() if translated: return self._translate_value(_gender, self.gender_map) return _gender def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) - return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + return name_map[val] if val in name_map else 'UNKNOWN' def to_dict(self): if not self.isValid(): return None data = self._names2dict(self.nameComponents()) for p, getter in self.properties_map.iteritems(): val = None if hasattr(self, getter): val = getattr(self, getter)() if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectori): val = [int(x) for x in val] elif isinstance(val, kolabformat.vectors): val = [str(x) for x in val] elif isinstance(val, kolabformat.vectortelephone): val = [self._struct2dict(x, 'number', self.phonetype_map) for x in val] elif isinstance(val, kolabformat.vectoremail): val = [self._struct2dict(x, 'address', self.emailtype_map) for x in val] elif isinstance(val, kolabformat.vectorurl): val = [self._struct2dict(x, 'url', self.urltype_map) for x in val] elif isinstance(val, kolabformat.vectorkey): val = [self._struct2dict(x, 'key', self.keytype_map) for x in val] elif isinstance(val, kolabformat.vectoraddress): val = [self._address2dict(x) for x in val] elif isinstance(val, kolabformat.vectorgeo): val = [[x.latitude, x.longitude] for x in val] if val is not None: data[p] = val affiliations = self.affiliations() if len(affiliations) > 0: _affiliation = self._affiliation2dict(affiliations[0]) - if _affiliation.has_key('address'): + if 'address' in _affiliation: data['address'].extend(_affiliation['address']) _affiliation.pop('address', None) data.update(_affiliation) data.update(self._relateds2dict(self.relateds())) if self.photoMimetype(): data['photo'] = dict(mimetype=self.photoMimetype(), base64=base64.b64encode(self.photo())) elif self.photo(): data['photo'] = dict(uri=self.photo()) return data def _names2dict(self, namecomp): names_map = { 'surname': 'surnames', 'given': 'given', 'additional': 'additional', 'prefix': 'prefixes', 'suffix': 'suffixes', } data = dict() for p, getter in names_map.iteritems(): val = None if hasattr(namecomp, getter): val = getattr(namecomp, getter)() if isinstance(val, kolabformat.vectors): val = [str(x) for x in val][0] if len(val) > 0 else None if val is not None: data[p] = val return data def _affiliation2dict(self, affiliation): props_map = { 'organization': 'organisation', 'department': 'organisationalUnits', 'role': 'roles', } data = dict() for p, getter in props_map.iteritems(): val = None if hasattr(affiliation, getter): val = getattr(affiliation, getter)() if isinstance(val, kolabformat.vectors): val = [str(x) for x in val][0] if len(val) > 0 else None if val is not None: data[p] = val data.update(self._relateds2dict(affiliation.relateds(), True)) addresses = affiliation.addresses() if len(addresses): data['address'] = [self._address2dict(adr, 'office') for adr in addresses] return data def _address2dict(self, adr, adrtype=None): props_map = { 'label': 'label', 'street': 'street', 'locality': 'locality', 'region': 'region', 'code': 'code', 'country': 'country', } addresstype_map = dict([(v, k) for (k, v) in self.addresstype_map.iteritems()]) data = dict() if adrtype is None: adrtype = addresstype_map.get(adr.types(), None) if adrtype is not None: data['type'] = adrtype for p, getter in props_map.iteritems(): val = None if hasattr(adr, getter): val = getattr(adr, getter)() if isinstance(val, kolabformat.vectors): val = [str(x) for x in val][0] if len(val) > 0 else None if val is not None: data[p] = val return data def _relateds2dict(self, relateds, aslist=True): data = dict() related_map = dict([(v, k) for (k, v) in self.related_map.iteritems()]) for rel in relateds: reltype = related_map.get(rel.relationTypes(), None) val = rel.uri() if rel.type() == kolabformat.Related.Uid else rel.text() if reltype and val is not None: if aslist: - if not data.has_key(reltype): + if reltype not in data: data[reltype] = [] data[reltype].append(val) else: data[reltype] = val return data def _struct2dict(self, struct, propname, map): type_map = dict([(v, k) for (k, v) in map.iteritems()]) result = dict() if hasattr(struct, 'types'): result['type'] = type_map.get(struct.types(), None) elif hasattr(struct, 'type'): result['type'] = type_map.get(struct.type(), None) if hasattr(struct, propname): result[propname] = getattr(struct, propname)() return result def __str__(self): xml = kolabformat.writeContact(self) error = kolabformat.error() if error == None or not error: return xml else: raise ContactIntegrityError(kolabformat.errorMessage()) class ContactIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index d85b621..e957a19 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -1,1511 +1,1511 @@ import datetime import icalendar import kolabformat import pytz import time import uuid import base64 import re import pykolab from pykolab import constants from pykolab import utils from pykolab.xml import utils as xmlutils from pykolab.xml import participant_status_label from pykolab.xml.utils import ustr from pykolab.translate import _ from os import path from attendee import Attendee from contact_reference import ContactReference from recurrence_rule import RecurrenceRule from collections import OrderedDict log = pykolab.getLogger('pykolab.xml_event') def event_from_ical(ical, string=None): return Event(from_ical=ical, from_string=string) def event_from_string(string): return Event(from_string=string) def event_from_message(message): event = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/calendar+xml": payload = part.get_payload(decode=True) event = event_from_string(payload) # append attachment parts to Event object - elif event and part.has_key('Content-ID'): + elif event and 'Content-ID' in part: event._attachment_parts.append(part) return event class Event(object): type = 'event' thisandfuture = False status_map = { None: kolabformat.StatusUndefined, "TENTATIVE": kolabformat.StatusTentative, "CONFIRMED": kolabformat.StatusConfirmed, "CANCELLED": kolabformat.StatusCancelled, "COMPLETED": kolabformat.StatusCompleted, "IN-PROCESS": kolabformat.StatusInProcess, "NEEDS-ACTION": kolabformat.StatusNeedsAction, } classification_map = { "PUBLIC": kolabformat.ClassPublic, "PRIVATE": kolabformat.ClassPrivate, "CONFIDENTIAL": kolabformat.ClassConfidential, } alarm_type_map = { 'EMAIL': kolabformat.Alarm.EMailAlarm, 'DISPLAY': kolabformat.Alarm.DisplayAlarm, 'AUDIO': kolabformat.Alarm.AudioAlarm } related_map = { 'START': kolabformat.Start, 'END': kolabformat.End } properties_map = { # property: getter "uid": "get_uid", "created": "get_created", "lastmodified-date": "get_lastmodified", "sequence": "sequence", "classification": "get_classification", "categories": "categories", "start": "get_start", "end": "get_end", "duration": "get_duration", "transparency": "transparency", "rrule": "recurrenceRule", "rdate": "recurrenceDates", "exdate": "exceptionDates", "recurrence-id": "recurrenceID", "summary": "summary", "description": "description", "priority": "priority", "status": "get_ical_status", "location": "location", "organizer": "organizer", "attendee": "get_attendees", "attach": "attachments", "url": "url", "alarm": "alarms", "x-custom": "customProperties", # TODO: add to_dict() support for these # "exception": "exceptions", } def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] self._exceptions = [] self._attachment_parts = [] if isinstance(from_ical, str) and from_ical == "": if from_string == "": self.event = kolabformat.Event() else: self.event = kolabformat.readEvent(from_string, False) self._load_attendees() self._load_exceptions() else: self.from_ical(from_ical, from_string) self.set_created(self.get_created()) self.uid = self.get_uid() def _load_attendees(self): for a in self.event.attendees(): att = Attendee(a.contact().email()) att.copy_from(a) self._attendees.append(att) def _load_exceptions(self): for ex in self.event.exceptions(): exception = Event() exception.uid = ex.uid() exception.event = ex exception._load_attendees() self._exceptions.append(exception) def add_attendee(self, email_or_attendee, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL", params=None): if isinstance(email_or_attendee, Attendee): attendee = email_or_attendee else: attendee = Attendee(email_or_attendee, name, rsvp, role, participant_status, cutype, params) # apply update to self and all exceptions self.update_attendees([attendee], True) def add_category(self, category): self._categories.append(ustr(category)) self.event.setCategories(self._categories) def add_recurrence_date(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo is None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Rdate needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.addRecurrenceDate(xmlutils.to_cdatetime(_datetime, True)) def add_exception_date(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Exdate needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.addExceptionDate(xmlutils.to_cdatetime(_datetime, True)) def add_exception(self, exception): recurrence_id = exception.get_recurrence_id() if recurrence_id is None: raise EventIntegrityError("Recurrence exceptions require a Recurrence-ID property") # check if an exception with the given recurrence-id already exists append = True vexceptions = self.event.exceptions() for i, ex in enumerate(self._exceptions): if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture: # update the existing exception vexceptions[i] = exception.event self._exceptions[i] = exception append = False # check if main event matches the given recurrence-id if append and self.get_recurrence_id() == recurrence_id: self.event = exception.event self._load_attendees() self._load_exceptions() append = False if append: vexceptions.append(exception.event) self._exceptions.append(exception) self.event.setExceptions(vexceptions) def del_exception(self, exception): recurrence_id = exception.get_recurrence_id() if recurrence_id is None: raise EventIntegrityError("Recurrence exceptions require a Recurrence-ID property") updated = False vexceptions = self.event.exceptions() for i, ex in enumerate(self._exceptions): if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture: del vexceptions[i] del self._exceptions[i] updated = True if updated: self.event.setExceptions(vexceptions) def as_string_itip(self, method="REQUEST"): cal = icalendar.Calendar() cal.add( 'prodid', '-//pykolab-%s-%s//kolab.org//' % ( constants.__version__, constants.__release__ ) ) cal.add('version', '2.0') # TODO: Really? cal.add('calscale', 'GREGORIAN') # TODO: Not always a request... cal.add('method', method) # TODO: Add timezone information using icalendar.?() # Not sure if there is a class for it. cal.add_component(self.to_ical()) # add recurrence exceptions if len(self._exceptions) > 0 and not method == 'REPLY': for exception in self._exceptions: cal.add_component(exception.to_ical()) if hasattr(cal, 'to_ical'): return cal.to_ical() elif hasattr(cal, 'as_string'): return cal.as_string() def to_ical(self): event = icalendar.Event() # Required event['uid'] = self.get_uid() # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(event.singletons)): _attr = attr.lower().replace('-', '') ical_getter = 'get_ical_%s' % (_attr) default_getter = 'get_%s' % (_attr) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() if not retval == None and not retval == "": event.add(attr.lower(), retval) elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if not retval == None and not retval == "": event.add(attr.lower(), retval, encode=0) # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(event.multiple)): _attr = attr.lower().replace('-', '') ical_getter = 'get_ical_%s' % (_attr) default_getter = 'get_%s' % (_attr) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if isinstance(retval, list) and not len(retval) == 0: for _retval in retval: event.add(attr.lower(), _retval, encode=0) # copy custom properties to iCal for cs in self.event.customProperties(): event.add(cs.identifier, cs.value) return event def delegate(self, delegators, delegatees, names=None): if not isinstance(delegators, list): delegators = [delegators] if not isinstance(delegatees, list): delegatees = [delegatees] if not isinstance(names, list): names = [names] _delegators = [] for delegator in delegators: _delegators.append(self.get_attendee(delegator)) _delegatees = [] for i,delegatee in enumerate(delegatees): try: _delegatees.append(self.get_attendee(delegatee)) except: # TODO: An iTip needs to be sent out to the new attendee self.add_attendee(delegatee, names[i] if i < len(names) else None) _delegatees.append(self.get_attendee(delegatee)) for delegator in _delegators: delegator.delegate_to(_delegatees) for delegatee in _delegatees: delegatee.delegate_from(_delegators) self.event.setAttendees(self._attendees) def from_ical(self, ical, raw=None): if isinstance(ical, icalendar.Event) or isinstance(ical, icalendar.Calendar): ical_event = ical elif hasattr(icalendar.Event, 'from_ical'): ical_event = icalendar.Event.from_ical(ical) elif hasattr(icalendar.Event, 'from_string'): ical_event = icalendar.Event.from_string(ical) # VCALENDAR block was given, find the first VEVENT item if isinstance(ical_event, icalendar.Calendar): for c in ical_event.walk(): if c.name == 'VEVENT': ical_event = c break # use the libkolab calendaring bindings to load the full iCal data - if ical_event.has_key('RRULE') or ical_event.has_key('ATTACH') \ + if 'RRULE' in ical_event or 'ATTACH' in ical_event \ or [part for part in ical_event.walk() if part.name == 'VALARM']: if raw is None or raw == "": raw = ical if isinstance(ical, str) else ical.to_ical() self._xml_from_ical(raw) else: self.event = kolabformat.Event() # TODO: Clause the timestamps for zulu suffix causing datetime.datetime # to fail substitution. for attr in list(set(ical_event.required)): - if ical_event.has_key(attr): + if attr in ical_event: self.set_from_ical(attr.lower(), ical_event[attr]) # NOTE: Make sure to list(set()) or duplicates may arise # NOTE: Keep the original order e.g. to read DTSTART before RECURRENCE-ID for attr in list(OrderedDict.fromkeys(ical_event.singletons)): - if ical_event.has_key(attr): + if attr in ical_event: if isinstance(ical_event[attr], list): ical_event[attr] = ical_event[attr][0]; self.set_from_ical(attr.lower(), ical_event[attr]) # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(ical_event.multiple)): - if ical_event.has_key(attr): + if attr in ical_event: self.set_from_ical(attr.lower(), ical_event[attr]) def _xml_from_ical(self, ical): if not "BEGIN:VCALENDAR" in ical: ical = "BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR" from kolab.calendaring import EventCal self.event = EventCal() success = self.event.fromICal(ical) if success: self._load_exceptions() return success def get_attendee_participant_status(self, attendee): return attendee.get_participant_status() def get_attendee(self, attendee): if isinstance(attendee, basestring): if attendee in [x.get_email() for x in self.get_attendees()]: attendee = self.get_attendee_by_email(attendee) elif attendee in [x.get_name() for x in self.get_attendees()]: attendee = self.get_attendee_by_name(attendee) else: raise ValueError(_("No attendee with email or name %r") %(attendee)) return attendee elif isinstance(attendee, Attendee): return attendee else: raise ValueError(_("Invalid argument value attendee %r, must be basestring or Attendee") % (attendee)) def find_attendee(self, attendee): try: return self.get_attendee(attendee) except: return None def get_attendee_by_email(self, email): if email in [x.get_email() for x in self.get_attendees()]: return [x for x in self.get_attendees() if x.get_email() == email][0] raise ValueError(_("No attendee with email %r") %(email)) def get_attendee_by_name(self, name): if name in [x.get_name() for x in self.get_attendees()]: return [x for x in self.get_attendees() if x.get_name() == name][0] raise ValueError(_("No attendee with name %r") %(name)) def get_attendees(self): return self._attendees def get_categories(self): return [str(c) for c in self.event.categories()] def get_classification(self): return self.event.classification() def get_created(self): try: return xmlutils.from_cdatetime(self.event.created(), True) except ValueError: return datetime.datetime.now() def get_description(self): return self.event.description() def get_comment(self): if hasattr(self.event, 'comment'): return self.event.comment() else: return None def get_duration(self): duration = self.event.duration() if duration and duration.isValid(): dtd = datetime.timedelta( days=duration.days(), seconds=duration.seconds(), minutes=duration.minutes(), hours=duration.hours(), weeks=duration.weeks() ) return dtd return None def get_end(self): dt = xmlutils.from_cdatetime(self.event.end(), True) if not dt: duration = self.get_duration() if duration is not None: dt = self.get_start() + duration return dt def get_date_text(self, date_format=None, time_format=None): if date_format is None: date_format = _("%Y-%m-%d") if time_format is None: time_format = _("%H:%M (%Z)") start = self.get_start() end = self.get_end() all_day = not hasattr(start, 'date') start_date = start.date() if not all_day else start end_date = end.date() if not all_day else end if start_date == end_date: end_format = time_format else: end_format = date_format + " " + time_format if all_day: time_format = '' if start_date == end_date: return start.strftime(date_format) return "%s - %s" % (start.strftime(date_format + " " + time_format), end.strftime(end_format)) def get_recurrence_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.recurrenceDates()) def get_exception_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates()) def get_exceptions(self): return self._exceptions def has_exceptions(self): return len(self._exceptions) > 0 def get_attachments(self): return self.event.attachments() def get_attachment_data(self, i): vattach = self.event.attachments() if i < len(vattach): attachment = vattach[i] uri = attachment.uri() if uri and uri[0:4] == 'cid:': # get data from MIME part with matching content-id cid = '<' + uri[4:] + '>' for p in self._attachment_parts: if p['Content-ID'] == cid: return p.get_payload(decode=True) else: return attachment.data() return None def get_alarms(self): return self.event.alarms() def get_ical_attendee(self): # TODO: Formatting, aye? See also the example snippet: # # ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP: # MAILTO:employee-A@host.com attendees = [] for attendee in self.get_attendees(): email = attendee.get_email() name = attendee.get_name() rsvp = attendee.get_rsvp() role = attendee.get_role() partstat = attendee.get_participant_status() cutype = attendee.get_cutype() delegators = attendee.get_delegated_from() delegatees = attendee.get_delegated_to() if rsvp in attendee.rsvp_map: _rsvp = rsvp elif rsvp in attendee.rsvp_map.values(): _rsvp = [k for k, v in attendee.rsvp_map.iteritems() if v == rsvp][0] else: _rsvp = None if role in attendee.role_map: _role = role elif role in attendee.role_map.values(): _role = [k for k, v in attendee.role_map.iteritems() if v == role][0] else: _role = None if partstat in attendee.participant_status_map: _partstat = partstat elif partstat in attendee.participant_status_map.values(): _partstat = [k for k, v in attendee.participant_status_map.iteritems() if v == partstat][0] else: _partstat = None if cutype in attendee.cutype_map: _cutype = cutype elif cutype in attendee.cutype_map.values(): _cutype = [k for k, v in attendee.cutype_map.iteritems() if v == cutype][0] else: _cutype = None _attendee = icalendar.vCalAddress("MAILTO:%s" % email) if not name == None and not name == "": _attendee.params['CN'] = icalendar.vText(name) if not _rsvp == None: _attendee.params['RSVP'] = icalendar.vText(_rsvp) if not _role == None: _attendee.params['ROLE'] = icalendar.vText(_role) if not _partstat == None: _attendee.params['PARTSTAT'] = icalendar.vText(_partstat) if not _cutype == None: _attendee.params['CUTYPE'] = icalendar.vText(_cutype) if not delegators == None and len(delegators) > 0: _attendee.params['DELEGATED-FROM'] = icalendar.vText(delegators[0].email()) if not delegatees == None and len(delegatees) > 0: _attendee.params['DELEGATED-TO'] = icalendar.vText(delegatees[0].email()) attendees.append(_attendee) return attendees def get_ical_attendee_participant_status(self, attendee): attendee = self.get_attendee(attendee) if attendee.get_participant_status() in attendee.participant_status_map: return attendee.get_participant_status() elif attendee.get_participant_status() in attendee.participant_status_map.values(): return [k for k, v in attendee.participant_status_map.iteritems() if v == attendee.get_participant_status()][0] else: raise ValueError(_("Invalid participant status")) def get_ical_created(self): return self.get_created() def get_ical_dtend(self): dtend = self.get_end() # shift end by one day on all-day events if not hasattr(dtend, 'hour'): dtend = dtend + datetime.timedelta(days=1) return dtend def get_ical_dtstamp(self): try: retval = self.get_lastmodified() if retval == None or retval == "": return datetime.datetime.now() except: return datetime.datetime.now() def get_ical_lastmodified(self): return self.get_ical_dtstamp() def get_ical_dtstart(self): return self.get_start() def get_ical_organizer(self): contact = self.get_organizer() organizer = icalendar.vCalAddress("MAILTO:%s" % contact.email()) name = contact.name() if not name == None and not name == "": organizer.params["CN"] = icalendar.vText(name) return organizer def get_ical_status(self): status = self.event.status() if status in self.status_map: return status return self._translate_value(status, self.status_map) if status else None def get_ical_class(self): class_ = self.event.classification() return self._translate_value(class_, self.classification_map) if class_ else None def get_ical_sequence(self): return str(self.event.sequence()) if self.event.sequence() else None def get_ical_comment(self): comment = self.get_comment() if comment is not None: return [ comment ] return None def get_ical_recurrenceid(self): rid = self.get_recurrence_id() if isinstance(rid, datetime.datetime) or isinstance(rid, datetime.date): prop = icalendar.vDatetime(rid) if isinstance(rid, datetime.datetime) else icalendar.vDate(rid) if self.thisandfuture: prop.params.update({'RANGE':'THISANDFUTURE'}) return prop return None def get_ical_rrule(self): result = [] rrule = self.get_recurrence() if rrule.isValid(): result.append(rrule.to_ical()) return result def get_ical_rdate(self): rdates = self.get_recurrence_dates() for i in range(len(rdates)): rdates[i] = icalendar.prop.vDDDLists(rdates[i]) return rdates def get_location(self): return self.event.location() def get_lastmodified(self): try: _datetime = self.event.lastModified() if _datetime == None or not _datetime.isValid(): self.__str__() except: self.__str__() return xmlutils.from_cdatetime(self.event.lastModified(), True) def get_organizer(self): organizer = self.event.organizer() return organizer def get_priority(self): return str(self.event.priority()) def get_start(self): return xmlutils.from_cdatetime(self.event.start(), True) def get_status(self, translated=False): status = self.event.status() if translated: return self._translate_value(status, self.status_map) if status else None return status def get_summary(self): return self.event.summary() def get_uid(self): uid = self.event.uid() if not uid == '': return uid else: self.set_uid(uuid.uuid4()) return self.get_uid() def get_recurrence_id(self): self.thisandfuture = self.event.thisAndFuture(); recurrence_id = xmlutils.from_cdatetime(self.event.recurrenceID(), True) # fix recurrence-id type if stored as date instead of datetime if recurrence_id is not None and isinstance(recurrence_id, datetime.date): dtstart = self.get_start() if not type(recurrence_id) == type(dtstart): recurrence_id = datetime.datetime.combine(recurrence_id, dtstart.time()).replace(tzinfo=dtstart.tzinfo) return recurrence_id def get_thisandfuture(self): self.thisandfuture = self.event.thisAndFuture(); return self.thisandfuture def get_sequence(self): return self.event.sequence() def get_url(self): return self.event.url() def get_transparency(self): return self.event.transparency() def get_recurrence(self): return RecurrenceRule(self.event.recurrenceRule()) def set_attendees(self, _attendees, recursive=False): if recursive: self._attendees = [] self.update_attendees(_attendees, True) else: self._attendees = _attendees self.event.setAttendees(self._attendees) def set_attendee_participant_status(self, attendee, status, rsvp=None): """ Set the participant status of an attendee to status. As the attendee arg, pass an email address or name, for this function to obtain the attendee object by searching the list of attendees for this event. """ attendee = self.get_attendee(attendee) attendee.set_participant_status(status) if rsvp is not None: attendee.set_rsvp(rsvp) # apply update to self and all exceptions self.update_attendees([attendee], False) def update_attendees(self, _attendees, append=True): self.merge_attendee_data(_attendees, append) if len(self._exceptions): vexceptions = self.event.exceptions() for i, exception in enumerate(self._exceptions): exception.merge_attendee_data(_attendees, append) vexceptions[i] = exception.event self.event.setExceptions(vexceptions) def merge_attendee_data(self, _attendees, append=True): for attendee in _attendees: found = False for candidate in self._attendees: if candidate.get_email() == attendee.get_email(): candidate.copy_from(attendee) found = True break if not found and append: self._attendees.append(attendee) self.event.setAttendees(self._attendees) def set_classification(self, classification): if classification in self.classification_map: self.event.setClassification(self.classification_map[classification]) elif classification in self.classification_map.values(): self.event.setClassification(classification) else: raise ValueError(_("Invalid classification %r") % (classification)) def set_created(self, _datetime=None): if _datetime is None or isinstance(_datetime, datetime.time): _datetime = datetime.datetime.utcnow() self.event.setCreated(xmlutils.to_cdatetime(_datetime, False, True)) def set_description(self, description): self.event.setDescription(ustr(description)) def set_comment(self, comment): if hasattr(self.event, 'setComment'): self.event.setComment(ustr(comment)) def set_dtstamp(self, _datetime): self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True)) def set_end(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event end needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setEnd(xmlutils.to_cdatetime(_datetime, True)) def set_exception_dates(self, _datetimes): for _datetime in _datetimes: self.add_exception_date(_datetime) def set_recurrence_dates(self, _datetimes): for _datetime in _datetimes: self.add_recurrence_date(_datetime) def add_custom_property(self, name, value): if not name.upper().startswith('X-'): raise ValueError(_("Invalid custom property name %r") % (name)) props = self.event.customProperties() props.append(kolabformat.CustomProperty(name.upper(), value)) self.event.setCustomProperties(props) def set_from_ical(self, attr, value): attr = attr.replace('-', '') ical_setter = 'set_ical_' + attr default_setter = 'set_' + attr params = value.params if hasattr(value, 'params') else {} if isinstance(value, icalendar.vDDDTypes) and hasattr(value, 'dt'): value = value.dt if attr == "categories": self.add_category(value) elif attr == "class": if (value and value[:2] not in ['X-', 'x-']): self.set_classification(value) elif attr == "recurrenceid": self.set_ical_recurrenceid(value, params) elif hasattr(self, ical_setter): getattr(self, ical_setter)(value) elif hasattr(self, default_setter): getattr(self, default_setter)(value) def set_ical_attendee(self, _attendee): if isinstance(_attendee, basestring): _attendee = [_attendee] if isinstance(_attendee, list): for attendee in _attendee: address = str(attendee).split(':')[-1] if hasattr(attendee, 'params'): params = attendee.params else: params = {} - if params.has_key('CN'): + if 'CN' in params: name = ustr(params['CN']) else: name = None - if params.has_key('ROLE'): + if 'ROLE' in params: role = params['ROLE'] else: role = None - if params.has_key('PARTSTAT'): + if 'PARTSTAT' in params: partstat = params['PARTSTAT'] else: partstat = None - if params.has_key('RSVP'): + if 'RSVP' in params: rsvp = params['RSVP'] else: rsvp = None - if params.has_key('CUTYPE'): + if 'CUTYPE' in params: cutype = params['CUTYPE'] else: cutype = kolabformat.CutypeIndividual att = self.add_attendee(address, name=name, rsvp=rsvp, role=role, participant_status=partstat, cutype=cutype, params=params) def set_ical_dtend(self, dtend): # shift end by one day on all-day events if not hasattr(dtend, 'hour'): dtend = dtend - datetime.timedelta(days=1) self.set_end(dtend) def set_ical_dtstamp(self, dtstamp): self.set_dtstamp(dtstamp) def set_ical_dtstart(self, dtstart): self.set_start(dtstart) def set_ical_lastmodified(self, lastmod): self.set_lastmodified(lastmod) def set_ical_duration(self, value): if hasattr(value, 'dt'): value = value.dt duration = kolabformat.Duration(value.days, 0, 0, value.seconds, False) self.event.setDuration(duration) def set_ical_organizer(self, organizer): address = str(organizer).split(':')[-1] cn = None if hasattr(organizer, 'params'): params = organizer.params else: params = {} - if params.has_key('CN'): + if 'CN' in params: cn = ustr(params['CN']) self.set_organizer(str(address), name=cn) def set_ical_priority(self, priority): self.set_priority(priority) def set_ical_sequence(self, sequence): self.set_sequence(sequence) def set_ical_summary(self, summary): self.set_summary(ustr(summary)) def set_ical_uid(self, uid): self.set_uid(str(uid)) def set_ical_rdate(self, rdate): rdates = [] # rdate here can be vDDDLists or a list of vDDDLists, why?! if isinstance(rdate, icalendar.prop.vDDDLists): rdate = [rdate] for _rdate in rdate: if isinstance(_rdate, icalendar.prop.vDDDLists): tzid = None if hasattr(_rdate, 'params') and 'TZID' in _rdate.params: tzid = _rdate.params['TZID'] dts = icalendar.prop.vDDDLists.from_ical(_rdate.to_ical(), tzid) for datetime in dts: rdates.append(datetime) self.set_recurrence_dates(rdates) def set_ical_recurrenceid(self, value, params): try: self.thisandfuture = params.get('RANGE', '') == 'THISANDFUTURE' self.set_recurrence_id(value, self.thisandfuture) except InvalidEventDateError: pass def set_lastmodified(self, _datetime=None): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): valid_datetime = True if _datetime is None or isinstance(_datetime, datetime.time): valid_datetime = True _datetime = datetime.datetime.utcnow() if not valid_datetime: raise InvalidEventDateError(_("Event last-modified needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True)) def set_location(self, location): self.event.setLocation(ustr(location)) def set_organizer(self, email, name=None): contactreference = ContactReference(email) if not name == None: contactreference.setName(name) self.event.setOrganizer(contactreference) def set_priority(self, priority): self.event.setPriority(priority) def set_sequence(self, sequence): self.event.setSequence(int(sequence)) def set_url(self, url): self.event.setUrl(ustr(url)) def set_recurrence(self, recurrence): self.event.setRecurrenceRule(recurrence) # reset eventcal instance if hasattr(self, 'eventcal'): del self.eventcal def set_start(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event start needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setStart(xmlutils.to_cdatetime(_datetime, True)) def set_status(self, status): if status in self.status_map: self.event.setStatus(self.status_map[status]) elif status in self.status_map.values(): self.event.setStatus(status) elif not status == kolabformat.StatusUndefined: raise InvalidEventStatusError(_("Invalid status set: %r") % (status)) def set_summary(self, summary): self.event.setSummary(summary) def set_uid(self, uid): self.uid = uid self.event.setUid(str(uid)) def set_recurrence_id(self, _datetime, _thisandfuture=None): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, use the one from event start if _datetime.tzinfo == None: _start = self.get_start() _datetime = _datetime.replace(tzinfo=_start.tzinfo) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event recurrence-id needs datetime.datetime instance")) if _thisandfuture is None: _thisandfuture = self.thisandfuture self.event.setRecurrenceID(xmlutils.to_cdatetime(_datetime), _thisandfuture) def set_transparency(self, transp): return self.event.setTransparency(transp) def __str__(self): event_xml = kolabformat.writeEvent(self.event) error = kolabformat.error() if error == None or not error: return event_xml else: raise EventIntegrityError(kolabformat.errorMessage()) def to_dict(self): data = dict() for p, getter in self.properties_map.iteritems(): val = None if hasattr(self, getter): val = getattr(self, getter)() elif hasattr(self.event, getter): val = getattr(self.event, getter)() if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectordatetime): val = [xmlutils.from_cdatetime(x, True) for x in val] elif isinstance(val, kolabformat.vectors): val = [str(x) for x in val] elif isinstance(val, kolabformat.vectorcs): for x in val: data[x.identifier] = x.value val = None elif isinstance(val, kolabformat.ContactReference): val = ContactReference(val).to_dict() elif isinstance(val, kolabformat.RecurrenceRule): val = RecurrenceRule(val).to_dict() elif isinstance(val, kolabformat.vectorattachment): val = [dict(fmttype=x.mimetype(), label=x.label(), uri=x.uri()) for x in val] elif isinstance(val, kolabformat.vectoralarm): val = [self._alarm_to_dict(x) for x in val] elif isinstance(val, list): val = [x.to_dict() for x in val if hasattr(x, 'to_dict')] if val is not None: data[p] = val return data def _alarm_to_dict(self, alarm): ret = dict( action=self._translate_value(alarm.type(), self.alarm_type_map), summary=alarm.summary(), description=alarm.description(), trigger=None ) start = alarm.start() if start and start.isValid(): ret['trigger'] = xmlutils.from_cdatetime(start, True) else: ret['trigger'] = dict(related=self._translate_value(alarm.relativeTo(), self.related_map)) duration = alarm.relativeStart() if duration and duration.isValid(): prefix = '-' if duration.isNegative() else '+' value = prefix + "P%dW%dDT%dH%dM%dS" % ( duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds() ) ret['trigger']['value'] = re.sub(r"T$", '', re.sub(r"0[WDHMS]", '', value)) if alarm.type() == kolabformat.Alarm.EMailAlarm: ret['attendee'] = [ContactReference(a).to_dict() for a in alarm.attendees()] return ret def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) - return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + return name_map[val] if val in name_map else 'UNKNOWN' def to_message(self, creator=None): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate msg = MIMEMultipart() organizer = self.get_organizer() email = organizer.email() name = organizer.name() if creator: msg['From'] = creator elif not name: msg['From'] = email else: msg['From'] = '"%s" <%s>' % (name, email) msg['To'] = ', '.join([x.__str__() for x in self.get_attendees()]) msg['Date'] = formatdate(localtime=True) msg.add_header('X-Kolab-MIME-Version', '3.0') msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.' + self.type) text = utils.multiline_message(""" This is a Kolab Groupware object. To view this object you will need an email client that understands the Kolab Groupware format. For a list of such email clients please visit http://www.kolab.org/ """) msg.attach( MIMEText(text) ) part = MIMEBase('application', "calendar+xml") part.set_charset('UTF-8') msg["Subject"] = self.get_uid() # extract attachment data into separate MIME parts vattach = self.event.attachments() i = 0 for attach in vattach: if attach.uri(): continue mimetype = attach.mimetype() (primary, seconday) = mimetype.split('/') name = attach.label() if not name: name = 'unknown.x' (basename, suffix) = path.splitext(name) t = datetime.datetime.now() cid = "%s.%s.%s%s" % (basename, time.mktime(t.timetuple()), t.microsecond + len(self._attachment_parts), suffix) p = MIMEBase(primary, seconday) p.add_header('Content-Disposition', 'attachment', filename=name) p.add_header('Content-Transfer-Encoding', 'base64') p.add_header('Content-ID', '<' + cid + '>') p.set_payload(base64.b64encode(attach.data())) self._attachment_parts.append(p) # modify attachment object attach.setData('', mimetype) attach.setUri('cid:' + cid, mimetype) vattach[i] = attach i += 1 self.event.setAttachments(vattach) part.set_payload(str(self)) part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"') part.replace_header('Content-Transfer-Encoding', '8bit') msg.attach(part) # append attachment parts for p in self._attachment_parts: msg.attach(p) return msg def to_message_itip(self, from_address, method="REQUEST", participant_status="ACCEPTED", subject=None, message_text=None): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate # encode unicode strings with quoted-printable from email import charset charset.add_charset('utf-8', charset.SHORTEST, charset.QP) msg = MIMEMultipart("alternative") msg_from = None attendees = None if method == "REPLY": # TODO: Make user friendly name msg['To'] = self.get_organizer().email() attendees = self.get_attendees() reply_attendees = [] # There's an exception here for delegation (partstat DELEGATED) for attendee in attendees: if attendee.get_email() == from_address: # Only the attendee is supposed to be listed in a reply attendee.set_participant_status(participant_status) attendee.set_rsvp(False) reply_attendees.append(attendee) name = attendee.get_name() email = attendee.get_email() if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) elif from_address in attendee.get_delegated_from(True): reply_attendees.append(attendee) # keep only replying (and delegated) attendee(s) self._attendees = reply_attendees self.event.setAttendees(self._attendees) if msg_from == None: organizer = self.get_organizer() email = organizer.email() name = organizer.name() if email == from_address: if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) elif method == "REQUEST": organizer = self.get_organizer() email = organizer.email() name = organizer.name() if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) if msg_from == None: if from_address == None: log.error(_("No sender specified")) else: msg_from = from_address msg['From'] = utils.str2unicode(msg_from) msg['Date'] = formatdate(localtime=True) if subject is None: subject = _("Invitation for %s was %s") % (self.get_summary(), participant_status_label(participant_status)) msg['Subject'] = utils.str2unicode(subject) if message_text is None: message_text = _("""This is an automated response to one of your event requests.""") msg.attach(MIMEText(utils.stripped_message(message_text), _charset='utf-8')) part = MIMEBase('text', 'calendar', charset='UTF-8', method=method) del part['MIME-Version'] # mime parts don't need this part.set_payload(self.as_string_itip(method=method)) part.add_header('Content-Transfer-Encoding', '8bit') msg.attach(part) # restore the original list of attendees # attendees being reduced to the replying attendee above if attendees is not None: self._attendees = attendees self.event.setAttendees(self._attendees) return msg def is_recurring(self): return self.event.recurrenceRule().isValid() or len(self.get_recurrence_dates()) > 0 def to_event_cal(self): from kolab.calendaring import EventCal return EventCal(self.event) def get_next_occurence(self, _datetime): if not hasattr(self, 'eventcal'): self.eventcal = self.to_event_cal() next_cdatetime = self.eventcal.getNextOccurence(xmlutils.to_cdatetime(_datetime, True)) next_datetime = xmlutils.from_cdatetime(next_cdatetime, True) if next_cdatetime is not None else None # cut infinite recurrence at a reasonable point if next_datetime and not self.get_last_occurrence() and next_datetime > xmlutils.to_dt(self._recurrence_end()): next_datetime = None # next_datetime is always a cdatetime, convert to date if necessary if next_datetime and not isinstance(self.get_start(), datetime.datetime): next_datetime = datetime.date(next_datetime.year, next_datetime.month, next_datetime.day) return next_datetime def get_occurence_end_date(self, datetime): if not datetime: return None if not hasattr(self, 'eventcal'): return None end_cdatetime = self.eventcal.getOccurenceEndDate(xmlutils.to_cdatetime(datetime, True)) return xmlutils.from_cdatetime(end_cdatetime, True) if end_cdatetime is not None else None def get_last_occurrence(self, force=False): if not hasattr(self, 'eventcal'): self.eventcal = self.to_event_cal() last = self.eventcal.getLastOccurrence() last_datetime = xmlutils.from_cdatetime(last, True) if last is not None else None # we're forced to return some date if last_datetime is None and force: last_datetime = self._recurrence_end() return last_datetime def get_next_instance(self, datetime): next_start = self.get_next_occurence(datetime) if next_start: instance = Event(from_string=str(self)) instance.set_start(next_start) instance.event.setRecurrenceID(xmlutils.to_cdatetime(next_start), False) next_end = self.get_occurence_end_date(next_start) if next_end: instance.set_end(next_end) # unset recurrence rule and exceptions instance.set_recurrence(kolabformat.RecurrenceRule()) instance.event.setExceptions(kolabformat.vectorevent()) instance.event.setExceptionDates(kolabformat.vectordatetime()) instance._exceptions = [] instance._isexception = False # unset attachments list (only stored in main event) instance.event.setAttachments(kolabformat.vectorattachment()) # copy data from matching exception # (give precedence to single occurrence exceptions over thisandfuture) for exception in self._exceptions: recurrence_id = exception.get_recurrence_id() if recurrence_id == next_start and (not exception.thisandfuture or not instance._isexception): instance = exception instance._isexception = True if not exception.thisandfuture: break elif exception.thisandfuture and next_start > recurrence_id: # TODO: merge exception properties over this instance + adjust start/end with the according offset pass return instance return None def get_instance(self, _datetime): # If no timezone information is given, use the one from event start if isinstance(_datetime, datetime.datetime) and _datetime.tzinfo == None: _start = self.get_start() if hasattr(_start, 'tzinfo'): _datetime = _datetime.replace(tzinfo=_start.tzinfo) if self.is_recurring(): instance = self.get_next_instance(_datetime - datetime.timedelta(days=1)) while instance: recurrence_id = instance.get_recurrence_id() if type(recurrence_id) == type(_datetime) and recurrence_id <= _datetime: if xmlutils.dates_equal(recurrence_id, _datetime): return instance instance = self.get_next_instance(instance.get_start()) else: break elif self.has_exceptions(): for exception in self._exceptions: recurrence_id = exception.get_recurrence_id() if type(recurrence_id) == type(_datetime) and xmlutils.dates_equal(recurrence_id, _datetime): return exception if self.get_recurrence_id(): recurrence_id = self.get_recurrence_id() if type(recurrence_id) == type(_datetime) and xmlutils.dates_equal(recurrence_id, _datetime): return self return None def _recurrence_end(self): """ Determine a reasonable end date for infinitely recurring events """ rrule = self.event.recurrenceRule() if rrule.isValid() and rrule.count() < 0 and not rrule.end().isValid(): now = datetime.datetime.now() switch = { kolabformat.RecurrenceRule.Yearly: 100, kolabformat.RecurrenceRule.Monthly: 20 } intvl = switch[rrule.frequency()] if rrule.frequency() in switch else 10 return self.get_start().replace(year=now.year + intvl) return xmlutils.from_cdatetime(rrule.end(), True) class EventIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidEventDateError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidEventStatusError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/note.py b/pykolab/xml/note.py index 030b1df..735a098 100644 --- a/pykolab/xml/note.py +++ b/pykolab/xml/note.py @@ -1,138 +1,138 @@ import pytz import datetime import kolabformat from pykolab.translate import _ from pykolab.xml import utils as xmlutils from pykolab.xml.utils import ustr def note_from_string(string): _xml = kolabformat.readNote(string, False) return Note(_xml) def note_from_message(message): note = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/vnd.kolab+xml": payload = part.get_payload(decode=True) note = note_from_string(payload) # append attachment parts to Note object - elif note and part.has_key('Content-ID'): + elif note and 'Content-ID' in part: note._attachment_parts.append(part) return note class Note(kolabformat.Note): type = 'note' classification_map = { 'PUBLIC': kolabformat.ClassPublic, 'PRIVATE': kolabformat.ClassPrivate, 'CONFIDENTIAL': kolabformat.ClassConfidential, } properties_map = { 'uid': 'get_uid', 'summary': 'summary', 'description': 'description', 'created': 'get_created', 'lastmodified-date': 'get_lastmodified', 'classification': 'get_classification', 'categories': 'categories', 'color': 'color', } def __init__(self, *args, **kw): self._attachment_parts = [] kolabformat.Note.__init__(self, *args, **kw) def get_uid(self): uid = self.uid() if not uid == '': return uid else: self.__str__() return kolabformat.getSerializedUID() def get_created(self): try: return xmlutils.from_cdatetime(self.created(), True) except ValueError: return datetime.datetime.now() def get_lastmodified(self): try: _datetime = self.lastModified() if _datetime == None or not _datetime.isValid(): self.__str__() except: return datetime.datetime.now(pytz.utc) return xmlutils.from_cdatetime(self.lastModified(), True) def set_summary(self, summary): self.setSummary(ustr(summary)) def set_description(self, description): self.setDescription(ustr(description)) def get_classification(self, translated=True): _class = self.classification() if translated: return self._translate_value(_class, self.classification_map) return _class def set_classification(self, classification): if classification in self.classification_map: self.setClassification(self.classification_map[classification]) elif classification in self.classification_map.values(): self.setClassification(status) else: raise ValueError(_("Invalid classification %r") % (classification)) def add_category(self, category): _categories = self.categories() _categories.append(ustr(category)) self.setCategories(_categories) def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) - return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + return name_map[val] if val in name_map else 'UNKNOWN' def to_dict(self): if not self.isValid(): return None data = dict() for p, getter in self.properties_map.iteritems(): val = None if hasattr(self, getter): val = getattr(self, getter)() if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectori): val = [int(x) for x in val] elif isinstance(val, kolabformat.vectors): val = [str(x) for x in val] if val is not None: data[p] = val return data def __str__(self): xml = kolabformat.writeNote(self) error = kolabformat.error() if error == None or not error: return xml else: raise NoteIntegrityError(kolabformat.errorMessage()) class NoteIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py index 05d651c..52058ca 100644 --- a/pykolab/xml/recurrence_rule.py +++ b/pykolab/xml/recurrence_rule.py @@ -1,219 +1,219 @@ import pytz import icalendar import datetime import kolabformat from pykolab.xml import utils as xmlutils from pykolab.translate import _ from pykolab.translate import N_ """ def setFrequency(self, *args): return _kolabformat.RecurrenceRule_setFrequency(self, *args) def frequency(self): return _kolabformat.RecurrenceRule_frequency(self) def setWeekStart(self, *args): return _kolabformat.RecurrenceRule_setWeekStart(self, *args) def weekStart(self): return _kolabformat.RecurrenceRule_weekStart(self) def setEnd(self, *args): return _kolabformat.RecurrenceRule_setEnd(self, *args) def end(self): return _kolabformat.RecurrenceRule_end(self) def setCount(self, *args): return _kolabformat.RecurrenceRule_setCount(self, *args) def count(self): return _kolabformat.RecurrenceRule_count(self) def setInterval(self, *args): return _kolabformat.RecurrenceRule_setInterval(self, *args) def interval(self): return _kolabformat.RecurrenceRule_interval(self) def setBysecond(self, *args): return _kolabformat.RecurrenceRule_setBysecond(self, *args) def bysecond(self): return _kolabformat.RecurrenceRule_bysecond(self) def setByminute(self, *args): return _kolabformat.RecurrenceRule_setByminute(self, *args) def byminute(self): return _kolabformat.RecurrenceRule_byminute(self) def setByhour(self, *args): return _kolabformat.RecurrenceRule_setByhour(self, *args) def byhour(self): return _kolabformat.RecurrenceRule_byhour(self) def setByday(self, *args): return _kolabformat.RecurrenceRule_setByday(self, *args) def byday(self): return _kolabformat.RecurrenceRule_byday(self) def setBymonthday(self, *args): return _kolabformat.RecurrenceRule_setBymonthday(self, *args) def bymonthday(self): return _kolabformat.RecurrenceRule_bymonthday(self) def setByyearday(self, *args): return _kolabformat.RecurrenceRule_setByyearday(self, *args) def byyearday(self): return _kolabformat.RecurrenceRule_byyearday(self) def setByweekno(self, *args): return _kolabformat.RecurrenceRule_setByweekno(self, *args) def byweekno(self): return _kolabformat.RecurrenceRule_byweekno(self) def setBymonth(self, *args): return _kolabformat.RecurrenceRule_setBymonth(self, *args) def bymonth(self): return _kolabformat.RecurrenceRule_bymonth(self) def isValid(self): return _kolabformat.RecurrenceRule_isValid(self) """ frequency_labels = { "YEARLY": N_("Every %d year(s)"), "MONTHLY": N_("Every %d month(s)"), "WEEKLY": N_("Every %d week(s)"), "DAILY": N_("Every %d day(s)"), "HOURLY": N_("Every %d hours"), "MINUTELY": N_("Every %d minutes"), "SECONDLY": N_("Every %d seconds") } def frequency_label(freq): - return _(frequency_labels[freq]) if frequency_labels.has_key(freq) else _(freq) + return _(frequency_labels[freq]) if freq in frequency_labels else _(freq) class RecurrenceRule(kolabformat.RecurrenceRule): frequency_map = { None: kolabformat.RecurrenceRule.FreqNone, "YEARLY": kolabformat.RecurrenceRule.Yearly, "MONTHLY": kolabformat.RecurrenceRule.Monthly, "WEEKLY": kolabformat.RecurrenceRule.Weekly, "DAILY": kolabformat.RecurrenceRule.Daily, "HOURLY": kolabformat.RecurrenceRule.Hourly, "MINUTELY": kolabformat.RecurrenceRule.Minutely, "SECONDLY": kolabformat.RecurrenceRule.Secondly } weekday_map = { "MO": kolabformat.Monday, "TU": kolabformat.Tuesday, "WE": kolabformat.Wednesday, "TH": kolabformat.Thursday, "FR": kolabformat.Friday, "SA": kolabformat.Saturday, "SU": kolabformat.Sunday } properties_map = { 'freq': 'get_frequency', 'interval': 'interval', 'count': 'count', 'until': 'end', 'bymonth': 'bymonth', 'byday': 'byday', 'bymonthday':'bymonthday', 'byyearday': 'byyearday', 'byweekno': 'byweekno', 'byhour': 'byhour', 'byminute': 'byminute', 'wkst': 'get_weekstart' } def __init__(self, rrule=None): if rrule == None: kolabformat.RecurrenceRule.__init__(self) else: kolabformat.RecurrenceRule.__init__(self, rrule) def from_ical(self, vrecur): vectorimap = { 'BYSECOND': 'setBysecond', 'BYMINUTE': 'setByminute', 'BYHOUR': 'setByhour', 'BYMONTHDAY': 'setBymonthday', 'BYYEARDAY': 'setByyearday', 'BYMONTH': 'setBymonth', } settermap = { 'FREQ': 'set_frequency', 'INTERVAL': 'set_interval', 'COUNT': 'set_count', 'UNTIL': 'set_until', 'WKST': 'set_weekstart', 'BYDAY': 'set_byday', } for prop,setter in vectorimap.items(): - if vrecur.has_key(prop): + if prop in vrecur: getattr(self, setter)([int(v) for v in vrecur[prop]]) for prop,setter in settermap.items(): - if vrecur.has_key(prop): + if prop in vrecur: getattr(self, setter)(vrecur[prop]) def set_count(self, count): if isinstance(count, list): count = count[0] self.setCount(int(count)) def set_interval(self, val): if isinstance(val, list): val = val[0] self.setInterval(int(val)) def set_frequency(self, freq): self._set_map_value(freq, self.frequency_map, 'setFrequency') def get_frequency(self, translated=False): freq = self.frequency() if translated: return self._translate_value(freq, self.frequency_map) return freq def set_byday(self, bdays): daypos = kolabformat.vectordaypos() for wday in bdays: if isinstance(wday, str): wday = icalendar.vWeekday(wday) weekday = str(wday)[-2:] occurrence = int(wday.relative) if str(wday)[0] == '-': occurrence = occurrence * -1 - if self.weekday_map.has_key(weekday): + if weekday in self.weekday_map: daypos.append(kolabformat.DayPos(occurrence, self.weekday_map[weekday])) self.setByday(daypos) def set_weekstart(self, wkst): self._set_map_value(wkst, self.weekday_map, 'setWeekStart') def get_weekstart(self, translated=False): wkst = self.weekStart() if translated: return self._translate_value(wkst, self.weekday_map) return wkst def set_until(self, until): if isinstance(until, list): until = until[0] if isinstance(until, datetime.datetime) or isinstance(until, datetime.date): # move into UTC timezone according to RFC 5545 if isinstance(until, datetime.datetime): until = until.astimezone(pytz.utc) self.setEnd(xmlutils.to_cdatetime(until, True)) def _set_map_value(self, val, pmap, setter): if isinstance(val, list): val = val[0] if val in pmap: getattr(self, setter)(pmap[val]) elif val in pmap.values(): getattr(self, setter)(val) def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) - return name_map[val] if name_map.has_key(val) else 'UNKNOWN' + return name_map[val] if val in name_map else 'UNKNOWN' def to_ical(self): rrule = icalendar.vRecur(dict((k,v) for k,v in self.to_dict(True).items() if not (type(v) == str and v == '' or type(v) == list and len(v) == 0))) return rrule def to_dict(self, raw=False): if not self.isValid() or self.frequency() == kolabformat.RecurrenceRule.FreqNone: return None data = dict() for p, getter in self.properties_map.iteritems(): val = None args = {} if hasattr(self, getter): if getter.startswith('get_'): args = dict(translated=True) if hasattr(self, getter): val = getattr(self, getter)(**args) if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectori): val = [int(x) for x in val] elif isinstance(val, kolabformat.vectordaypos): val = ["%s%s" % (str(x.occurence()) if x.occurence() != 0 else '', self._translate_value(x.weekday(), self.weekday_map)) for x in val] if not raw and isinstance(val, list): val = ",".join(val) if val is not None: data[p] = val return data diff --git a/pykolab/xml/todo.py b/pykolab/xml/todo.py index 1c711b5..effb8be 100644 --- a/pykolab/xml/todo.py +++ b/pykolab/xml/todo.py @@ -1,267 +1,267 @@ import datetime import kolabformat import icalendar import pytz import base64 import pykolab from pykolab import constants from pykolab.xml import Event from pykolab.xml import RecurrenceRule from pykolab.xml import utils as xmlutils from pykolab.xml.event import InvalidEventDateError from pykolab.translate import _ log = pykolab.getLogger('pykolab.xml_todo') def todo_from_ical(ical, string=None): return Todo(from_ical=ical, from_string=string) def todo_from_string(string): return Todo(from_string=string) def todo_from_message(message): todo = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/calendar+xml": payload = part.get_payload(decode=True) todo = todo_from_string(payload) # append attachment parts to Todo object - elif todo and part.has_key('Content-ID'): + elif todo and 'Content-ID' in part: todo._attachment_parts.append(part) return todo # FIXME: extend a generic pykolab.xml.Xcal class instead of Event class Todo(Event): type = 'task' # This have to be a copy (see T1221) properties_map = Event.properties_map.copy() def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] self._exceptions = [] self._attachment_parts = [] self.properties_map.update({ "due": "get_due", "percent-complete": "get_percentcomplete", "related-to": "get_related_to", "duration": "void", "end": "void" }) if isinstance(from_ical, str) and from_ical == "": if from_string == "": self.event = kolabformat.Todo() else: self.event = kolabformat.readTodo(from_string, False) self._load_attendees() else: self.from_ical(from_ical, from_string) self.set_created(self.get_created()) self.uid = self.get_uid() def from_ical(self, ical, raw): if isinstance(ical, icalendar.Todo): ical_todo = ical elif hasattr(icalendar.Todo, 'from_ical'): ical_todo = icalendar.Todo.from_ical(ical) elif hasattr(icalendar.Todo, 'from_string'): ical_todo = icalendar.Todo.from_string(ical) # VCALENDAR block was given, find the first VTODO item if isinstance(ical_todo, icalendar.Calendar): for c in ical_todo.walk(): if c.name == 'VTODO': ical_todo = c break - log.debug("Todo.from_ical(); %r, %r, %r" % (type(ical_todo), ical_todo.has_key('ATTACH'), ical_todo.has_key('ATTENDEE')), level=8) + log.debug("Todo.from_ical(); %r, %r, %r" % (type(ical_todo), 'ATTACH' in ical_todo, 'ATTENDEE' in ical_todo), level=8) # DISABLED: use the libkolab calendaring bindings to load the full iCal data # TODO: this requires support for iCal parsing in the kolab.calendaring bindings - if False and ical_todo.has_key('ATTACH') or [part for part in ical_todo.walk() if part.name == 'VALARM']: + if False and 'ATTACH' in ical_todo or [part for part in ical_todo.walk() if part.name == 'VALARM']: if raw is None or raw == "": raw = ical if isinstance(ical, str) else ical.to_ical() self._xml_from_ical(raw) else: self.event = kolabformat.Todo() for attr in list(set(ical_todo.required)): - if ical_todo.has_key(attr): + if attr in ical_todo: self.set_from_ical(attr.lower(), ical_todo[attr]) for attr in list(set(ical_todo.singletons)): - if ical_todo.has_key(attr): + if attr in ical_todo: if isinstance(ical_todo[attr], list): ical_todo[attr] = ical_todo[attr][0]; self.set_from_ical(attr.lower(), ical_todo[attr]) for attr in list(set(ical_todo.multiple)): - if ical_todo.has_key(attr): + if attr in ical_todo: self.set_from_ical(attr.lower(), ical_todo[attr]) # although specified by RFC 2445/5545, icalendar doesn't have this property listed - if ical_todo.has_key('PERCENT-COMPLETE'): + if 'PERCENT-COMPLETE' in ical_todo: self.set_from_ical('percentcomplete', ical_todo['PERCENT-COMPLETE']) def _xml_from_ical(self, ical): # FIXME: kolabformat or kolab.calendaring modules do not provide bindings to import Todo from iCal self.event = Todo() def set_ical_attach(self, attachment): if hasattr(attachment, 'params'): params = attachment.params else: params = {} _attachment = kolabformat.Attachment() - if params.has_key('FMTTYPE'): + if 'FMTTYPE' in params: mimetype = str(params['FMTTYPE']) else: mimetype = 'application/octet-stream' - if params.has_key('X-LABEL'): + if 'X-LABEL' in params: _attachment.setLabel(str(params['X-LABEL'])) decode = False - if params.has_key('ENCODING'): + if 'ENCODING' in params: if params['ENCODING'] == "BASE64" or params['ENCODING'] == "B": decode = True _attachment.setData(base64.b64decode(str(attachment)) if decode else str(attachment), mimetype) vattach = self.event.attachments() vattach.append(_attachment) self.event.setAttachments(vattach) def set_ical_rrule(self, rrule): _rrule = RecurrenceRule() _rrule.from_ical(rrule) if _rrule.isValid(): self.event.setRecurrenceRule(_rrule) def set_ical_due(self, due): self.set_due(due) def set_due(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Todo due needs datetime.date or datetime.datetime instance")) self.event.setDue(xmlutils.to_cdatetime(_datetime, True)) def set_ical_percent(self, percent): self.set_percentcomplete(percent) def set_percentcomplete(self, percent): self.event.setPercentComplete(int(percent)) def set_transparency(self, transp): # empty stub pass def get_due(self): return xmlutils.from_cdatetime(self.event.due(), True) def get_ical_due(self): dt = self.get_due() if dt: return icalendar.vDatetime(dt) return None def get_percentcomplete(self): return self.event.percentComplete() def get_duration(self): return None def get_related_to(self): for x in self.event.relatedTo(): return x return None def as_string_itip(self, method="REQUEST"): cal = icalendar.Calendar() cal.add( 'prodid', '-//pykolab-%s-%s//kolab.org//' % ( constants.__version__, constants.__release__ ) ) cal.add('version', '2.0') cal.add('calscale', 'GREGORIAN') cal.add('method', method) ical_todo = icalendar.Todo() singletons = list(set(ical_todo.singletons)) singletons.extend(['PERCENT-COMPLETE']) for attr in singletons: ical_getter = 'get_ical_%s' % (attr.lower()) default_getter = 'get_%s' % (attr.lower()) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() if not retval == None and not retval == "": ical_todo.add(attr.lower(), retval) elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if not retval == None and not retval == "": ical_todo.add(attr.lower(), retval, encode=0) for attr in list(set(ical_todo.multiple)): ical_getter = 'get_ical_%s' % (attr.lower()) default_getter = 'get_%s' % (attr.lower()) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if isinstance(retval, list) and not len(retval) == 0: for _retval in retval: ical_todo.add(attr.lower(), _retval, encode=0) # copy custom properties to iCal for cs in self.event.customProperties(): ical_todo.add(cs.identifier, cs.value) cal.add_component(ical_todo) if hasattr(cal, 'to_ical'): return cal.to_ical() elif hasattr(cal, 'as_string'): return cal.as_string() def __str__(self): xml = kolabformat.writeTodo(self.event) error = kolabformat.error() if error == None or not error: return xml else: raise TodoIntegrityError(kolabformat.errorMessage()) class TodoIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py index 5a58bf5..3e3fc9e 100644 --- a/pykolab/xml/utils.py +++ b/pykolab/xml/utils.py @@ -1,369 +1,369 @@ import datetime import pytz import kolabformat from dateutil.tz import tzlocal from collections import OrderedDict from pykolab.translate import _ from pykolab.translate import N_ def to_dt(dt): """ Convert a naive date or datetime to a tz-aware datetime. """ if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime) or dt is not None and not hasattr(dt, 'hour'): dt = datetime.datetime(dt.year, dt.month, dt.day, 0, 0, 0, 0, tzinfo=pytz.utc) elif isinstance(dt, datetime.datetime): if dt.tzinfo == None: return dt.replace(tzinfo=pytz.utc) return dt def from_cdatetime(_cdatetime, with_timezone=True): """ Convert from kolabformat.cDateTime to datetime.date(time) """ if not _cdatetime.isValid(): return None ( year, month, day, ) = ( _cdatetime.year(), _cdatetime.month(), _cdatetime.day(), ) if _cdatetime.hour() == None or _cdatetime.hour() < 0: return datetime.date(year, month, day) ( hour, minute, second ) = ( _cdatetime.hour(), _cdatetime.minute(), _cdatetime.second() ) if with_timezone: _timezone = _cdatetime.timezone() if _timezone == '' or _timezone == None: _dt = datetime.datetime(year, month, day, hour, minute, second, tzinfo=pytz.utc) else: try: # use pytz.timezone.localize() to correctly set DST in tzinfo according to the given date _tz = pytz.timezone(_timezone) _dt = _tz.localize(datetime.datetime(year, month, day, hour, minute, second)) except: # fall back to local time _dt = datetime.datetime(year, month, day, hour, minute, second) return _dt else: return datetime.datetime(year, month, day, hour, minute, second) def to_cdatetime(_datetime, with_timezone=True, as_utc=False): """ Convert a datetime.dateime object into a kolabformat.cDateTime instance """ # convert date into UTC timezone if as_utc and hasattr(_datetime, "tzinfo"): if _datetime.tzinfo is not None: _datetime = _datetime.astimezone(pytz.utc) else: datetime = _datetime.replace(tzinfo=pytz.utc) with_timezone = False # Sometimes we deal with dummy 00000000T000000 values from iCalendar # in such cases we end up with datetime.time objects if not hasattr(_datetime, 'year'): (year, month, day) = (1970, 1, 1) else: (year, month, day) = (_datetime.year, _datetime.month, _datetime.day) if hasattr(_datetime, 'hour'): (hour, minute, second) = (_datetime.hour, _datetime.minute, _datetime.second) _cdatetime = kolabformat.cDateTime(year, month, day, hour, minute, second) else: _cdatetime = kolabformat.cDateTime(year, month, day) if with_timezone and hasattr(_datetime, "tzinfo"): if _datetime.tzinfo.__str__() in ['UTC','GMT']: _cdatetime.setUTC(True) else: _cdatetime.setTimezone(_datetime.tzinfo.__str__()) if as_utc: _cdatetime.setUTC(True) return _cdatetime def dates_equal(a, b): date_format = '%Y%m%d' if isinstance(a, datetime.date) and isinstance(b, datetime.date) else '%Y%m%dT%H%M%S' return type(a) == type(b) and a.strftime(date_format) == b.strftime(date_format) def ustr(s): """ Force the given (unicode) string into UTF-8 encoding """ if not isinstance(s, unicode): for cs in ['utf-8','latin-1']: try: s = unicode(s, cs) break except: pass if isinstance(s, unicode): return s.encode('utf-8') return s property_labels = { "name": N_("Name"), "summary": N_("Summary"), "location": N_("Location"), "description": N_("Description"), "url": N_("URL"), "status": N_("Status"), "priority": N_("Priority"), "attendee": N_("Attendee"), "start": N_("Start"), "end": N_("End"), "due": N_("Due"), "rrule": N_("Repeat"), "exdate": N_("Repeat Exception"), "organizer": N_("Organizer"), "attach": N_("Attachment"), "alarm": N_("Alarm"), "classification": N_("Classification"), "percent-complete": N_("Progress") } def property_label(propname): """ Return a localized name for the given object property """ - return _(property_labels[propname]) if property_labels.has_key(propname) else _(propname) + return _(property_labels[propname]) if propname in property_labels else _(propname) def property_to_string(propname, value): """ Render a human readable string for the given object property """ date_format = _("%Y-%m-%d") time_format = _("%H:%M (%Z)") date_time_format = date_format + " " + time_format maxlen = 50 if isinstance(value, datetime.datetime): return value.strftime(date_time_format) elif isinstance(value, datetime.date): return value.strftime(date_format) elif isinstance(value, int): return str(value) elif isinstance(value, str): if len(value) > maxlen: return value[:maxlen].rsplit(' ', 1)[0] + '...' return value elif isinstance(value, object) and hasattr(value, 'to_dict'): value = value.to_dict() if isinstance(value, dict): if propname == 'attendee': from . import attendee - name = value['name'] if value.has_key('name') and not value['name'] == '' else value['email'] + name = value['name'] if 'name' in value and not value['name'] == '' else value['email'] return "%s, %s" % (name, attendee.participant_status_label(value['partstat'])) elif propname == 'organizer': - return value['name'] if value.has_key('name') and not value['name'] == '' else value['email'] + return value['name'] if 'name' in value and not value['name'] == '' else value['email'] elif propname == 'rrule': from . import recurrence_rule rrule = recurrence_rule.frequency_label(value['freq']) % (value['interval']) - if value.has_key('count') and value['count'] > 0: + if 'count' in value and value['count'] > 0: rrule += " " + _("for %d times") % (value['count']) - elif value.has_key('until') and (isinstance(value['until'], datetime.datetime) or isinstance(value['until'], datetime.date)): + elif 'until' in value and (isinstance(value['until'], datetime.datetime) or isinstance(value['until'], datetime.date)): rrule += " " + _("until %s") % (value['until'].strftime(date_format)) return rrule elif propname == 'alarm': alarm_type_labels = { 'DISPLAY': _("Display message"), 'EMAIL': _("Send email"), 'AUDIO': _("Play sound") } alarm = alarm_type_labels.get(value['action'], "") if isinstance(value['trigger'], datetime.datetime): alarm += " @ " + property_to_string('trigger', value['trigger']) else: rel = _("%s after") if value['trigger']['related'] == 'END' else _("%s before") offsets = [] try: from icalendar import vDuration duration = vDuration.from_ical(value['trigger']['value'].strip('-')) except: return None if duration.days: offsets.append(_("%d day(s)") % (duration.days)) if duration.seconds: hours = duration.seconds // 3600 minutes = duration.seconds % 3600 // 60 seconds = duration.seconds % 60 if hours: offsets.append(_("%d hour(s)") % (hours)) if minutes or (hours and seconds): offsets.append(_("%d minute(s)") % (minutes)) if len(offsets): alarm += " " + rel % (", ".join(offsets)) return alarm elif propname == 'attach': - return value['label'] if value.has_key('label') else value['fmttype'] + return value['label'] if 'label' in value else value['fmttype'] return None def compute_diff(a, b, reduced=False): """ List the differences between two given dicts """ diff = [] properties = a.keys() properties.extend([x for x in b if x not in properties]) for prop in properties: - aa = a[prop] if a.has_key(prop) else None - bb = b[prop] if b.has_key(prop) else None + aa = a[prop] if prop in a else None + bb = b[prop] if prop in b else None # compare two lists if isinstance(aa, list) or isinstance(bb, list): if not isinstance(aa, list): aa = [aa] if not isinstance(bb, list): bb = [bb] (aa, bb) = order_proplists(aa, bb) index = 0 length = max(len(aa), len(bb)) while index < length: aai = aa[index] if index < len(aa) else None bbi = bb[index] if index < len(bb) else None if not compare_values(aai, bbi): (old, new) = reduce_properties(aai, bbi) if reduced else (aai, bbi) diff.append(OrderedDict([('property', prop), ('index', index), ('old', old), ('new', new)])) index += 1 # the two properties differ elif not compare_values(aa, bb): if reduced: (old, new) = reduce_properties(aa, bb) else: (old, new) = (aa, bb) diff.append(OrderedDict([('property', prop), ('old', old), ('new', new)])) return diff def order_proplists(a, b): """ Orders two lists so that equal entries have the same position """ # nothing to be done here if len(a) == 0 and len(b) == 0: return (a, b) base = a comp = b flip = False if len(a) > len(b): flip = True base = b comp = a indices = [] top = len(comp) + 1 for bb in comp: index = None # find a matching entry in base for j, aa in enumerate(base): if compare_values(aa, bb, True): index = j break # move non-matching items to the end of the list if index is None: index = top top += 1 indices.append(index) # do sort by indices indices, comp = zip(*sorted(zip(indices, comp), key=lambda x: x[0])) return (comp, base) if flip else (base, comp) def compare_values(aa, bb, partial=False): ignore_keys = ['rsvp'] if not aa.__class__ == bb.__class__: return False if isinstance(aa, dict) and isinstance(bb, dict): aa = dict(aa) bb = dict(bb) # ignore some properties for comparison for k in ignore_keys: aa.pop(k, None) bb.pop(k, None) # accept partial match if partial: for k,v in aa.iteritems(): - if bb.has_key(k) and bb[k] == v: + if k in bb and bb[k] == v: return True return False return aa == bb def reduce_properties(aa, bb): """ Compares two given structs and removes equal values in bb """ if not isinstance(aa, dict) or not isinstance(bb, dict): return (aa, bb) properties = aa.keys() properties.extend([x for x in bb if x not in properties]) for prop in properties: - if not aa.has_key(prop) or not bb.has_key(prop): + if prop not in aa or prop not in bb: continue if isinstance(aa[prop], dict) and isinstance(bb[prop], dict): (aa[prop], bb[prop]) = reduce_properties(aa[prop], bb[prop]) if aa[prop] == bb[prop]: # del aa[prop] del bb[prop] return (aa, bb) diff --git a/wallace/module_gpgencrypt.py b/wallace/module_gpgencrypt.py index 100d1d1..9083055 100644 --- a/wallace/module_gpgencrypt.py +++ b/wallace/module_gpgencrypt.py @@ -1,293 +1,293 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3 or, at your option, any later version # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # import os import tempfile import time from email import message_from_string from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.parser import Parser from email.utils import formataddr from email.utils import getaddresses import email.mime.application import email.mime.multipart import email.mime.text import email.encoders import gnupg import modules import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/gpgencrypt') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/gpgencrypt/' def __init__(): modules.register('gpgencrypt', execute, description=description()) def description(): return """Encrypt messages to the recipient(s).""" def pgp_mime(msg, recepients): gpg = gnupg.GPG(gnupghome='/var/lib/kolab/.gnupg', verbose=conf.debuglevel > 8) gpg.encoding = 'utf-8' msg = msg msg_boundary = str(msg.get_boundary()) msg_content_type = str(msg.get_content_type()) payload = msg.get_payload() content = "Content-Type: " + msg_content_type + ";" + "\n boundary=\"" + msg_boundary + "\"\n\n" + payload encrypted_content = gpg.encrypt(content, recepients, always_trust=True) msg.set_type("multipart/encrypted") msg.set_param("protocol","application/pgp-encrypted") msg_boundary_gpg = "--boundary-gpg-encryption-42" msg_preamble = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n\ " + msg_boundary_gpg + "\n\ Content-Type: application/pgp-encrypted\n\ Content-Description: PGP/MIME version identification\n\ \n\ Version: 1\n\ \n\ " + msg_boundary_gpg + "\n\ Content-Type: application/octet-stream; name=\"encrypted.asc\"\n\ Content-Description: OpenPGP encrypted message\n\ Content-Disposition: inline; filename=\"encrypted.asc\"\n\n" msg.set_boundary(msg_boundary_gpg) msg.set_payload(msg_preamble + str(encrypted_content) + "\n" + msg_boundary_gpg) return msg def execute(*args, **kw): global extra_log_params # TODO: Test for correct call. filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) - if kw.has_key('stage'): + if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'],'gpgencrypt',filepath)) log.debug(_("Executing module gpgencrypt for %r, %r") % (args, kw), level=8) new_filepath = os.path.join('/var/spool/pykolab/wallace/gpgencrypt/incoming', os.path.basename(filepath)) if not filepath == new_filepath: log.debug("Renaming %r to %r" % (filepath, new_filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse message headers # @TODO: make sure we can use True as the 2nd argument here message = Parser().parse(open(filepath, 'r'), True) # Possible gpgencrypt answers are limited to ACCEPT only answers = [ 'ACCEPT' ] # from Mail::GnuPG.is_encrypted # #sub is_encrypted { # my ($self,$entity) = @_; # return 1 # if (($entity->effective_type =~ m!multipart/encrypted!) # || # ($entity->as_string =~ m!^-----BEGIN PGP MESSAGE-----!m)); # return 0; #} message_already_encrypted = False for part in message.walk(): if part.get_content_type() in [ "application/pgp-encrypted" ]: message_already_encrypted = True log.debug(_("Message is already encrypted (app/pgp-enc content-type)"), level=8) if message.get_content_type() in [ "multipart/encrypted" ]: message_already_encrypted = True log.debug(_("Message already encrypted by main content-type header"), level=8) if message_already_encrypted: return filepath try: # What are recipient addresses to encrypt to (bitmask)? # 1 - organization key # 2 - envelope to # 4 - to # 8 - cc # 16 - resent-to # 32 - resent-cc encrypt_to_rcpts = conf.get('wallace', 'gpgencrypt_to_rcpts') if encrypt_to_rcpts == None: encrypt_to_rcpts = 14 else: encrypt_to_rcpts = (int)(encrypt_to_rcpts) # Only encrypt to keys that are trusted strict_crypt = conf.get('wallace', 'gpgencrypt_strict') if strict_crypt == None: strict_crypt = False # Organization key ID if encrypt_to_rcpts & 1: encrypt_to_org = conf.get('wallace', 'gpgencrypt_to_org_key') if encrypt_to_org == None and encrypt_to_rcpts & 1: if strict_crypt: log.error(_("Configured to encrypt to a key not configured, and strict policy enabled. Bailing out.")) modules.cb_action_REJECT('gpgencrypt',filepath) else: log.error(_("Configured to encrypt to a key not configured, but continuing anyway (see 'gpgencrypt_strict').")) else: encrypt_to_org = [] # Bounce the message if encryption fails? force_crypt = conf.get('wallace', 'gpgencrypt_force') if force_crypt == None: force_crypt = False # Retrieve keys from remote server(s) automatically? retrieve_keys = conf.get('wallace', 'gpgencrypt_retrieve_keys') if retrieve_keys == None: retrieve_keys = False if retrieve_keys: gpgserver = conf.get('wallace', 'gpgencrypt_server') if gpgserver == None: gpgserver = 'pgp.mit.edu' encrypt_to = [] if encrypt_to_rcpts & 2: encrypt_to.extend(message.get_all('X-Kolab-To', [])) if encrypt_to_rcpts & 4: encrypt_to.extend(message.get_all('to', [])) if encrypt_to_rcpts & 8: encrypt_to.extend(message.get_all('cc', [])) if encrypt_to_rcpts & 16: encrypt_to.extend(message.get_all('resent-to', [])) if encrypt_to_rcpts & 32: encrypt_to.extend(message.get_all('resent-cc', [])) recipients = [address for displayname,address in getaddresses(encrypt_to)] log.debug(_("Recipients: %r") % (recipients)) # Split between recipients we can encrypt for/to, and ones we can not encrypt_rcpts = [] nocrypt_rcpts = [] gpg = gnupg.GPG(gnupghome='/var/lib/kolab/.gnupg', verbose=conf.debuglevel > 8) gpg.encoding = 'utf-8' local_keys = gpg.list_keys() log.debug(_("Current keys: %r") % (local_keys), level=8) for recipient in recipients: key_local = False log.debug(_("Retrieving key for recipient: %r") % (recipient)) for key in local_keys: for address in [x for x in [address for displayname,address in getaddresses(key['uids'])] if x == recipient]: log.debug(_("Found matching address %r") % (address)) key_local = key['keyid'] if key_local == False: if retrieve_keys: remote_keys = gpg.search_keys(recipient, gpgserver) if len(remote_keys) == 1: for address in [x for x in [address for displayname,address in getaddresses(remote_keys[0]['uids'])] if x == recipient]: log.debug(_("Found matching address %r in remote keys") % (address)) gpg.recv_keys(gpgserver, remote_keys[0]['keyid']) local_keys = gpg.list_keys() else: nocrypt_rcpts.append(recipient) for key in local_keys: for address in [x for x in [address for displayname,address in getaddresses(key['uids'])] if x == recipient]: log.debug(_("Found matching address %r") % (address)) key_local = key['keyid'] if not key_local == False: encrypt_rcpts.append(key_local) payload = message.get_payload() #print "payload:", payload if len(encrypt_rcpts) < 1: return filepath if "multipart" in message.get_content_type(): log.debug(_("Mime Message - we need to build multipart/encrypted structure"), level=8) msg = message enc_mime_message = pgp_mime(msg, encrypt_rcpts) message = enc_mime_message else: log.debug(_("No Mime Message - encypt plain"), level=8) encrypted_data = gpg.encrypt(payload, encrypt_rcpts, always_trust=True) encrypted_string = str(encrypted_data) message.set_payload(encrypted_string) message.add_header('X-wallace-gpg-encrypted', 'true') (fp, new_filepath) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/gpgencrypt/ACCEPT") os.write(fp, message.as_string()) os.close(fp) os.unlink(filepath) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','gpgencrypt', new_filepath)) except Exception as errmsg: log.error(_("An error occurred: %r") % (errmsg)) if conf.debuglevel > 8: import traceback traceback.print_exc() diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py index 72295a7..e1f0b52 100644 --- a/wallace/module_invitationpolicy.py +++ b/wallace/module_invitationpolicy.py @@ -1,1480 +1,1480 @@ # -*- coding: utf-8 -*- # Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) # # Thomas Bruederli (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import datetime import os import random import signal import tempfile import time from urlparse import urlparse import urllib import hashlib import traceback import re from email import message_from_string from email.parser import Parser from email.utils import formataddr from email.utils import getaddresses import modules import pykolab import kolabformat from pykolab import utils from pykolab.auth import Auth from pykolab.conf import Conf from pykolab.imap import IMAP from pykolab.xml import to_dt from pykolab.xml import utils as xmlutils from pykolab.xml import todo_from_message from pykolab.xml import event_from_message from pykolab.xml import participant_status_label from pykolab.itip import objects_from_message from pykolab.itip import check_event_conflict from pykolab.itip import send_reply from pykolab.translate import _ # define some contstants used in the code below ACT_MANUAL = 1 ACT_ACCEPT = 2 ACT_DELEGATE = 4 ACT_REJECT = 8 ACT_UPDATE = 16 ACT_CANCEL_DELETE = 32 ACT_SAVE_TO_FOLDER = 64 COND_IF_AVAILABLE = 128 COND_IF_CONFLICT = 256 COND_TENTATIVE = 512 COND_NOTIFY = 1024 COND_FORWARD = 2048 COND_TYPE_EVENT = 4096 COND_TYPE_TASK = 8192 COND_TYPE_ALL = COND_TYPE_EVENT + COND_TYPE_TASK ACT_TENTATIVE = ACT_ACCEPT + COND_TENTATIVE ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY ACT_SAVE_AND_FORWARD = ACT_SAVE_TO_FOLDER + COND_FORWARD ACT_CANCEL_DELETE_AND_NOTIFY = ACT_CANCEL_DELETE + COND_NOTIFY FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type' MESSAGE_PROCESSED = 1 MESSAGE_FORWARD = 2 policy_name_map = { # policy values applying to all object types 'ALL_MANUAL': ACT_MANUAL + COND_TYPE_ALL, 'ALL_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL, 'ALL_REJECT': ACT_REJECT + COND_TYPE_ALL, 'ALL_DELEGATE': ACT_DELEGATE + COND_TYPE_ALL, # not implemented 'ALL_UPDATE': ACT_UPDATE + COND_TYPE_ALL, 'ALL_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL, 'ALL_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_ALL, 'ALL_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_ALL, 'ALL_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_ALL, 'ALL_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL, # event related policy values 'EVENT_MANUAL': ACT_MANUAL + COND_TYPE_EVENT, 'EVENT_ACCEPT': ACT_ACCEPT + COND_TYPE_EVENT, 'EVENT_TENTATIVE': ACT_TENTATIVE + COND_TYPE_EVENT, 'EVENT_REJECT': ACT_REJECT + COND_TYPE_EVENT, 'EVENT_DELEGATE': ACT_DELEGATE + COND_TYPE_EVENT, # not implemented 'EVENT_UPDATE': ACT_UPDATE + COND_TYPE_EVENT, 'EVENT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_EVENT, 'EVENT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'EVENT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'EVENT_DELEGATE_IF_CONFLICT': ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT, 'EVENT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT, 'EVENT_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT, 'EVENT_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT, 'EVENT_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_EVENT, 'EVENT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_EVENT, # task related policy values 'TASK_MANUAL': ACT_MANUAL + COND_TYPE_TASK, 'TASK_ACCEPT': ACT_ACCEPT + COND_TYPE_TASK, 'TASK_REJECT': ACT_REJECT + COND_TYPE_TASK, 'TASK_DELEGATE': ACT_DELEGATE + COND_TYPE_TASK, # not implemented 'TASK_UPDATE': ACT_UPDATE + COND_TYPE_TASK, 'TASK_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK, 'TASK_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_TASK, 'TASK_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_TASK, 'TASK_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_TASK, 'TASK_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_TASK, # legacy values 'ACT_MANUAL': ACT_MANUAL + COND_TYPE_ALL, 'ACT_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL, 'ACT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'ACT_TENTATIVE': ACT_TENTATIVE + COND_TYPE_EVENT, 'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'ACT_DELEGATE': ACT_DELEGATE + COND_TYPE_ALL, 'ACT_DELEGATE_IF_CONFLICT': ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT, 'ACT_REJECT': ACT_REJECT + COND_TYPE_ALL, 'ACT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT, 'ACT_UPDATE': ACT_UPDATE + COND_TYPE_ALL, 'ACT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL, 'ACT_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_ALL, 'ACT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL, 'ACT_SAVE_TO_CALENDAR': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT, 'ACT_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT, } policy_value_map = dict([(v &~ COND_TYPE_ALL, k) for (k, v) in policy_name_map.iteritems()]) object_type_conditons = { 'event': COND_TYPE_EVENT, 'task': COND_TYPE_TASK } log = pykolab.getLogger('pykolab.wallace/invitationpolicy') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/invitationpolicy/' auth = None imap = None write_locks = [] def __init__(): modules.register('invitationpolicy', execute, description=description()) def accept(filepath): new_filepath = os.path.join( mybasepath, 'ACCEPT', os.path.basename(filepath) ) cleanup() os.rename(filepath, new_filepath) filepath = new_filepath exec('modules.cb_action_ACCEPT(%r, %r)' % ('invitationpolicy',filepath)) def reject(filepath): new_filepath = os.path.join( mybasepath, 'REJECT', os.path.basename(filepath) ) os.rename(filepath, new_filepath) filepath = new_filepath exec('modules.cb_action_REJECT(%r, %r)' % ('invitationpolicy',filepath)) def description(): return """Invitation policy execution module.""" def cleanup(): global auth, imap, write_locks, extra_log_params log.debug("cleanup(): %r, %r" % (auth, imap), level=8) extra_log_params['qid'] = '-' auth.disconnect() del auth # Disconnect IMAP or we lock the mailbox almost constantly imap.disconnect() del imap # remove remaining write locks for key in write_locks: remove_write_lock(key, False) def execute(*args, **kw): global auth, imap, extra_log_params filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) # (re)set language to default pykolab.translate.setUserLanguage(conf.get('kolab','default_locale')) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER', 'locks']: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) log.debug(_("Invitation policy called for %r, %r") % (args, kw), level=8) auth = Auth() imap = IMAP() # ignore calls on lock files - if '/locks/' in filepath or kw.has_key('stage') and kw['stage'] == 'locks': + if '/locks/' in filepath or 'stage' in kw and kw['stage'] == 'locks': return False log.debug("Invitation policy executing for %r, %r" % (filepath, '/locks/' in filepath), level=8) - if kw.has_key('stage'): + if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec( 'modules.cb_action_%s(%r, %r)' % ( kw['stage'], 'invitationpolicy', filepath ) ) return filepath else: # Move to incoming new_filepath = os.path.join( mybasepath, 'incoming', os.path.basename(filepath) ) if not filepath == new_filepath: log.debug("Renaming %r to %r" % (filepath, new_filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse full message message = Parser().parse(open(filepath, 'r')) # invalid message, skip if not message.get('X-Kolab-To'): return filepath recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))] sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0] any_itips = False recipient_email = None recipient_emails = [] recipient_user_dn = None # An iTip message may contain multiple events. Later on, test if the message # is an iTip message by checking the length of this list. try: itip_events = objects_from_message(message, ['VEVENT','VTODO'], ['REQUEST', 'REPLY', 'CANCEL']) except Exception as errmsg: log.error(_("Failed to parse iTip objects from message: %r" % (errmsg))) itip_events = [] if not len(itip_events) > 0: log.info(_("Message is not an iTip message or does not contain any (valid) iTip objects.")) else: any_itips = True log.debug(_("iTip objects attached to this message contain the following information: %r") % (itip_events), level=8) # See if any iTip actually allocates a user. - if any_itips and len([x['uid'] for x in itip_events if x.has_key('attendees') or x.has_key('organizer')]) > 0: + if any_itips and len([x['uid'] for x in itip_events if 'attendees' in x or 'organizer' in x]) > 0: auth.connect() # we're looking at the first itip object itip_event = itip_events[0] for recipient in recipients: recipient_user_dn = user_dn_from_email_address(recipient) if recipient_user_dn: receiving_user = auth.get_entry_attributes(None, recipient_user_dn, ['*']) recipient_emails = auth.extract_recipient_addresses(receiving_user) recipient_email = recipient # extend with addresses from delegators # (only do this lookup for REPLY messages) receiving_user['_delegated_mailboxes'] = [] if itip_event['method'] == 'REPLY': for _delegator in auth.list_delegators(recipient_user_dn): if not _delegator['_mailbox_basename'] == None: receiving_user['_delegated_mailboxes'].append( _delegator['_mailbox_basename'].split('@')[0] ) log.debug(_("Recipient emails for %s: %r") % (recipient_user_dn, recipient_emails), level=8) break if not any_itips: log.debug(_("No itips, no users, pass along %r") % (filepath), level=5) return filepath elif recipient_email is None: log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5) return filepath # for replies, the organizer is the recipient if itip_event['method'] == 'REPLY': # Outlook can send iTip replies without an organizer property - if itip_event.has_key('organizer'): + if 'organizer' in itip_event: organizer_mailto = str(itip_event['organizer']).split(':')[-1] user_attendees = [organizer_mailto] if organizer_mailto in recipient_emails else [] else: user_attendees = [recipient_email] else: # Limit the attendees to the one that is actually invited with the current message. - attendees = [str(a).split(':')[-1] for a in (itip_event['attendees'] if itip_event.has_key('attendees') else [])] + attendees = [str(a).split(':')[-1] for a in (itip_event['attendees'] if 'attendees' in itip_event else [])] user_attendees = [a for a in attendees if a in recipient_emails] - if itip_event.has_key('organizer'): + if 'organizer' in itip_event: sender_email = itip_event['xml'].get_organizer().email() # abort if no attendee matches the envelope recipient if len(user_attendees) == 0: log.info(_("No user attendee matching envelope recipient %s, skip message") % (recipient_email)) return filepath log.debug(_("Receiving user: %r") % (receiving_user), level=8) # set recipient_email to the matching attendee mailto: address recipient_email = user_attendees[0] # change gettext language to the preferredlanguage setting of the receiving user - if receiving_user.has_key('preferredlanguage'): + if 'preferredlanguage' in receiving_user: pykolab.translate.setUserLanguage(receiving_user['preferredlanguage']) # find user's kolabInvitationPolicy settings and the matching policy values type_condition = object_type_conditons.get(itip_event['type'], COND_TYPE_ALL) policies = get_matching_invitation_policies(receiving_user, sender_email, type_condition) # select a processing function according to the iTip request method method_processing_map = { 'REQUEST': process_itip_request, 'REPLY': process_itip_reply, 'CANCEL': process_itip_cancel } done = None - if method_processing_map.has_key(itip_event['method']): + if itip_event['method'] in method_processing_map: processor_func = method_processing_map[itip_event['method']] # connect as cyrus-admin imap.connect() for policy in policies: log.debug(_("Apply invitation policy %r for sender %r") % (policy_value_map[policy], sender_email), level=8) done = processor_func(itip_event, policy, recipient_email, sender_email, receiving_user) # matching policy found if done is not None: break # remove possible write lock from this iteration remove_write_lock(get_lock_key(receiving_user, itip_event['uid'])) else: log.debug(_("Ignoring '%s' iTip method") % (itip_event['method']), level=8) # message has been processed by the module, remove it if done == MESSAGE_PROCESSED: log.debug(_("iTip message %r consumed by the invitationpolicy module") % (message.get('Message-ID')), level=5) os.unlink(filepath) cleanup() return None # accept message into the destination inbox accept(filepath) def process_itip_request(itip_event, policy, recipient_email, sender_email, receiving_user): """ Process an iTip REQUEST message according to the given policy """ # if invitation policy is set to MANUAL, pass message along if policy & ACT_MANUAL: log.info(_("Pass invitation for manual processing")) return MESSAGE_FORWARD try: receiving_attendee = itip_event['xml'].get_attendee_by_email(recipient_email) log.debug(_("Receiving attendee: %r") % (receiving_attendee.to_dict()), level=8) except Exception as errmsg: log.error("Could not find envelope attendee: %r" % (errmsg)) return MESSAGE_FORWARD # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION is_task = itip_event['type'] == 'task' nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant partstat = receiving_attendee.get_participant_status() save_object = not nonpart or not partstat == kolabformat.PartNeedsAction rsvp = receiving_attendee.get_rsvp() scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction respond_with = receiving_attendee.get_participant_status(True) condition_fulfilled = True # find existing event in user's calendar (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True) # compare sequence number to determine a (re-)scheduling request if existing is not None: scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence() log.debug(_("Scheduling required: %r, for existing %s: %s") % (scheduling_required, existing.type, existing.get_uid()), level=8) save_object = True # if scheduling: check availability (skip that for tasks) if scheduling_required: if not is_task and policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): condition_fulfilled = check_availability(itip_event, receiving_user) if not is_task and policy & COND_IF_CONFLICT: condition_fulfilled = not condition_fulfilled log.debug(_("Precondition for object %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5) if existing: respond_with = None if policy & ACT_ACCEPT and condition_fulfilled: respond_with = 'TENTATIVE' if policy & COND_TENTATIVE else 'ACCEPTED' elif policy & ACT_REJECT and condition_fulfilled: respond_with = 'DECLINED' # TODO: only save declined invitation when a certain config option is set? elif policy & ACT_DELEGATE and condition_fulfilled: # TODO: delegate (but to whom?) return None # auto-update changes if enabled for this user elif policy & ACT_UPDATE and existing: # compare sequence number to avoid outdated updates if not itip_event['sequence'] == existing.get_sequence(): log.info(_("The iTip request sequence (%r) doesn't match the referred object version (%r). Ignoring.") % ( itip_event['sequence'], existing.get_sequence() )) return None log.debug(_("Auto-updating %s %r on iTip REQUEST (no re-scheduling)") % (existing.type, existing.uid), level=8) save_object = True rsvp = False # retain task status and percent-complete properties from my old copy if is_task: itip_event['xml'].set_status(existing.get_status()) itip_event['xml'].set_percentcomplete(existing.get_percentcomplete()) if policy & COND_NOTIFY: sender = itip_event['xml'].get_organizer() comment = itip_event['xml'].get_comment() send_update_notification(itip_event['xml'], receiving_user, existing, False, sender, comment) # if RSVP, send an iTip REPLY if rsvp or scheduling_required: # set attendee's CN from LDAP record if yet missing - if not receiving_attendee.get_name() and receiving_user.has_key('cn'): + if not receiving_attendee.get_name() and 'cn' in receiving_user: receiving_attendee.set_name(receiving_user['cn']) # send iTip reply if respond_with is not None and not respond_with == 'NEEDS-ACTION': receiving_attendee.set_participant_status(respond_with) send_reply(recipient_email, itip_event, invitation_response_text(itip_event['type']), subject=_('"%(summary)s" has been %(status)s')) elif policy & ACT_SAVE_TO_FOLDER: # copy the invitation into the user's default folder with PARTSTAT=NEEDS-ACTION itip_event['xml'].set_attendee_participant_status(receiving_attendee, respond_with or 'NEEDS-ACTION') save_object = True else: # policy doesn't match, pass on to next one return None if save_object: targetfolder = None # delete old version from IMAP if existing: targetfolder = existing._imap_folder delete_object(existing) elif master and hasattr(master, '_imap_folder'): targetfolder = master._imap_folder delete_object(master) if not nonpart or existing: # save new copy from iTip if store_object(itip_event['xml'], receiving_user, targetfolder, master): if policy & COND_FORWARD: log.debug(_("Forward invitation for notification"), level=5) return MESSAGE_FORWARD else: return MESSAGE_PROCESSED return None def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiving_user): """ Process an iTip REPLY message according to the given policy """ # if invitation policy is set to MANUAL, pass message along if policy & ACT_MANUAL: log.info(_("Pass reply for manual processing")) return MESSAGE_FORWARD # auto-update is enabled for this user if policy & ACT_UPDATE: try: sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email) log.debug(_("Sender Attendee: %r") % (sender_attendee), level=8) except Exception as errmsg: log.error("Could not find envelope sender attendee: %r" % (errmsg)) return MESSAGE_FORWARD # find existing event in user's calendar # sets/checks lock to avoid concurrent wallace processes trying to update the same event simultaneously (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True) if existing: # compare sequence number to avoid outdated replies? if not itip_event['sequence'] == existing.get_sequence(): log.info(_("The iTip reply sequence (%r) doesn't match the referred object version (%r). Forwarding to Inbox.") % ( itip_event['sequence'], existing.get_sequence() )) remove_write_lock(existing._lock_key) return MESSAGE_FORWARD log.debug(_("Auto-updating %s %r on iTip REPLY") % (existing.type, existing.uid), level=8) updated_attendees = [] try: existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), rsvp=False) existing_attendee = existing.get_attendee(sender_email) updated_attendees.append(existing_attendee) except Exception as errmsg: log.error("Could not find corresponding attende in organizer's copy: %r" % (errmsg)) # append delegated-from attendee ? if len(sender_attendee.get_delegated_from()) > 0: existing.add_attendee(sender_attendee) updated_attendees.append(sender_attendee) else: # TODO: accept new participant if ACT_ACCEPT ? remove_write_lock(existing._lock_key) return MESSAGE_FORWARD # append delegated-to attendee if len(sender_attendee.get_delegated_to()) > 0: try: delegatee_email = sender_attendee.get_delegated_to(True)[0] sender_delegatee = itip_event['xml'].get_attendee_by_email(delegatee_email) existing_delegatee = existing.find_attendee(delegatee_email) if not existing_delegatee: existing.add_attendee(sender_delegatee) log.debug(_("Add delegatee: %r") % (sender_delegatee.to_dict()), level=8) else: existing_delegatee.copy_from(sender_delegatee) log.debug(_("Update existing delegatee: %r") % (existing_delegatee.to_dict()), level=8) updated_attendees.append(sender_delegatee) # copy all parameters from replying attendee (e.g. delegated-to, role, etc.) existing_attendee.copy_from(sender_attendee) existing.update_attendees([existing_attendee]) log.debug(_("Update delegator: %r") % (existing_attendee.to_dict()), level=8) except Exception as errmsg: log.error("Could not find delegated-to attendee: %r" % (errmsg)) # update the organizer's copy of the object if update_object(existing, receiving_user, master): if policy & COND_NOTIFY: send_update_notification(existing, receiving_user, existing, True, sender_attendee, itip_event['xml'].get_comment()) # update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): propagate_changes_to_attendees_accounts(existing, updated_attendees) return MESSAGE_PROCESSED else: log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox.")) return MESSAGE_FORWARD return None def process_itip_cancel(itip_event, policy, recipient_email, sender_email, receiving_user): """ Process an iTip CANCEL message according to the given policy """ # if invitation policy is set to MANUAL, pass message along if policy & ACT_MANUAL: log.info(_("Pass cancellation for manual processing")) return MESSAGE_FORWARD # auto-update the local copy if policy & ACT_UPDATE or policy & ACT_CANCEL_DELETE: # find existing object in user's folders (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True) remove_object = policy & ACT_CANCEL_DELETE if existing: # on this-and-future cancel requests, set the recurrence until date on the master event if itip_event['recurrence-id'] and master and itip_event['xml'].get_thisandfuture(): rrule = master.get_recurrence() rrule.set_count(0) rrule.set_until(existing.get_start() + datetime.timedelta(days=-1)) master.set_recurrence(rrule) existing.set_recurrence_id(existing.get_recurrence_id(), True) remove_object = False # delete the local copy if remove_object: # remove exception and register an exdate to the main event if master: log.debug(_("Remove cancelled %s instance %s from %r") % (existing.type, itip_event['recurrence-id'], existing.uid), level=8) master.add_exception_date(existing.get_start()) master.del_exception(existing) success = update_object(master, receiving_user) # delete main event else: success = delete_object(existing) # update the local copy with STATUS=CANCELLED else: log.debug(_("Update cancelled %s %r with STATUS=CANCELLED") % (existing.type, existing.uid), level=8) existing.set_status('CANCELLED') existing.set_transparency(True) success = update_object(existing, receiving_user, master) if success: # send cancellation notification if policy & COND_NOTIFY: sender = itip_event['xml'].get_organizer() comment = itip_event['xml'].get_comment() send_cancel_notification(existing, receiving_user, remove_object, sender, comment) return MESSAGE_PROCESSED else: log.error(_("The object referred by this cancel request was not found in the user's folders. Forwarding to Inbox.")) return MESSAGE_FORWARD return None def user_dn_from_email_address(email_address): """ Resolves the given email address to a Kolab user entity """ global auth if not auth: auth = Auth() auth.connect() # return cached value - if user_dn_from_email_address.cache.has_key(email_address): + if email_address in user_dn_from_email_address.cache: return user_dn_from_email_address.cache[email_address] local_domains = auth.list_domains() if local_domains is not None: local_domains = list(set(local_domains.keys())) if not email_address.split('@')[1] in local_domains: user_dn_from_email_address.cache[email_address] = None return None log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8) user_dn = auth.find_user_dn(email_address, True) if isinstance(user_dn, basestring): log.debug(_("User DN: %r") % (user_dn), level=8) else: log.debug(_("No user record(s) found for %r") % (email_address), level=8) # remember this lookup user_dn_from_email_address.cache[email_address] = user_dn return user_dn user_dn_from_email_address.cache = {} def get_matching_invitation_policies(receiving_user, sender_email, type_condition=COND_TYPE_ALL): # get user's kolabInvitationPolicy settings - policies = receiving_user['kolabinvitationpolicy'] if receiving_user.has_key('kolabinvitationpolicy') else [] + policies = receiving_user['kolabinvitationpolicy'] if 'kolabinvitationpolicy' in receiving_user else [] if policies and not isinstance(policies, list): policies = [policies] if len(policies) == 0: policies = conf.get_list('wallace', 'kolab_invitation_policy') # match policies agains the given sender_email matches = [] for p in policies: if ':' in p: (value, domain) = p.split(':', 1) else: value = p domain = '' if domain == '' or domain == '*' or str(sender_email).endswith(domain): value = value.upper() - if policy_name_map.has_key(value): + if value in policy_name_map: val = policy_name_map[value] # append if type condition matches if val & type_condition: matches.append(val &~ COND_TYPE_ALL) # add manual as default action if len(matches) == 0: matches.append(ACT_MANUAL) return matches def imap_proxy_auth(user_rec): """ Perform IMAP login using proxy authentication with admin credentials """ global imap mail_attribute = conf.get('cyrus-sasl', 'result_attribute') if mail_attribute is None: mail_attribute = 'mail' mail_attribute = mail_attribute.lower() - if not user_rec.has_key(mail_attribute): + if mail_attribute not in user_rec: log.error(_("User record doesn't have the mailbox attribute %r set" % (mail_attribute))) return False # do IMAP prox auth with the given user backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') try: imap.disconnect() imap.connect(login=False) imap.login_plain(admin_login, admin_password, user_rec[mail_attribute]) except Exception as errmsg: log.error(_("IMAP proxy authentication failed: %r") % (errmsg)) return False return True def list_user_folders(user_rec, _type): """ Get a list of the given user's private calendar/tasks folders """ global imap # return cached list if '_imap_folders' in user_rec: return user_rec['_imap_folders'] result = [] if not imap_proxy_auth(user_rec): return result folders = imap.get_metadata('*') log.debug( _("List %r folders for user %r: %r") % ( _type, user_rec['mail'], folders ), level=8 ) (ns_personal, ns_other, ns_shared) = imap.namespaces() _folders = {} # Filter the folders by type relevance for folder, metadata in folders.items(): key = '/shared' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): _folders[folder] = metadata key = '/private' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): _folders[folder] = metadata for folder, metadata in _folders.items(): folder_delegated = False # Exclude shared and other user's namespace # # First, test if this is another users folder if ns_other is not None and folder.startswith(ns_other): # If we have no delegated mailboxes, we can skip this entirely if '_delegated_mailboxes' not in user_rec: continue for _m in user_rec['_delegated_mailboxes']: if folder.startswith(ns_other + _m + '/'): folder_delegated = True if not folder_delegated: continue # TODO: list shared folders the user has write privileges ? if ns_shared is not None: if len([_ns for _ns in ns_shared if folder.startswith(_ns)]) > 0: continue key = '/shared' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): result.append(folder) key = '/private' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): result.append(folder) # store default folder in user record if metadata[key].endswith('.default'): user_rec['_default_folder'] = folder continue # store private and confidential folders in user record if metadata[key].endswith('.confidential'): if '_confidential_folder' not in user_rec: user_rec['_confidential_folder'] = folder continue if metadata[key].endswith('.private'): if '_private_folder' not in user_rec: user_rec['_private_folder'] = folder continue # cache with user record user_rec['_imap_folders'] = result return result def find_existing_object(uid, type, recurrence_id, user_rec, lock=False): """ Search user's private folders for the given object (by UID+type) """ global imap lock_key = None if lock: lock_key = get_lock_key(user_rec, uid) set_write_lock(lock_key) event = None master = None for folder in list_user_folders(user_rec, type): log.debug(_("Searching folder %r for %s %r") % (folder, type, uid), level=8) imap.imap.m.select(imap.folder_utf7(folder)) res, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) for num in reversed(data[0].split()): res, data = imap.imap.m.fetch(num, '(UID RFC822)') try: msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1) except Exception: log.error(_("No UID found in IMAP response: %r") % (data[0][0])) continue try: if type == 'task': event = todo_from_message(message_from_string(data[0][1])) else: event = event_from_message(message_from_string(data[0][1])) # find instance in a recurring series if recurrence_id and (event.is_recurring() or event.has_exceptions() or event.get_recurrence_id()): master = event event = master.get_instance(recurrence_id) setattr(master, '_imap_folder', folder) setattr(master, '_msguid', msguid) # return master, even if instance is not found if not event and master.uid == uid: return (event, master) if event is not None: setattr(event, '_imap_folder', folder) setattr(event, '_lock_key', lock_key) setattr(event, '_msguid', msguid) except Exception: log.error(_("Failed to parse %s from message %s/%s: %s") % (type, folder, num, traceback.format_exc())) event = None master = None continue if event and event.uid == uid: return (event, master) if lock_key is not None: remove_write_lock(lock_key) return (event, master) def check_availability(itip_event, receiving_user): """ For the receiving user, determine if the event in question is in conflict. """ start = time.time() num_messages = 0 conflict = False # return previously detected conflict - if itip_event.has_key('_conflicts'): + if '_conflicts' in itip_event: return not itip_event['_conflicts'] for folder in list_user_folders(receiving_user, 'event'): log.debug(_("Listing events from folder %r") % (folder), level=8) imap.imap.m.select(imap.folder_utf7(folder)) res, data = imap.imap.m.search(None, '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")') num_messages += len(data[0].split()) for num in reversed(data[0].split()): event = None res, data = imap.imap.m.fetch(num, '(RFC822)') try: event = event_from_message(message_from_string(data[0][1])) except Exception as errmsg: log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, errmsg)) continue if event and event.uid: conflict = check_event_conflict(event, itip_event) if conflict: log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid'])) break if conflict: break end = time.time() log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=8) # remember the result of this check for further iterations itip_event['_conflicts'] = conflict return not conflict def set_write_lock(key, wait=True): """ Set a write-lock for the given key and wait if such a lock already exists """ if not os.path.isdir(mybasepath): os.makedirs(mybasepath) if not os.path.isdir(os.path.join(mybasepath, 'locks')): os.makedirs(os.path.join(mybasepath, 'locks')) filename = os.path.join(mybasepath, 'locks', key + '.lock') locktime = 0 if os.path.isfile(filename): locktime = os.path.getmtime(filename) # wait if file lock is in place while time.time() < locktime + 300: if not wait: return False log.debug(_("%r is locked, waiting...") % (key), level=8) time.sleep(0.5) locktime = os.path.getmtime(filename) if os.path.isfile(filename) else 0 # touch the file if os.path.isfile(filename): os.utime(filename, None) else: open(filename, 'w').close() # register active lock write_locks.append(key) return True def remove_write_lock(key, update=True): """ Remove the lock file for the given key """ global write_locks if key is not None: file = os.path.join(mybasepath, 'locks', key + '.lock') if os.path.isfile(file): os.remove(file) if update: write_locks = [k for k in write_locks if not k == key] def get_lock_key(user, uid): return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest() def update_object(object, user_rec, master=None): """ Update the given object in IMAP (i.e. delete + append) """ success = False saveobj = object # updating a single instance only: use master event if object.get_recurrence_id() and master: saveobj = master if hasattr(saveobj, '_imap_folder'): if delete_object(saveobj): saveobj.set_lastmodified() # update last-modified timestamp success = store_object(object, user_rec, saveobj._imap_folder, master) # remove write lock for this event if hasattr(saveobj, '_lock_key') and saveobj._lock_key is not None: remove_write_lock(saveobj._lock_key) return success def store_object(object, user_rec, targetfolder=None, master=None): """ Append the given object to the user's default calendar/tasklist """ # find calendar folder to save object to if not specified if targetfolder is None: targetfolders = list_user_folders(user_rec, object.type) oc = object.get_classification() # use *.confidential/private folder for confidential/private invitations - if oc == kolabformat.ClassConfidential and user_rec.has_key('_confidential_folder'): + if oc == kolabformat.ClassConfidential and '_confidential_folder' in user_rec: targetfolder = user_rec['_confidential_folder'] - elif oc == kolabformat.ClassPrivate and user_rec.has_key('_private_folder'): + elif oc == kolabformat.ClassPrivate and '_private_folder' in user_rec: targetfolder = user_rec['_private_folder'] # use *.default folder if exists - elif user_rec.has_key('_default_folder'): + elif '_default_folder' in user_rec: targetfolder = user_rec['_default_folder'] # fallback to any existing folder of specified type elif targetfolders is not None and len(targetfolders) > 0: targetfolder = targetfolders[0] if targetfolder is None: log.error(_("Failed to save %s: no target folder found for user %r") % (object.type, user_rec['mail'])) return False saveobj = object # updating a single instance only: add exception to master event if object.get_recurrence_id() and master: object.set_lastmodified() # update last-modified timestamp master.add_exception(object) saveobj = master log.debug(_("Save %s %r to user folder %r") % (saveobj.type, saveobj.uid, targetfolder), level=8) try: imap.imap.m.select(imap.folder_utf7(targetfolder)) result = imap.imap.m.append( imap.folder_utf7(targetfolder), None, None, saveobj.to_message(creator="Kolab Server ").as_string() ) return result except Exception as errmsg: log.error(_("Failed to save %s to user folder at %r: %r") % ( saveobj.type, targetfolder, errmsg )) return False def delete_object(existing): """ Removes the IMAP object with the given UID from a user's folder """ targetfolder = existing._imap_folder msguid = existing._msguid if hasattr(existing, '_msguid') else None try: imap.imap.m.select(imap.folder_utf7(targetfolder)) # delete by IMAP UID if msguid is not None: log.debug(_("Delete %s %r in %r by UID: %r") % ( existing.type, existing.uid, targetfolder, msguid ), level=8) imap.imap.m.uid('store', msguid, '+FLAGS', '(\\Deleted)') else: res, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid) log.debug(_("Delete %s %r in %r: %r") % ( existing.type, existing.uid, targetfolder, data ), level=8) for num in data[0].split(): imap.imap.m.store(num, '+FLAGS', '(\\Deleted)') imap.imap.m.expunge() return True except Exception as errmsg: log.error(_("Failed to delete %s from folder %r: %r") % ( existing.type, targetfolder, errmsg )) return False def send_update_notification(object, receiving_user, old=None, reply=True, sender=None, comment=None): """ Send a (consolidated) notification about the current participant status to organizer """ global auth from email.MIMEText import MIMEText from email.Utils import formatdate from email.header import Header from email import charset # encode unicode strings with quoted-printable charset.add_charset('utf-8', charset.SHORTEST, charset.QP) organizer = object.get_organizer() orgemail = organizer.email() orgname = organizer.name() itip_comment = None if comment is not None: comment = comment.strip() if sender is not None and not comment == '': itip_comment = _("%s commented: %s") % (_attendee_name(sender), comment) if reply: log.debug(_("Compose participation status summary for %s %r to user %r") % ( object.type, object.uid, receiving_user['mail'] ), level=8) auto_replies_expected = 0 auto_replies_received = 0 is_manual_reply = True partstats = {'ACCEPTED': [], 'TENTATIVE': [], 'DECLINED': [], 'DELEGATED': [], 'IN-PROCESS': [], 'COMPLETED': [], 'PENDING': []} for attendee in object.get_attendees(): parstat = attendee.get_participant_status(True) - if partstats.has_key(parstat): + if parstat in partstats: partstats[parstat].append(attendee.get_displayname()) else: partstats['PENDING'].append(attendee.get_displayname()) # look-up kolabinvitationpolicy for this attendee if attendee.get_cutype() == kolabformat.CutypeResource: resource_dns = auth.find_resource(attendee.get_email()) if isinstance(resource_dns, list): attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None else: attendee_dn = resource_dns else: attendee_dn = user_dn_from_email_address(attendee.get_email()) if attendee_dn: attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy']) if is_auto_reply(attendee_rec, orgemail, object.type): auto_replies_expected += 1 if not parstat == 'NEEDS-ACTION': auto_replies_received += 1 if sender is not None and sender.get_email() == attendee.get_email(): is_manual_reply = False # skip notification until we got replies from all automatically responding attendees if not is_manual_reply and auto_replies_received < auto_replies_expected: log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % ( auto_replies_received, auto_replies_expected ), level=8) return # build notification message body roundup = '' if itip_comment is not None: roundup += "\n" + itip_comment for status,attendees in partstats.iteritems(): if len(attendees) > 0: roundup += "\n" + participant_status_label(status) + ":\n\t" + "\n\t".join(attendees) + "\n" else: # build notification message body roundup = '' if itip_comment is not None: roundup += "\n" + itip_comment roundup += "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail) # list properties changed from previous version if old: diff = xmlutils.compute_diff(old.to_dict(), object.to_dict()) if len(diff) > 1: roundup += "\n" for change in diff: if not change['property'] in ['created','lastmodified-date','sequence']: new_value = xmlutils.property_to_string(change['property'], change['new']) if change['new'] else _("(removed)") if new_value: roundup += "\n- %s: %s" % (xmlutils.property_label(change['property']), new_value) # compose different notification texts for events/tasks if object.type == 'task': message_text = _(""" The assignment for '%(summary)s' has been updated in your tasklist. %(roundup)s """) % { 'summary': object.get_summary(), 'roundup': roundup } else: message_text = _(""" The event '%(summary)s' at %(start)s has been updated in your calendar. %(roundup)s """) % { 'summary': object.get_summary(), 'start': xmlutils.property_to_string('start', object.get_start()), 'roundup': roundup } if object.get_recurrence_id(): message_text += _("NOTE: This update only refers to this single occurrence!") + "\n" message_text += "\n" + _("*** This is an automated message. Please do not reply. ***") # compose mime message msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') msg['To'] = receiving_user['mail'] msg['Date'] = formatdate(localtime=True) msg['Subject'] = utils.str2unicode(_('"%s" has been updated') % (object.get_summary())) msg['From'] = Header(utils.str2unicode('%s' % orgname) if orgname else '') msg['From'].append("<%s>" % orgemail) seed = random.randint(0, 6) alarm_after = (seed * 10) + 60 log.debug(_("Set alarm to %s seconds") % (alarm_after), level=8) signal.alarm(alarm_after) result = modules._sendmail(orgemail, receiving_user['mail'], msg.as_string()) log.debug(_("Sent update notification to %r: %r") % (receiving_user['mail'], result), level=8) signal.alarm(0) def send_cancel_notification(object, receiving_user, deleted=False, sender=None, comment=None): """ Send a notification about event/task cancellation """ from email.MIMEText import MIMEText from email.Utils import formatdate from email.header import Header from email import charset # encode unicode strings with quoted-printable charset.add_charset('utf-8', charset.SHORTEST, charset.QP) log.debug(_("Send cancellation notification for %s %r to user %r") % ( object.type, object.uid, receiving_user['mail'] ), level=8) organizer = object.get_organizer() orgemail = organizer.email() orgname = organizer.name() # compose different notification texts for events/tasks if object.type == 'task': message_text = _("The assignment for '%(summary)s' has been cancelled by %(organizer)s.") % { 'summary': object.get_summary(), 'organizer': orgname if orgname else orgemail } if deleted: message_text += " " + _("The copy in your tasklist has been removed accordingly.") else: message_text += " " + _("The copy in your tasklist has been marked as cancelled accordingly.") else: message_text = _("The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.") % { 'summary': object.get_summary(), 'start': xmlutils.property_to_string('start', object.get_start()), 'organizer': orgname if orgname else orgemail } if deleted: message_text += " " + _("The copy in your calendar has been removed accordingly.") else: message_text += " " + _("The copy in your calendar has been marked as cancelled accordingly.") if comment is not None: comment = comment.strip() if sender is not None and not comment == '': message_text += "\n" + _("%s commented: %s") % (_attendee_name(sender), comment) if object.get_recurrence_id(): message_text += "\n" + _("NOTE: This cancellation only refers to this single occurrence!") message_text += "\n\n" + _("*** This is an automated message. Please do not reply. ***") # compose mime message msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') msg['To'] = receiving_user['mail'] msg['Date'] = formatdate(localtime=True) msg['Subject'] = utils.str2unicode(_('"%s" has been cancelled') % (object.get_summary())) msg['From'] = Header(utils.str2unicode('%s' % orgname) if orgname else '') msg['From'].append("<%s>" % orgemail) seed = random.randint(0, 6) alarm_after = (seed * 10) + 60 log.debug(_("Set alarm to %s seconds") % (alarm_after), level=8) signal.alarm(alarm_after) result = modules._sendmail(orgemail, receiving_user['mail'], msg.as_string()) log.debug(_("Sent cancel notification to %r: %r") % (receiving_user['mail'], result), level=8) signal.alarm(0) def is_auto_reply(user, sender_email, type): accept_available = False accept_conflicts = False for policy in get_matching_invitation_policies(user, sender_email, object_type_conditons.get(type, COND_TYPE_EVENT)): if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE): if check_policy_condition(policy, True): accept_available = True if check_policy_condition(policy, False): accept_conflicts = True # we have both cases covered by a policy if accept_available and accept_conflicts: return True # manual action reached if policy & (ACT_MANUAL | ACT_SAVE_TO_FOLDER): return False return False def check_policy_condition(policy, available): condition_fulfilled = True if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): condition_fulfilled = available if policy & COND_IF_CONFLICT: condition_fulfilled = not condition_fulfilled return condition_fulfilled def propagate_changes_to_attendees_accounts(object, updated_attendees=None): """ Find and update copies of this object in all attendee's personal folders """ recurrence_id = object.get_recurrence_id() for attendee in object.get_attendees(): attendee_user_dn = user_dn_from_email_address(attendee.get_email()) if attendee_user_dn: attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*']) (attendee_object, master_object) = find_existing_object(object.uid, object.type, recurrence_id, attendee_user, True) # does IMAP authenticate if attendee_object: # find attendee's entry by one of its email addresses attendee_emails = auth.extract_recipient_addresses(attendee_user) for attendee_email in attendee_emails: try: attendee_entry = attendee_object.get_attendee_by_email(attendee_email) except: attendee_entry = None if attendee_entry: break # copy all attendees from master object (covers additions and removals) new_attendees = [] for a in object.get_attendees(): # keep my own entry intact if attendee_entry is not None and attendee_entry.get_email() == a.get_email(): new_attendees.append(attendee_entry) else: new_attendees.append(a) attendee_object.set_attendees(new_attendees) if updated_attendees and not recurrence_id: log.debug("Update Attendees %r for %s" % ([a.get_email()+':'+a.get_participant_status(True) for a in updated_attendees], attendee_user['mail']), level=8) attendee_object.update_attendees(updated_attendees, False) success = update_object(attendee_object, attendee_user, master_object) log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], object.uid, success), level=8) else: log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], object.uid), level=8) else: log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8) def invitation_response_text(type): footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***") if type == 'task': return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer else: return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer def _attendee_name(attendee): # attendee here can be Attendee or ContactReference try: name = attendee.get_name() except Exception: name = attendee.name() if name == '': try: name = attendee.get_email() except Exception: name = attendee.email() return name diff --git a/wallace/module_optout.py b/wallace/module_optout.py index ed7fe0e..c710180 100644 --- a/wallace/module_optout.py +++ b/wallace/module_optout.py @@ -1,192 +1,192 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import json import os import random import tempfile import time from urlparse import urlparse import urllib from email import message_from_file from email.utils import formataddr from email.utils import getaddresses import modules import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/optout') conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/optout/' def __init__(): modules.register('optout', execute, description=description()) def description(): return """Consult the opt-out service.""" def execute(*args, **kw): if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) # TODO: Test for correct call. filepath = args[0] - if kw.has_key('stage'): + if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'],'optout',filepath)) return #modules.next_module('optout') log.debug(_("Consulting opt-out service for %r, %r") % (args, kw), level=8) message = message_from_file(open(filepath, 'r')) envelope_sender = getaddresses(message.get_all('From', [])) recipients = { "To": getaddresses(message.get_all('To', [])), "Cc": getaddresses(message.get_all('Cc', [])) # TODO: Are those all recipient addresses? } # optout answers are ACCEPT, REJECT, HOLD or DEFER answers = [ 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ] # Initialize our results placeholders. _recipients = {} for answer in answers: _recipients[answer] = { "To": [], "Cc": [] } for recipient_type in recipients: for recipient in recipients[recipient_type]: log.debug( _("Running opt-out consult from envelope sender '%s " + \ "<%s>' to recipient %s <%s>") % ( envelope_sender[0][0], envelope_sender[0][1], recipient[0], recipient[1] ), level=8 ) optout_answer = request( { 'unique-message-id': 'bogus', 'envelope_sender': envelope_sender[0][1], 'recipient': recipient[1] } ) _recipients[optout_answer][recipient_type].append(recipient) #print _recipients ## ## TODO ## ## If one of them all is DEFER, DEFER the entire message and discard the ## other results. ## for answer in answers: # Create the directory for the answer if not os.path.isdir(os.path.join(mybasepath, answer)): os.makedirs(os.path.join(mybasepath, answer)) # Consider using a new mktemp()-like call new_filepath = os.path.join(mybasepath, answer, os.path.basename(filepath)) # Write out a message file representing the new contents for the message # use formataddr(recipient) _message = message_from_file(open(filepath, 'r')) use_this = False for recipient_type in _recipients[answer]: _message.__delitem__(recipient_type) if not len(_recipients[answer][recipient_type]) == 0: _message.__setitem__( recipient_type, ',\n '.join( [formataddr(x) for x in _recipients[answer][recipient_type]] ) ) use_this = True if use_this: # TODO: Do not set items with an empty list. (fp, filename) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/optout/%s" % (answer)) os.write(fp, _message.__str__()) os.close(fp) # Callback with new filename if hasattr(modules, 'cb_action_%s' % (answer)): log.debug(_("Attempting to execute cb_action_%s(%r, %r)") % (answer, 'optout', filename), level=8) exec('modules.cb_action_%s(%r, %r)' % (answer,'optout', filename)) os.unlink(filepath) #print "Moving filepath %s to new_filepath %s" % (filepath, new_filepath) #os.rename(filepath, new_filepath) #if hasattr(modules, 'cb_action_%s' % (optout_answer)): #log.debug(_("Attempting to execute cb_action_%s()") % (optout_answer), level=8) #exec('modules.cb_action_%s(%r, %r)' % (optout_answer,'optout', new_filepath)) #return def request(params=None): params = json.dumps(params) optout_url = conf.get('wallace_optout', 'optout_url') try: f = urllib.urlopen(optout_url, params) except Exception: log.error(_("Could not send request to optout_url %s") % (optout_url)) return "DEFER" response = f.read() try: response_data = json.loads(response) except ValueError: # Some data is not JSON print("Response data is not JSON") return response_data['result'] diff --git a/wallace/modules.py b/wallace/modules.py index 1eb3ebe..aed60b6 100644 --- a/wallace/modules.py +++ b/wallace/modules.py @@ -1,456 +1,456 @@ # -*- coding: utf-8 -*- # Copyright 2010-2019 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import os import sys import time from email import message_from_string from email.message import Message from email.mime.base import MIMEBase from email.mime.message import MIMEMessage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.parser import Parser from email.utils import COMMASPACE from email.utils import formatdate from email.utils import formataddr from email.utils import getaddresses from email.utils import parsedate_tz import smtplib import pykolab from pykolab import constants from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/modules') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() modules = {} def initialize(): # We only want the base path modules_base_path = os.path.dirname(__file__) for modules_path, dirnames, filenames in os.walk(modules_base_path): if not modules_path == modules_base_path: continue for filename in filenames: if filename.startswith('module_') and filename.endswith('.py'): module_name = filename.replace('.py', '') name = module_name.replace('module_', '') # print("exec(\"from %s import __init__ as %s_register\")" % (module_name,name)) exec("from %s import __init__ as %s_register" % (module_name, name)) exec("%s_register()" % (name)) for dirname in dirnames: register_group(modules_path, dirname) def list_modules(*args, **kw): """ List modules """ __modules = {} for module in modules: if isinstance(module, tuple): module_group, module = module __modules[module_group] = { module: modules[(module_group, module)] } else: __modules[module] = modules[module] _modules = __modules.keys() _modules.sort() for _module in _modules: if 'function' in __modules[_module]: # This is a top-level module if __modules[_module]['description'] is not None: print("%-25s - %s" % (_module.replace('_', '-'), __modules[_module]['description'])) else: print("%-25s" % (_module.replace('_', '-'))) for _module in _modules: if 'function' not in __modules[_module]: # This is a nested module print("\n" + _("Module Group: %s") % (_module) + "\n") ___modules = __modules[_module].keys() ___modules.sort() for __module in ___modules: if __modules[_module][__module]['description'] is not None: print( "%-4s%-21s - %s" % ( '', _module.replace('_', '-'), __modules[_module][__module]['description'] ) ) else: print("%-4s%-21s" % ('', __module.replace('_', '-'))) def execute(name, *args, **kw): if name not in modules: log.error(_("No such module %r in modules %r (1).") % (name, modules)) sys.exit(1) if 'function' not in modules[name] and 'group' not in modules[name]: log.error(_("No such module %r in modules %r (2).") % (name, modules)) sys.exit(1) try: return modules[name]['function'](*args, **kw) except Exception as errmsg: log.exception(_("Module %r - Unknown error occurred; %r") % (name, errmsg)) def heartbeat(name, *args, **kw): - if not modules.has_key(name): + if name not in modules: log.warning(_("No such module %r in modules %r (1).") % (name, modules)) - if modules[name].has_key('heartbeat'): + if 'heartbeat' in modules[name]: return modules[name]['heartbeat'](*args, **kw) def _sendmail(sender, recipients, msg): # NOTE: Use "127.0.0.1" here for IPv6 (see also the service # definition in master.cf). sl = pykolab.logger.StderrToLogger(log) smtplib.stderr = sl smtp = smtplib.SMTP(timeout=15) if conf.debuglevel > 8: smtp.set_debuglevel(1) success = False attempt = 1 while not success and attempt <= 5: try: log.debug(_("Sending email via smtplib from %r, to %r (Attempt %r)") % (sender, recipients, attempt), level=8) smtp.connect("127.0.0.1", 10027) _response = smtp.sendmail(sender, recipients, msg) if len(_response) == 0: log.debug(_("SMTP sendmail OK"), level=8) else: log.debug(_("SMTP sendmail returned: %r") % (_response), level=8) smtp.quit() success = True break except smtplib.SMTPServerDisconnected as errmsg: log.error("SMTP Server Disconnected Error, %r" % (errmsg)) except smtplib.SMTPConnectError as errmsg: # DEFER log.error("SMTP Connect Error, %r" % (errmsg)) except smtplib.SMTPDataError as errmsg: # DEFER log.error("SMTP Data Error, %r" % (errmsg)) except smtplib.SMTPHeloError as errmsg: # DEFER log.error("SMTP HELO Error, %r" % (errmsg)) except smtplib.SMTPRecipientsRefused as errmsg: # REJECT, send NDR log.error("SMTP Recipient(s) Refused, %r" % (errmsg)) except smtplib.SMTPSenderRefused as errmsg: # REJECT, send NDR log.error("SMTP Sender Refused, %r" % (errmsg)) except Exception as errmsg: log.exception(_("smtplib - Unknown error occurred: %r") % (errmsg)) try: smtp.quit() except Exception as errmsg: log.error("smtplib quit() error - %r" % errmsg) time.sleep(10) attempt += 1 return success def cb_action_HOLD(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Holding message in queue for manual review (%s by %s)") % (filepath, module)) def cb_action_DEFER(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Deferring message in %s (by module %s)") % (filepath, module)) # parse message headers message = Parser().parse(open(filepath, 'r'), True) internal_time = parsedate_tz(message.__getitem__('Date')) internal_time = time.mktime(internal_time[:9]) + internal_time[9] now_time = time.time() delta = now_time - internal_time log.debug(_("The time when the message was sent: %r") % (internal_time), level=8) log.debug(_("The time now: %r") % (now_time), level=8) log.debug(_("The time delta: %r") % (delta), level=8) if delta > 432000: # TODO: Send NDR back to user log.debug(_("Message in file %s older then 5 days, deleting") % (filepath), level=8) os.unlink(filepath) # Alternative method is file age. #Date sent(/var/spool/pykolab/wallace/optout/DEFER/tmpIv7pDl): 'Thu, 08 Mar 2012 11:51:03 +0000' #(2012, 3, 8, 11, 51, 3, 0, 1, -1) # YYYY M D H m s weekday, yearday #log.debug(datetime.datetime(*), level=8) #import os #stat = os.stat(filepath) #fileage = datetime.datetime.fromtimestamp(stat.st_mtime) #now = datetime.datetime.now() #delta = now - fileage #print("file:", filepath, "fileage:", fileage, "now:", now, "delta(seconds):", delta.seconds) #if delta.seconds > 1800: ## TODO: Send NDR back to user #log.debug(_("Message in file %s older then 1800 seconds, deleting") % (filepath), level=8) #os.unlink(filepath) def cb_action_REJECT(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Rejecting message in %s (by module %s)") % (filepath, module)) log.debug(_("Rejecting message in: %r") %(filepath), level=8) # parse message headers message = Parser().parse(open(filepath, 'r'), True) envelope_sender = getaddresses(message.get_all('From', [])) recipients = getaddresses(message.get_all('To', [])) + \ getaddresses(message.get_all('Cc', [])) + \ getaddresses(message.get_all('X-Kolab-To', [])) _recipients = [] for recipient in recipients: if not recipient[0] == '': _recipients.append('%s <%s>' % (recipient[0], recipient[1])) else: _recipients.append('%s' % (recipient[1])) # TODO: Find the preferredLanguage for the envelope_sender user. ndr_message_subject = "Undelivered Mail Returned to Sender" ndr_message_text = _("""This is the email system Wallace at %s. I'm sorry to inform you we could not deliver the attached message to the following recipients: - %s Your message is being delivered to any other recipients you may have sent your message to. There is no need to resend the message to those recipients. """) % ( constants.fqdn, "\n- ".join(_recipients) ) diagnostics = _("""X-Wallace-Module: %s X-Wallace-Result: REJECT """) % ( module ) msg = MIMEMultipart("report") msg['From'] = "MAILER-DAEMON@%s" % (constants.fqdn) msg['To'] = formataddr(envelope_sender[0]) msg['Date'] = formatdate(localtime=True) msg['Subject'] = ndr_message_subject msg.preamble = "This is a MIME-encapsulated message." part = MIMEText(ndr_message_text) part.add_header("Content-Description", "Notification") msg.attach(part) _diag_message = Message() _diag_message.set_payload(diagnostics) part = MIMEMessage(_diag_message, "delivery-status") part.add_header("Content-Description", "Delivery Report") msg.attach(part) # @TODO: here I'm not sure message will contain the whole body # when we used headersonly argument of Parser().parse() above # delete X-Kolab-* headers del message['X-Kolab-From'] del message['X-Kolab-To'] part = MIMEMessage(message) part.add_header("Content-Description", "Undelivered Message") msg.attach(part) result = _sendmail( "MAILER-DAEMON@%s" % (constants.fqdn), [formataddr(envelope_sender[0])], msg.as_string() ) log.debug(_("Rejection message was sent successfully: %r") % result) if result: os.unlink(filepath) else: log.debug(_("Message %r was not removed from spool") % filepath) def cb_action_ACCEPT(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Accepting message in %s (by module %s)") % (filepath, module)) log.debug(_("Accepting message in: %r") %(filepath), level=8) # parse message headers message = Parser().parse(open(filepath, 'r'), True) messageid = message['message-id'] if 'message-id' in message else None sender = [formataddr(x) for x in getaddresses(message.get_all('X-Kolab-From', []))] recipients = [formataddr(x) for x in getaddresses(message.get_all('X-Kolab-To', []))] log.debug( _("Message-ID: %s, sender: %r, recipients: %r") % (messageid, sender, recipients), level=6 ) # delete X-Kolab-* headers del message['X-Kolab-From'] del message['X-Kolab-To'] log.debug(_("Removed X-Kolab- headers"), level=8) result = _sendmail( sender, recipients, # - Make sure we do not send this as binary. # - Second, strip NUL characters - I don't know where they # come from (TODO) # - Third, a character return is inserted somewhere. It # divides the body from the headers - and we don't like (TODO) # @TODO: check if we need Parser().parse() to load the whole message message.as_string() ) log.debug(_("Message was sent successfully: %r") % result) if result: os.unlink(filepath) else: log.debug(_("Message %r was not removed from spool") % filepath) def register_group(dirname, module): modules_base_path = os.path.join(os.path.dirname(__file__), module) modules[module] = {} for modules_path, dirnames, filenames in os.walk(modules_base_path): if not modules_path == modules_base_path: continue for filename in filenames: if filename.startswith('module_') and filename.endswith('.py'): module_name = filename.replace('.py','') name = module_name.replace('module_', '') # TODO: Error recovery from incomplete / incorrect modules. exec( "from %s.%s import __init__ as %s_%s_register" % ( module, module_name, module, name ) ) exec("%s_%s_register()" % (module,name)) def register(name, func, group=None, description=None, aliases=[], heartbeat=None): if not group == None: module = "%s_%s" % (group,name) else: module = name if isinstance(aliases, basestring): aliases = [aliases] - if modules.has_key(module): + if module in modules: log.fatal(_("Module '%s' already registered") % (module)) sys.exit(1) if callable(func): if group == None: modules[name] = { 'function': func, 'description': description } else: modules[group][name] = { 'function': func, 'description': description } modules[module] = modules[group][name] modules[module]['group'] = group modules[module]['name'] = name for alias in aliases: modules[alias] = { 'function': func, 'description': _("Alias for %s") % (name) } if callable(heartbeat): modules[module]['heartbeat'] = heartbeat