diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py index a2a64e0..622df71 100644 --- a/pykolab/auth/ldap/__init__.py +++ b/pykolab/auth/ldap/__init__.py @@ -1,3116 +1,3119 @@ # 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 datetime import _ldap import ldap import ldap.async import ldap.controls import ldap.filter import logging import time import traceback import pykolab import pykolab.base from pykolab import utils from pykolab.constants import * from pykolab.errors import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.auth') conf = pykolab.getConf() import auth_cache import cache # Catch python-ldap-2.4 changes from distutils import version if version.StrictVersion('2.4.0') <= version.StrictVersion(ldap.__version__): LDAP_CONTROL_PAGED_RESULTS = ldap.CONTROL_PAGEDRESULTS else: LDAP_CONTROL_PAGED_RESULTS = ldap.LDAP_CONTROL_PAGE_OID try: from ldap.controls import psearch except: log.warning(_("Python LDAP library does not support persistent search")) class SimplePagedResultsControl(ldap.controls.SimplePagedResultsControl): """ Python LDAP 2.4 and later breaks the API. This is an abstraction class so that we can handle either. """ def __init__(self, page_size=0, cookie=''): if version.StrictVersion( '2.4.0' ) <= version.StrictVersion( ldap.__version__ ): ldap.controls.SimplePagedResultsControl.__init__( self, size=page_size, cookie=cookie ) else: ldap.controls.SimplePagedResultsControl.__init__( self, LDAP_CONTROL_PAGED_RESULTS, True, (page_size, '') ) def cookie(self): if version.StrictVersion( '2.4.0' ) <= version.StrictVersion( ldap.__version__ ): return self.cookie else: return self.controlValue[1] def size(self): if version.StrictVersion( '2.4.0' ) <= version.StrictVersion( ldap.__version__ ): return self.size else: return self.controlValue[0] class LDAP(pykolab.base.Base): """ Abstraction layer for the LDAP authentication / authorization backend, for use with Kolab. """ def __init__(self, domain=None): """ Initialize the LDAP object for domain. If no domain is specified, domain name space configured as 'kolab'.'primary_domain' is used. """ pykolab.base.Base.__init__(self, domain=domain) self.ldap = None self.ldap_priv = None self.bind = None if domain == None: self.domain = conf.get('kolab', 'primary_domain') else: self.domain = domain def authenticate(self, login, realm): """ Find the entry corresponding to login, and attempt a bind. login is a tuple with 4 values. In order of appearance; [0] - the login username. [1] - the password [2] - the service (optional) [3] - the realm Called from pykolab.auth.Auth, the realm parameter is derived, while login[3] preserves the originally specified realm. """ try: log.debug( _("Attempting to authenticate user %s in realm %s") % ( login[0], realm ), level=8 ) except: pass self.connect(immediate=True) self._bind() # See if we know a base_dn for the domain base_dn = None try: base_dn = auth_cache.get_entry(self.domain) except Exception, errmsg: log.error(_("Authentication cache failed: %r") % (errmsg)) pass if base_dn == None: config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn try: auth_cache.set_entry(self.domain, base_dn) except Exception, errmsg: log.error(_("Authentication cache failed: %r") % (errmsg)) pass try: user_filter = self.config_get_raw('user_filter') % ( {'base_dn': base_dn} ) except TypeError, errmsg: user_filter = self.config_get_raw('user_filter') _filter = '(&(|' auth_attrs = self.config_get_list('auth_attributes') for attr in auth_attrs: _filter += "(%s=%s)" % (attr, login[0]) _filter += "(%s=%s@%s)" % (attr, login[0], realm) _filter += ')%s)' % (user_filter) entry_dn = None # Attempt to obtain an entry_dn from cache. try: entry_dn = auth_cache.get_entry(_filter) except Exception, errmsg: log.error(_("Authentication cache failed: %r") % (errmsg)) pass retval = False if entry_dn is None: _search = self.ldap.search_ext( base_dn, ldap.SCOPE_SUBTREE, _filter, ['entrydn'] ) try: ( _result_type, _result_data, _result_msgid, _result_controls ) = self.ldap.result3(_search) except ldap.SERVER_DOWN, errmsg: log.error(_("LDAP server unavailable: %r") % (errmsg)) log.error(_("%s") % (traceback.format_exc())) self._disconnect() return False except Exception, errmsg: log.error(_("Exception occurred: %r") % (errmsg)) log.error(_("%s") % (traceback.format_exc())) self._disconnect() return False log.debug( _("Length of entries found: %r") % ( len(_result_data) ), level=8 ) + # Remove referrals + _result_data = [_e for _e in _result_data if _e[0] is not None] + if len(_result_data) == 1: (entry_dn, entry_attrs) = _result_data[0] elif len(_result_data) > 1: try: log.info( _("Authentication for %r failed " + "(multiple entries)") % ( login[0] ) ) except: pass self._disconnect() return False else: try: log.info( _("Authentication for %r failed (no entry)") % ( login[0] ) ) except: pass self._disconnect() return False if entry_dn is None: try: log.info( _("Authentication for %r failed (LDAP error?)") % ( login[0] ) ) except: pass self._disconnect() return False try: # Needs to be synchronous or succeeds and continues setting # retval to True!! retval = self._bind(entry_dn, login[1]) if retval: try: log.info( _("Authentication for %r succeeded") % ( login[0] ) ) except: pass else: try: log.info( _("Authentication for %r failed (error)") % ( login[0] ) ) except: pass self._disconnect() return False try: auth_cache.set_entry(_filter, entry_dn) except Exception, errmsg: log.error(_("Authentication cache failed: %r") % (errmsg)) pass except ldap.SERVER_DOWN, errmsg: log.error(_("Authentication failed, LDAP server unavailable")) self._disconnect() return False except Exception, errmsg: try: log.debug( _("Failed to authenticate as user %r") % ( login[0] ), level=8 ) except: pass self._disconnect() return False else: try: # Needs to be synchronous or succeeds and continues setting # retval to True!! retval = self._bind(entry_dn, login[1]) if retval: log.info(_("Authentication for %r succeeded") % (login[0])) else: log.info( _("Authentication for %r failed (password)") % ( login[0] ) ) self._disconnect() return False auth_cache.set_entry(_filter, entry_dn) except ldap.NO_SUCH_OBJECT, errmsg: log.debug( _("Error occured, there is no such object: %r") % ( errmsg ), level=8 ) self.bind = None try: auth_cache.del_entry(_filter) except: log.error(_("Authentication cache failed to clear entry")) pass retval = self.authenticate(login, realm) except Exception, errmsg: log.debug(_("Exception occured: %r") %(errmsg)) try: log.debug( _("Failed to authenticate as user %r") % ( login[0] ), level=8 ) except: pass self._disconnect() return False self._disconnect() return retval def connect(self, priv=None, immediate=False): """ Connect to the LDAP server through the uri configured. """ # Already connected if priv is None and self.ldap is not None: return # Already connected if priv is not None and self.ldap_priv is not None: return log.debug(_("Connecting to LDAP..."), level=8) uri = self.config_get('ldap_uri') log.debug(_("Attempting to use LDAP URI %s") % (uri), level=8) trace_level = 0 if conf.debuglevel > 8: trace_level = 1 if immediate: retry_max = 1 retry_delay = 1.0 else: retry_max = 200 retry_delay = 3.0 conn = ldap.ldapobject.ReconnectLDAPObject( uri, trace_level=trace_level, retry_max=retry_max, retry_delay=retry_delay ) if immediate: conn.set_option(ldap.OPT_TIMEOUT, 10) conn.protocol_version = 3 conn.supported_controls = [] if priv is None: self.ldap = conn else: self.ldap_priv = conn def entry_dn(self, entry_id): """ Get a entry's distinguished name for an entry ID. The entry ID may be any of: - an entry's value for the configured unique_attribute, - a (syntactically valid) Distinguished Name, - a dictionary such as previously returned as (part of) the result of a search. """ entry_dn = None if self._entry_dn(entry_id): return entry_id if self._entry_dict(entry_id): return entry_id['dn'] unique_attribute = self.config_get('unique_attribute') config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn _search = self.ldap.search_ext( base_dn, ldap.SCOPE_SUBTREE, '(%s=%s)' % (unique_attribute, entry_id), ['entrydn'] ) ( _result_type, _result_data, _result_msgid, _result_controls ) = self.ldap.result3(_search) if len(_result_data) >= 1: (entry_dn, entry_attrs) = _result_data[0] return entry_dn def get_entry_attribute(self, entry_id, attribute): """ Get an attribute for an entry. Return the attribute value if successful, or None if not. """ entry_attrs = self.get_entry_attributes(entry_id, [attribute]) if entry_attrs.has_key(attribute): return entry_attrs[attribute] elif entry_attrs.has_key(attribute.lower()): return entry_attrs[attribute.lower()] else: return None def get_entry_attributes(self, entry_id, attributes): """ Get multiple attributes for an entry. """ self._bind() log.debug(_("Entry ID: %r") % (entry_id), level=9) entry_dn = self.entry_dn(entry_id) log.debug(_("Entry DN: %r") % (entry_dn), level=9) log.debug( _("ldap search: (%r, %r, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + %r") % ( entry_dn, ldap.SCOPE_BASE, attributes ), level=9 ) _search = self.ldap.search_ext( entry_dn, ldap.SCOPE_BASE, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + attributes ) ( _result_type, _result_data, _result_msgid, _result_controls ) = self.ldap.result3(_search) if len(_result_data) >= 1: (_entry_dn, _entry_attrs) = _result_data[0] else: return None return utils.normalize(_entry_attrs) def list_recipient_addresses(self, entry_id): """ Give a list of all valid recipient addresses for an LDAP entry identified by its ID. """ mail_attributes = conf.get_list('ldap', 'mail_attributes') entry = self.get_entry_attributes(entry_id, mail_attributes) return self.extract_recipient_addresses(entry) if entry is not None else [] def extract_recipient_addresses(self, entry): """ Extact a list of all valid recipient addresses for the given LDAP entry. This includes all attributes configured for ldap.mail_attributes """ recipient_addresses = [] mail_attributes = conf.get_list('ldap', 'mail_attributes') for attr in mail_attributes: if entry.has_key(attr): if isinstance(entry[attr], list): recipient_addresses.extend(entry[attr]) elif isinstance(entry[attr], basestring): recipient_addresses.append(entry[attr]) return recipient_addresses def list_delegators(self, entry_id): """ Get a list of user records the given user is set to be a delegatee """ delegators = [] mailbox_attribute = conf.get('cyrus-sasl', 'result_attribute') if mailbox_attribute == None: mailbox_attribute = 'mail' for __delegator in self.search_entry_by_attribute('kolabDelegate', entry_id): (_dn, _delegator) = __delegator _delegator['dn'] = _dn; _delegator['_mailbox_basename'] = _delegator[mailbox_attribute] if _delegator.has_key(mailbox_attribute) else None if isinstance(_delegator['_mailbox_basename'], list): _delegator['_mailbox_basename'] = _delegator['_mailbox_basename'][0] delegators.append(_delegator) return delegators def find_folder_resource(self, folder="*", exclude_entry_id=None): """ Given a shared folder name or list of folder names, find one or more valid resources. Specify an additional entry_id to exclude to exclude matches. """ self._bind() if not exclude_entry_id == None: __filter_prefix = "(&" __filter_suffix = "(!(%s=%s)))" % ( self.config_get('unique_attribute'), exclude_entry_id ) else: __filter_prefix = "" __filter_suffix = "" resource_filter = self.config_get('resource_filter') if not resource_filter == None: __filter_prefix = "(&%s" % resource_filter __filter_suffix = ")" recipient_address_attrs = self.config_get_list("mail_attributes") result_attributes = recipient_address_attrs result_attributes.append(self.config_get('unique_attribute')) result_attributes.append('kolabTargetFolder') _filter = "(|" if isinstance(folder, basestring): _filter += "(kolabTargetFolder=%s)" % (folder) else: for _folder in folder: _filter += "(kolabTargetFolder=%s)" % (_folder) _filter += ")" _filter = "%s%s%s" % (__filter_prefix,_filter,__filter_suffix) log.debug(_("Finding resource with filter %r") % (_filter), level=8) if len(_filter) <= 6: return None config_base_dn = self.config_get('resource_base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: resource_base_dn = ldap_base_dn else: resource_base_dn = config_base_dn _results = self.ldap.search_s( resource_base_dn, scope=ldap.SCOPE_SUBTREE, filterstr=_filter, attrlist=result_attributes, attrsonly=True ) _entry_dns = [] for _result in _results: (_entry_id, _entry_attrs) = _result _entry_dns.append(_entry_id) return _entry_dns def find_recipient(self, address="*", exclude_entry_id=None, search_attrs=None): """ Given an address string or list of addresses, find one or more valid recipients. Use this function only to detect whether an address is already in use by any entry in the tree. Specify an additional entry_id to exclude to exclude matches against the current entry. In search_attrs you can specify list of search attributes. By default mail_attributes are used. """ self._bind() if not exclude_entry_id == None: __filter_prefix = "(&" __filter_suffix = "(!(%s=%s)))" % ( self.config_get('unique_attribute'), exclude_entry_id ) else: __filter_prefix = "" __filter_suffix = "" kolab_filter = self._kolab_filter() if search_attrs is not None: recipient_address_attrs = search_attrs else: recipient_address_attrs = self.config_get_list("mail_attributes") result_attributes = recipient_address_attrs result_attributes.append(self.config_get('unique_attribute')) _filter = "(|" for recipient_address_attr in recipient_address_attrs: if isinstance(address, basestring): _filter += "(%s=%s)" % (recipient_address_attr, address) else: for _address in address: _filter += "(%s=%s)" % (recipient_address_attr, _address) _filter += ")" _filter = "%s%s%s" % (__filter_prefix,_filter,__filter_suffix) log.debug(_("Finding recipient with filter %r") % (_filter), level=8) if len(_filter) <= 6: return None config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn _results = self.ldap.search_s( base_dn, scope=ldap.SCOPE_SUBTREE, filterstr=_filter, attrlist=result_attributes, attrsonly=True ) _entry_dns = [] for _result in _results: (_entry_id, _entry_attrs) = _result # Prevent Active Directory referrals if not _entry_id == None: _entry_dns.append(_entry_id) return _entry_dns def find_resource(self, address="*", exclude_entry_id=None): """ Given an address string or list of addresses, find one or more valid resources. Specify an additional entry_id to exclude to exclude matches. """ self._bind() if not exclude_entry_id == None: __filter_prefix = "(&" __filter_suffix = "(!(%s=%s)))" % ( self.config_get('unique_attribute'), exclude_entry_id ) else: __filter_prefix = "" __filter_suffix = "" resource_filter = self.config_get('resource_filter') if not resource_filter == None: __filter_prefix = "(&%s" % resource_filter __filter_suffix = ")" recipient_address_attrs = self.config_get_list("mail_attributes") result_attributes = recipient_address_attrs result_attributes.append(self.config_get('unique_attribute')) _filter = "(|" for recipient_address_attr in recipient_address_attrs: if isinstance(address, basestring): _filter += "(%s=%s)" % (recipient_address_attr, address) else: for _address in address: _filter += "(%s=%s)" % (recipient_address_attr, _address) _filter += ")" _filter = "%s%s%s" % (__filter_prefix,_filter,__filter_suffix) log.debug(_("Finding resource with filter %r") % (_filter), level=8) if len(_filter) <= 6: return None config_base_dn = self.config_get('resource_base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: resource_base_dn = ldap_base_dn else: resource_base_dn = config_base_dn _results = self.ldap.search_s( resource_base_dn, scope=ldap.SCOPE_SUBTREE, filterstr=_filter, attrlist=result_attributes, attrsonly=True ) _entry_dns = [] for _result in _results: (_entry_id, _entry_attrs) = _result _entry_dns.append(_entry_id) return _entry_dns def get_latest_sync_timestamp(self): timestamp = cache.last_modify_timestamp(self.domain) log.debug(_("Using timestamp %r") % (timestamp), level=9) return timestamp def list_secondary_domains(self): """ List alias domain name spaces for the current domain name space. """ if not self.domains == None: return [s for s in self.domains.keys() if not s in self.domains.values()] else: return [] def recipient_policy(self, entry): """ Apply a recipient policy, if configured. Given an entry, returns the entry's attribute values to be set. """ entry_dn = self.entry_dn(entry) entry_modifications = {} entry_type = self._entry_type(entry) mail_attributes = self.config_get_list('mail_attributes') primary_mail = None primary_mail_attribute = None secondary_mail = None secondary_mail_attribute = None if len(mail_attributes) >= 1: primary_mail_attribute = mail_attributes[0] if len(mail_attributes) >= 2: secondary_mail_attribute = mail_attributes[1] daemon_rcpt_policy = self.config_get('daemon_rcpt_policy') if not utils.true_or_false(daemon_rcpt_policy) and not daemon_rcpt_policy == None: log.debug( _( "Not applying recipient policy for %s " + \ "(disabled through configuration)" ) % (entry_dn), level=1 ) return entry_modifications want_attrs = [] log.debug(_("Applying recipient policy to %r") % (entry_dn), level=8) # See which mail attributes we would want to control. # # 'mail' is considered for primary_mail, # 'alias' and 'mailalternateaddress' are considered for secondary mail. # primary_mail = self.config_get_raw('%s_primary_mail' % (entry_type)) if primary_mail == None and entry_type == 'user': primary_mail = self.config_get_raw('primary_mail') if not secondary_mail_attribute == None: secondary_mail = self.config_get_raw('%s_secondary_mail' % (entry_type)) if secondary_mail == None and entry_type == 'user': secondary_mail = self.config_get_raw('secondary_mail') log.debug( _("Using mail attributes: %r, with primary %r and " + \ "secondary %r") % ( mail_attributes, primary_mail_attribute, secondary_mail_attribute ), level=8 ) for _mail_attr in mail_attributes: if not entry.has_key(_mail_attr): log.debug(_("key %r not in entry") % (_mail_attr), level=8) if _mail_attr == primary_mail_attribute: log.debug(_("key %r is the prim. mail attr.") % (_mail_attr), level=8) if not primary_mail == None: log.debug(_("prim. mail pol. is not empty"), level=8) want_attrs.append(_mail_attr) elif _mail_attr == secondary_mail_attribute: log.debug(_("key %r is the sec. mail attr.") % (_mail_attr), level=8) if not secondary_mail == None: log.debug(_("sec. mail pol. is not empty"), level=8) want_attrs.append(_mail_attr) if len(want_attrs) > 0: log.debug(_("Attributes %r are not yet available for entry %r") % ( want_attrs, entry_dn ), level=8 ) # Also append the preferredlanguage or 'native tongue' configured # for the entry. if not entry.has_key('preferredlanguage'): want_attrs.append('preferredlanguage') # If we wanted anything, now is the time to get it. if len(want_attrs) > 0: log.debug(_("Attributes %r are not yet available for entry %r") % ( want_attrs, entry_dn ), level=8 ) attributes = self.get_entry_attributes(entry_dn, want_attrs) for attribute in attributes.keys(): entry[attribute] = attributes[attribute] if not entry.has_key('preferredlanguage'): entry['preferredlanguage'] = conf.get('kolab', 'default_locale') # Primary mail address if not primary_mail == None: primary_mail_address = conf.plugins.exec_hook( "set_primary_mail", kw={ 'primary_mail': primary_mail, 'entry': entry, 'primary_domain': self.domain } ) if primary_mail_address == None: return entry_modifications i = 1 _primary_mail = primary_mail_address done = False while not done: results = self.find_recipient(_primary_mail, entry['id']) # Length of results should be 0 (no entry found) # or 1 (which should be the entry we're looking at here) if len(results) == 0: log.debug( _("No results for mail address %s found") % ( _primary_mail ), level=8 ) done = True continue if len(results) == 1: log.debug( _("1 result for address %s found, verifying") % ( _primary_mail ), level=8 ) almost_done = True for result in results: if not result == entry_dn: log.debug( _("Too bad, primary email address %s " + \ "already in use for %s (we are %s)") % ( _primary_mail, result, entry_dn ), level=8 ) almost_done = False else: log.debug(_("Address assigned to us"), level=8) if almost_done: done = True continue i += 1 _primary_mail = "%s%d@%s" % ( primary_mail_address.split('@')[0], i, primary_mail_address.split('@')[1] ) primary_mail_address = _primary_mail ### ### FIXME ### if not primary_mail_address == None: if not entry.has_key(primary_mail_attribute): self.set_entry_attribute(entry, primary_mail_attribute, primary_mail_address) entry_modifications[primary_mail_attribute] = primary_mail_address else: if not primary_mail_address == entry[primary_mail_attribute]: self.set_entry_attribute(entry, primary_mail_attribute, primary_mail_address) entry_modifications[primary_mail_attribute] = primary_mail_address if not secondary_mail == None: # Execute the plugin hook suggested_secondary_mail = conf.plugins.exec_hook( "set_secondary_mail", kw={ 'secondary_mail': secondary_mail, 'entry': entry, 'domain': self.domain, 'primary_domain': self.domain, 'secondary_domains': self.list_secondary_domains() } ) # end of conf.plugins.exec_hook() call secondary_mail_addresses = [] for _secondary_mail in suggested_secondary_mail: i = 1 __secondary_mail = _secondary_mail done = False while not done: results = self.find_recipient(__secondary_mail, entry['id']) # Length of results should be 0 (no entry found) # or 1 (which should be the entry we're looking at here) if len(results) == 0: log.debug( _("No results for address %s found") % ( __secondary_mail ), level=8 ) done = True continue if len(results) == 1: log.debug( _("1 result for address %s found, " + \ "verifying...") % ( __secondary_mail ), level=8 ) almost_done = True for result in results: if not result == entry_dn: log.debug( _("Too bad, secondary email " + \ "address %s already in use for " + \ "%s (we are %s)") % ( __secondary_mail, result, entry_dn ), level=8 ) almost_done = False else: log.debug(_("Address assigned to us"), level=8) if almost_done: done = True continue i += 1 __secondary_mail = "%s%d@%s" % ( _secondary_mail.split('@')[0], i, _secondary_mail.split('@')[1] ) secondary_mail_addresses.append(__secondary_mail) log.debug(_("Recipient policy composed the following set of secondary " + \ "email addresses: %r") % (secondary_mail_addresses), level=8) if entry.has_key(secondary_mail_attribute): if isinstance(entry[secondary_mail_attribute], list): secondary_mail_addresses.extend(entry[secondary_mail_attribute]) else: secondary_mail_addresses.append(entry[secondary_mail_attribute]) if not secondary_mail_addresses == None: log.debug( _("Secondary mail addresses that we want is not None: %r") % ( secondary_mail_addresses ), level=9 ) secondary_mail_addresses = list(set(secondary_mail_addresses)) # Avoid duplicates while primary_mail_address in secondary_mail_addresses: log.debug( _("Avoiding the duplication of the primary mail " + \ "address %r in the list of secondary mail " + \ "addresses") % (primary_mail_address), level=9 ) secondary_mail_addresses.pop( secondary_mail_addresses.index(primary_mail_address) ) log.debug( _("Entry is getting secondary mail addresses: %r") % ( secondary_mail_addresses ), level=9 ) if not entry.has_key(secondary_mail_attribute): log.debug( _("Entry did not have any secondary mail " + \ "addresses in %r") % (secondary_mail_attribute), level=9 ) if not len(secondary_mail_addresses) == 0: self.set_entry_attribute( entry, secondary_mail_attribute, secondary_mail_addresses ) entry_modifications[secondary_mail_attribute] = secondary_mail_addresses else: if isinstance(entry[secondary_mail_attribute], basestring): entry[secondary_mail_attribute] = [entry[secondary_mail_attribute]] log.debug(_("secondary_mail_addresses: %r") % (secondary_mail_addresses), level=8) log.debug(_("entry[%s]: %r") % (secondary_mail_attribute,entry[secondary_mail_attribute]), level=8) secondary_mail_addresses.sort() entry[secondary_mail_attribute].sort() log.debug(_("secondary_mail_addresses: %r") % (secondary_mail_addresses), level=8) log.debug(_("entry[%s]: %r") % (secondary_mail_attribute,entry[secondary_mail_attribute]), level=8) if not list(set(secondary_mail_addresses)) == list(set(entry[secondary_mail_attribute])): self.set_entry_attribute( entry, secondary_mail_attribute, list(set(secondary_mail_addresses)) ) entry_modifications[secondary_mail_attribute] = list(set(secondary_mail_addresses)) log.debug(_("Entry modifications list: %r") % (entry_modifications), level=8) return entry_modifications def reconnect(self): bind = self.bind self._disconnect() self.connect() if bind is not None: self._bind(bind['dn'], bind['pw']) def search_entry_by_attribute(self, attr, value, **kw): self._bind() _filter = "(%s=%s)" % (attr, ldap.filter.escape_filter_chars(value)) config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn return self._search( base_dn, filterstr=_filter, attrlist=[ '*', ], override_search='_regular_search' ) def set_entry_attribute(self, entry_id, attribute, value): log.debug(_("Setting entry attribute %r to %r for %r") % (attribute, value, entry_id), level=9) self.set_entry_attributes(entry_id, { attribute: value }) def set_entry_attributes(self, entry_id, attributes): self._bind() entry_dn = self.entry_dn(entry_id) entry = self.get_entry_attributes(entry_dn, ['*']) attrs = {} for attribute in attributes.keys(): attrs[attribute.lower()] = attributes[attribute] modlist = [] for attribute in attrs.keys(): if not entry.has_key(attribute): entry[attribute] = self.get_entry_attribute(entry_id, attribute) for attribute in attrs.keys(): if entry.has_key(attribute) and entry[attribute] == None: modlist.append((ldap.MOD_ADD, attribute, attrs[attribute])) elif entry.has_key(attribute) and not entry[attribute] == None: if attrs[attribute] == None: modlist.append((ldap.MOD_DELETE, attribute, entry[attribute])) else: modlist.append((ldap.MOD_REPLACE, attribute, attrs[attribute])) dn = entry_dn if len(modlist) > 0 and self._bind_priv() is True: try: self.ldap_priv.modify_s(dn, modlist) except Exception, errmsg: log.error(_("Could not update dn:\nDN: %r\nModlist: %r\nError Message: %r") % (dn, modlist, errmsg)) import traceback log.error("%s" % (traceback.format_exc())) def synchronize(self, mode=0, callback=None): """ Synchronize with LDAP """ self._bind() _filter = self._kolab_filter() modified_after = None if hasattr(conf, 'resync'): if not conf.resync: modified_after = self.get_latest_sync_timestamp() else: modifytimestamp_format = conf.get_raw('ldap', 'modifytimestamp_format') if modifytimestamp_format == None: modifytimestamp_format = "%Y%m%d%H%M%SZ" modified_after = datetime.datetime(1900, 01, 01, 00, 00, 00).strftime(modifytimestamp_format) else: modified_after = self.get_latest_sync_timestamp() _filter = "(&%s(modifytimestamp>=%s))" % (_filter,modified_after) log.debug(_("Using filter %r") % (_filter), level=8) if not mode == 0: override_search = mode else: override_search = False config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn log.debug(_("Synchronization is searching against base DN: %s") % (base_dn), level=8) if callback == None: callback = self._synchronize_callback try: self._search( base_dn, filterstr=_filter, attrlist=[ '*', self.config_get('unique_attribute'), conf.get('cyrus-sasl', 'result_attribute'), 'modifytimestamp' ], override_search=override_search, callback=callback, ) except Exception, errmsg: log.error("An error occurred: %r" % (errmsg)) log.error(_("%s") % (traceback.format_exc())) def user_quota(self, entry_id, folder): default_quota = self.config_get('default_quota') quota_attribute = self.config_get('quota_attribute') if quota_attribute == None: return # The default quota may be None, but LDAP quota could still be set if default_quota == None: default_quota = 0 self._bind() entry_dn = self.entry_dn(entry_id) current_ldap_quota = self.get_entry_attribute(entry_dn, quota_attribute) _imap_quota = self.imap.get_quota(folder) if _imap_quota == None: used = None current_imap_quota = None else: (used, current_imap_quota) = _imap_quota log.debug( _("About to consider the user quota for %r (used: %r, " + \ "imap: %r, ldap: %r, default: %r") % ( entry_dn, used, current_imap_quota, current_ldap_quota, default_quota ), level=9 ) new_quota = conf.plugins.exec_hook("set_user_folder_quota", kw={ 'used': used, 'imap_quota': current_imap_quota, 'ldap_quota': current_ldap_quota, 'default_quota': default_quota } ) try: current_ldap_quota = (int)(current_ldap_quota) except: current_ldap_quota = None # If the new quota is zero, get out if new_quota == 0: return if not current_ldap_quota == None: if not new_quota == (int)(current_ldap_quota): self.set_entry_attribute( entry_dn, quota_attribute, "%s" % (new_quota) ) else: if not new_quota == None: self.set_entry_attribute( entry_dn, quota_attribute, "%s" % (new_quota) ) if not current_imap_quota == None: if not new_quota == current_imap_quota: self.imap.set_quota(folder, new_quota) else: if not new_quota == None: self.imap.set_quota(folder, new_quota) ### ### API depth level increasing! ### def _bind(self, bind_dn=None, bind_pw=None): # If we have no LDAP, we have no previous state. if self.ldap is None: self.bind = None self.connect() # If the bind_dn is None and the bind_pw is not... fail if bind_dn is None and bind_pw is not None: log.error(_("Attempting to bind without a DN but with a password")) return False # and the same vice-versa if bind_dn is not None and bind_pw is None: log.error(_("Attempting to bind with a DN but without a password")) return False # If we are to bind as foo, we have no state. if bind_dn is not None: self.bind = None # Only if we have no state and no bind credentials specified in the # function call. if self.bind is None: if bind_dn is None: bind_dn = self.config_get('service_bind_dn') if bind_pw is None: bind_pw = self.config_get('service_bind_pw') if bind_dn is not None: log.debug(_("Binding with bind_dn: %s and password: %s") % (bind_dn, '*' * len(bind_pw))) # TODO: Binding errors control try: # Must be synchronous self.ldap.simple_bind_s(bind_dn, bind_pw) self.bind = {'dn': bind_dn, 'pw': bind_pw} return True except ldap.SERVER_DOWN, errmsg: log.error(_("LDAP server unavailable: %r") % (errmsg)) log.error(_("%s") % (traceback.format_exc())) return False except ldap.INVALID_CREDENTIALS: log.error(_("Invalid DN, username and/or password.")) return False else: log.debug(_("bind() called but already bound"), level=8) return True def _bind_priv(self): if self.ldap_priv is None: self.connect(True) bind_dn = self.config_get('bind_dn') bind_pw = self.config_get('bind_pw') try: self.ldap_priv.simple_bind_s(bind_dn, bind_pw) return True except ldap.SERVER_DOWN, errmsg: log.error(_("LDAP server unavailable: %r") % (errmsg)) log.error(_("%s") % (traceback.format_exc())) return False except ldap.INVALID_CREDENTIALS: log.error(_("Invalid DN, username and/or password.")) return False else: log.debug(_("bind_priv() called but already bound"), level=8) return True def _change_add_group(self, entry, change): """ An entry of type group was added. The Kolab daemon has little to do for this type of action on this type of entry. """ pass def _change_add_None(self, entry, change): """ Redirect to _change_add_unknown """ self._change_add_unknown(entry, change) def _change_add_resource(self, entry, change): """ An entry of type resource was added. The Kolab daemon has little to do for this type of action on this type of entry. """ pass def _change_add_role(self, entry, change): """ An entry of type role was added. The Kolab daemon has little to do for this type of action on this type of entry. """ pass def _change_add_sharedfolder(self, entry, change): """ An entry of type sharedfolder was added. """ self.imap.connect(domain=self.domain) server = None # Get some configuration values mailserver_attribute = self.config_get('mailserver_attribute') if entry.has_key(mailserver_attribute): server = entry[mailserver_attribute] foldertype_attribute = self.config_get('sharedfolder_type_attribute') if not foldertype_attribute == None: if not entry.has_key(foldertype_attribute): entry[foldertype_attribute] = self.get_entry_attribute( entry['id'], foldertype_attribute ) if not entry[foldertype_attribute] == None: entry['kolabfoldertype'] = entry[foldertype_attribute] if not entry.has_key('kolabfoldertype'): entry['kolabfoldertype'] = self.get_entry_attribute( entry['id'], 'kolabfoldertype' ) # A delivery address is postuser+targetfolder delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute') if delivery_address_attribute == None: delivery_address_attribute = 'mail' if not entry.has_key(delivery_address_attribute): entry[delivery_address_attribute] = self.get_entry_attribute( entry['id'], delivery_address_attribute ) if not entry[delivery_address_attribute] == None: if len(entry[delivery_address_attribute].split('+')) > 1: entry['kolabtargetfolder'] = entry[delivery_address_attribute].split('+')[1] if not entry.has_key('kolabtargetfolder'): entry['kolabtargetfolder'] = self.get_entry_attribute( entry['id'], 'kolabtargetfolder' ) if entry.has_key('kolabtargetfolder') and \ not entry['kolabtargetfolder'] == None: folder_path = entry['kolabtargetfolder'] else: # TODO: What is *the* way to see if we need to create an @domain # shared mailbox? # TODO^2: self.domain, really? Presumes any mail attribute is # set to the primary domain name space... # TODO^3: Test if the cn is already something@domain result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute in ['mail']: folder_path = "%s@%s" % (entry['cn'], self.domain) else: folder_path = entry['cn'] if not folder_path.startswith('shared/'): folder_path = "shared/%s" % folder_path folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute') if folderacl_entry_attribute == None: folderacl_entry_attribute = 'acl' if not entry.has_key(folderacl_entry_attribute): entry[folderacl_entry_attribute] = self.get_entry_attribute( entry['id'], folderacl_entry_attribute ) if not self.imap.shared_folder_exists(folder_path): self.imap.shared_folder_create(folder_path, server) if entry.has_key('kolabfoldertype') and \ not entry['kolabfoldertype'] == None: self.imap.shared_folder_set_type( folder_path, entry['kolabfoldertype'] ) entry['kolabfolderaclentry'] = self._parse_acl(entry[folderacl_entry_attribute]) self.imap._set_kolab_mailfolder_acls( entry['kolabfolderaclentry'], folder_path ) if entry.has_key(delivery_address_attribute) and \ not entry[delivery_address_attribute] == None: self.imap.set_acl(folder_path, 'anyone', '+p') #if server == None: #self.entry_set_attribute(mailserver_attribute, server) def _change_add_unknown(self, entry, change): """ An entry has been add, and we do not know of what object type the entry was - user, group, role or sharedfolder. """ result_attribute = conf.get('cyrus-sasl', 'result_attribute') if not entry.has_key(result_attribute): return None if entry[result_attribute] == None: return None for _type in ['user','group','role','sharedfolder']: try: eval("self._change_add_%s(entry, change)" % (_type)) success = True except: success = False if success: break def _change_add_user(self, entry, change): """ An entry of type user was added. """ mailserver_attribute = self.config_get('mailserver_attribute') if mailserver_attribute == None: mailserver_attribute = 'mailhost' mailserver_attribute = mailserver_attribute.lower() result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute == None: result_attribute = 'mail' result_attribute = result_attribute.lower() if not entry.has_key(mailserver_attribute): entry[mailserver_attribute] = \ self.get_entry_attribute(entry, mailserver_attribute) rcpt_addrs = self.recipient_policy(entry) for key in rcpt_addrs: entry[key] = rcpt_addrs[key] if not entry.has_key(result_attribute): return if entry[result_attribute] == None: return if entry[result_attribute] == '': return cache.get_entry(self.domain, entry) self.imap.connect(domain=self.domain) if not self.imap.user_mailbox_exists(entry[result_attribute].lower()): folder = self.imap.user_mailbox_create( entry[result_attribute], entry[mailserver_attribute] ) else: folder = "user%s%s" % (self.imap.get_separator(),entry[result_attribute].lower()) server = self.imap.user_mailbox_server(folder) log.debug( _("Entry %s attribute value: %r") % ( mailserver_attribute, entry[mailserver_attribute] ), level=8 ) log.debug( _("imap.user_mailbox_server(%r) result: %r") % ( folder, server ), level=8 ) if not entry[mailserver_attribute] == server: self.set_entry_attribute(entry, mailserver_attribute, server) self.user_quota(entry, folder) def _change_delete_group(self, entry, change): """ An entry of type group was deleted. """ result_attribute = conf.get('cyrus-sasl', 'result_attribute') if not entry.has_key(result_attribute): return None if entry[result_attribute] == None: return None self.imap.cleanup_acls(entry[result_attribute]) def _change_delete_None(self, entry, change): """ Redirect to _change_delete_unknown """ self._change_delete_unknown(entry, change) def _change_delete_resource(self, entry, change): pass def _change_delete_role(self, entry, change): pass def _change_delete_sharedfolder(self, entry, change): pass def _change_delete_unknown(self, entry, change): """ An entry has been deleted, and we do not know of what object type the entry was - user, group, resource, role or sharedfolder. """ result_attribute = conf.get('cyrus-sasl', 'result_attribute') if not entry.has_key(result_attribute): return None if entry[result_attribute] == None: return None success = True for _type in ['user','group','resource','role','sharedfolder']: try: success = eval("self._change_delete_%s(entry, change)" % (_type)) except Exception, errmsg: log.error(_("An error occured: %r") % (errmsg)) log.error(_("%s") % (traceback.format_exc())) success = False if success: break def _change_delete_user(self, entry, change): """ An entry of type user was deleted. """ result_attribute = conf.get('cyrus-sasl', 'result_attribute') if not entry.has_key(result_attribute): return None if entry[result_attribute] == None: return None cache.delete_entry(self.domain, entry) self.imap.user_mailbox_delete(entry[result_attribute]) self.imap.cleanup_acls(entry[result_attribute]) # let plugins act upon this deletion conf.plugins.exec_hook( 'user_delete', kw = { 'user': entry, 'domain': self.domain } ) def _change_moddn_group(self, entry, change): # TODO: If the rdn attribute is the same as the result attribute... pass def _change_moddn_role(self, entry, change): pass def _change_moddn_user(self, entry, change): old_dn = change['previous_dn'] new_dn = change['dn'] import ldap.dn old_rdn = ldap.dn.explode_dn(old_dn)[0].split('=')[0] new_rdn = ldap.dn.explode_dn(new_dn)[0].split('=')[0] result_attribute = conf.get('cyrus-sasl', 'result_attribute') old_canon_attr = None cache_entry = cache.get_entry(self.domain, entry) if not cache_entry == None: old_canon_attr = cache_entry.result_attribute # See if we have to trigger the recipient policy. Only really applies to # situations in which the result_attribute is used in the old or in the # new DN. trigger_recipient_policy = False if old_rdn == result_attribute: if new_rdn == result_attribute: if new_rdn == old_rdn: trigger_recipient_policy = True else: if not new_rdn == old_rdn: trigger_recipient_policy = True else: if new_rdn == result_attribute: if not new_rdn == old_rdn: trigger_recipient_policy = True if trigger_recipient_policy: entry_changes = self.recipient_policy(entry) for key in entry_changes.keys(): entry[key] = entry_changes[key] if not entry.has_key(result_attribute): return if entry[result_attribute] == None: return if entry[result_attribute] == '': return # Now look at entry_changes and old_canon_attr, and see if they're # the same value. if entry_changes.has_key(result_attribute): if not old_canon_attr == None: self.imap.user_mailbox_create( entry_changes[result_attribute] ) elif not entry_changes[result_attribute] == old_canon_attr: self.imap.user_mailbox_rename( old_canon_attr, entry_changes[result_attribute] ) cache.get_entry(self.domain, entry) def _change_moddn_sharedfolder(self, entry, change): pass def _change_modify_None(self, entry, change): pass def _change_modify_group(self, entry, change): pass def _change_modify_role(self, entry, change): pass def _change_modify_sharedfolder(self, entry, change): """ A shared folder was modified. """ self.imap.connect(domain=self.domain) server = None # Get some configuration values mailserver_attribute = self.config_get('mailserver_attribute') if entry.has_key(mailserver_attribute): server = entry[mailserver_attribute] foldertype_attribute = self.config_get('sharedfolder_type_attribute') if not foldertype_attribute == None: if not entry.has_key(foldertype_attribute): entry[foldertype_attribute] = self.get_entry_attribute( entry['id'], foldertype_attribute ) if not entry[foldertype_attribute] == None: entry['kolabfoldertype'] = entry[foldertype_attribute] if not entry.has_key('kolabfoldertype'): entry['kolabfoldertype'] = self.get_entry_attribute( entry['id'], 'kolabfoldertype' ) # A delivery address is postuser+targetfolder delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute') if not delivery_address_attribute == None: if not entry.has_key(delivery_address_attribute): entry[delivery_address_attribute] = self.get_entry_attribute( entry['id'], delivery_address_attribute ) if not entry[delivery_address_attribute] == None: if len(entry[delivery_address_attribute].split('+')) > 1: entry['kolabtargetfolder'] = entry[delivery_address_attribute].split('+')[1] if not entry.has_key('kolabtargetfolder'): entry['kolabtargetfolder'] = self.get_entry_attribute( entry['id'], 'kolabtargetfolder' ) if entry.has_key('kolabtargetfolder') and \ not entry['kolabtargetfolder'] == None: folder_path = entry['kolabtargetfolder'] else: # TODO: What is *the* way to see if we need to create an @domain # shared mailbox? # TODO^2: self.domain, really? Presumes any mail attribute is # set to the primary domain name space... # TODO^3: Test if the cn is already something@domain result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute in ['mail']: folder_path = "%s@%s" % (entry['cn'], self.domain) else: folder_path = entry['cn'] if not folder_path.startswith('shared/'): folder_path = "shared/%s" % folder_path folderacl_entry_attribute = self.config_get('sharedfolder_acl_entry_attribute') if folderacl_entry_attribute == None: folderacl_entry_attribute = 'acl' if not entry.has_key(folderacl_entry_attribute): entry[folderacl_entry_attribute] = self.get_entry_attribute( entry['id'], folderacl_entry_attribute ) if not self.imap.shared_folder_exists(folder_path): self.imap.shared_folder_create(folder_path, server) if entry.has_key('kolabfoldertype') and \ not entry['kolabfoldertype'] == None: self.imap.shared_folder_set_type( folder_path, entry['kolabfoldertype'] ) entry['kolabfolderaclentry'] = self._parse_acl(entry[folderacl_entry_attribute]) self.imap._set_kolab_mailfolder_acls( entry['kolabfolderaclentry'], folder_path, True ) if entry.has_key(delivery_address_attribute) and \ not entry[delivery_address_attribute] == None: self.imap.set_acl(folder_path, 'anyone', '+p') def _change_modify_user(self, entry, change): """ Handle the changes for an object of type user. Expects the new entry. """ # Initialize old_canon_attr (#1701) old_canon_attr = None result_attribute = conf.get('cyrus-sasl','result_attribute') _entry = cache.get_entry(self.domain, entry, update=False) # We do not necessarily have a synchronisation cache entry (#1701) if not _entry == None: if _entry.__dict__.has_key('result_attribute') and not _entry.result_attribute == '': old_canon_attr = _entry.result_attribute entry_changes = self.recipient_policy(entry) log.debug( _("Result from recipient policy: %r") % (entry_changes), level=8 ) if entry_changes.has_key(result_attribute): if not entry_changes[result_attribute] == old_canon_attr: if old_canon_attr == None: self.imap.user_mailbox_create( entry_changes[result_attribute] ) else: self.imap.user_mailbox_rename( old_canon_attr, entry_changes[result_attribute] ) entry[result_attribute] = entry_changes[result_attribute] cache.get_entry(self.domain, entry) elif entry.has_key(result_attribute): if not entry[result_attribute] == old_canon_attr: if old_canon_attr == None: self.imap.user_mailbox_create( entry[result_attribute] ) else: self.imap.user_mailbox_rename( old_canon_attr, entry[result_attribute] ) cache.get_entry(self.domain, entry) else: imap_mailbox = "user%s%s" % ( self.imap.get_separator(), entry[result_attribute] ) if not self.imap.has_folder(imap_mailbox): self.imap_user_mailbox_create( entry[result_attribute] ) self.user_quota( entry, "user%s%s" % ( self.imap.get_separator(), entry[result_attribute] ) ) if conf.has_option(self.domain, 'sieve_mgmt'): sieve_mgmt_enabled = conf.get(self.domain, 'sieve_mgmt') if utils.true_or_false(sieve_mgmt_enabled): conf.plugins.exec_hook( 'sieve_mgmt_refresh', kw={ 'user': entry[result_attribute] } ) def _change_none_group(self, entry, change): """ A group entry as part of the initial search result set. The Kolab daemon has little to do for this type of action on this type of entry. """ pass def _change_none_None(self, entry, change): pass def _change_none_role(self, entry, change): """ A role entry as part of the initial search result set. The Kolab daemon has little to do for this type of action on this type of entry. """ pass def _change_none_sharedfolder(self, entry, change): """ A sharedfolder entry as part of the initial search result set. """ self.imap.connect(domain=self.domain) server = None mailserver_attribute = self.config_get('mailserver_attribute') if entry.has_key(mailserver_attribute): server = entry[mailserver_attribute] if not entry.has_key('kolabtargetfolder'): entry['kolabtargetfolder'] = self.get_entry_attribute( entry['id'], 'kolabtargetfolder' ) if not entry.has_key('kolabfoldertype'): entry['kolabfoldertype'] = self.get_entry_attribute( entry['id'], 'kolabfoldertype' ) folderacl_entry_attribute = conf.get('ldap', 'sharedfolder_acl_entry_attribute') if folderacl_entry_attribute == None: folderacl_entry_attribute = 'acl' if not entry.has_key(folderacl_entry_attribute): entry['kolabfolderaclentry'] = self.get_entry_attribute( entry['id'], folderacl_entry_attribute ) else: entry['kolabfolderaclentry'] = entry[folderacl_entry_attribute] del entry[folderacl_entry_attribute] if entry.has_key('kolabtargetfolder') and \ not entry['kolabtargetfolder'] == None: folder_path = entry['kolabtargetfolder'] else: # TODO: What is *the* way to see if we need to create an @domain # shared mailbox? # TODO^2: self.domain, really? Presumes any mail attribute is # set to the primary domain name space... # TODO^3: Test if the cn is already something@domain result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute in ['mail']: folder_path = "%s@%s" % (entry['cn'], self.domain) else: folder_path = entry['cn'] if not folder_path.startswith('shared/'): folder_path = "shared/%s" % folder_path if not self.imap.shared_folder_exists(folder_path): self.imap.shared_folder_create(folder_path, server) if entry.has_key('kolabfoldertype') and \ not entry['kolabfoldertype'] == None: self.imap.shared_folder_set_type( folder_path, entry['kolabfoldertype'] ) entry['kolabfolderaclentry'] = self._parse_acl(entry['kolabfolderaclentry']) self.imap._set_kolab_mailfolder_acls( entry['kolabfolderaclentry'], folder_path, True ) delivery_address_attribute = self.config_get('sharedfolder_delivery_address_attribute') if entry.has_key(delivery_address_attribute) and \ not entry[delivery_address_attribute] == None: self.imap.set_acl(folder_path, 'anyone', '+p') #if server == None: #self.entry_set_attribute(mailserver_attribute, server) def _change_none_user(self, entry, change): """ A user entry as part of the initial search result set. """ mailserver_attribute = self.config_get('mailserver_attribute') if mailserver_attribute == None: mailserver_attribute = 'mailhost' mailserver_attribute = mailserver_attribute.lower() result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute == None: result_attribute = 'mail' result_attribute = result_attribute.lower() old_canon_attr = None _entry = cache.get_entry(self.domain, entry, update=False) if not _entry == None and _entry.__dict__.has_key('result_attribute') and not _entry.result_attribute == '': old_canon_attr = _entry.result_attribute entry_changes = self.recipient_policy(entry) if entry.has_key(result_attribute) and entry_changes.has_key(result_attribute): if not entry[result_attribute] == entry_changes[result_attribute]: old_canon_attr = entry[result_attribute] log.debug( _("Result from recipient policy: %r") % (entry_changes), level=8 ) if entry_changes.has_key(result_attribute) and not old_canon_attr == None: if not entry_changes[result_attribute] == old_canon_attr: self.imap.user_mailbox_rename( old_canon_attr, entry_changes[result_attribute] ) for key in entry_changes.keys(): entry[key] = entry_changes[key] self.set_entry_attribute(entry, key, entry[key]) cache.get_entry(self.domain, entry) self.imap.connect(domain=self.domain) server = None if not entry.has_key(mailserver_attribute): entry[mailserver_attribute] = self.get_entry_attribute(entry, mailserver_attribute) if entry[mailserver_attribute] == "" or entry[mailserver_attribute] == None: server = None else: server = entry[mailserver_attribute].lower() if entry.has_key(result_attribute) and \ not entry.has_key(result_attribute) == None: if not self.imap.user_mailbox_exists(entry[result_attribute]): folder = self.imap.user_mailbox_create(entry[result_attribute], server=server) server = self.imap.user_mailbox_server(folder) else: folder = "user%s%s" % ( self.imap.get_separator(), entry[result_attribute] ) server = self.imap.user_mailbox_server(folder) self.user_quota(entry, folder) mailserver_attr = self.config_get('mailserver_attribute') if not entry.has_key(mailserver_attr): self.set_entry_attribute(entry, mailserver_attr, server) else: if not entry[mailserver_attr] == server: # TODO: Should actually transfer mailbox self.set_entry_attribute(entry, mailserver_attr, server) else: log.warning( _("Kolab user %s does not have a result attribute %r") % ( entry['id'], result_attribute ) ) def _disconnect(self): del self.ldap del self.ldap_priv self.ldap = None self.ldap_priv = None self.bind = None def _domain_naming_context(self, domain): self._bind() # The list of naming contexts in the LDAP server attrs = self.get_entry_attributes("", ['namingContexts']) naming_contexts = attrs['namingcontexts'] if isinstance(naming_contexts, basestring): naming_contexts = [ naming_contexts ] log.debug( _("Naming contexts found: %r") % (naming_contexts), level=8 ) self._kolab_domain_root_dn(domain) log.debug( _("Domains/Root DNs found: %r") % ( self.domain_rootdns ), level=8 ) # If we have a 1:1 match, continue as planned for naming_context in naming_contexts: if self.domain_rootdns[domain].endswith(naming_context): return naming_context def _primary_domain_for_naming_context(self, naming_context): self._bind() _domain = '.'.join(naming_context.split(',dc='))[3:] _naming_context = self._kolab_domain_root_dn(_domain) if naming_context == _naming_context: return _domain def _entry_dict(self, value): """ Tests if 'value' is a valid entry dictionary with a DN contained within key 'dn'. Returns True or False """ if isinstance(value, dict): if value.has_key('dn'): return True return False def _entry_dn(self, value): """ Tests if 'value' is a valid DN. Returns True or False """ # Only basestrings can be DNs if not isinstance(value, basestring): return False try: import ldap.dn ldap_dn = ldap.dn.explode_dn(value) except ldap.DECODING_ERROR: # This is not a DN. return False return True def _entry_type(self, entry_id): """ Return the type of object for an entry. """ self._bind() entry_dn = self.entry_dn(entry_id) config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if not ldap_base_dn == None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn for _type in ['user', 'group', 'sharedfolder']: __filter = self.config_get('kolab_%s_filter' % (_type)) if __filter == None: __filter = self.config_get('%s_filter' % (_type)) if not __filter == None: try: result = self._regular_search(entry_dn, filterstr=__filter) except: result = self._regular_search( base_dn, filterstr="(%s=%s)" %( self.config_get('unique_attribute'), entry_id['id']) ) if not result: continue else: return _type def _find_user_dn(self, login, kolabuser=False): """ Find the distinguished name (DN) for a (Kolab) user entry in LDAP. """ conf_prefix = 'kolab_' if kolabuser else '' domain_root_dn = self._kolab_domain_root_dn(self.domain) user_base_dn = self.config_get(conf_prefix + 'user_base_dn') if user_base_dn == None: user_base_dn = self.config_get('base_dn') auth_attrs = self.config_get_list('auth_attributes') auth_search_filter = [ '(|' ] for auth_attr in auth_attrs: auth_search_filter.append('(%s=%s)' % (auth_attr,login)) if not '@' in login: auth_search_filter.append( '(%s=%s@%s)' % ( auth_attr, login, self.domain ) ) auth_search_filter.append(')') auth_search_filter = ''.join(auth_search_filter) user_filter = self.config_get(conf_prefix + 'user_filter') search_filter = "(&%s%s)" % ( auth_search_filter, user_filter ) _results = self._search( user_base_dn, filterstr=search_filter, attrlist=[ 'dn' ], override_search='_regular_search' ) if len(_results) == 1: (_user_dn, _user_attrs) = _results[0] else: # Retry to find the user_dn with just uid=%s against the root_dn, # if the login is not fully qualified if len(login.split('@')) < 2: search_filter = "(uid=%s)" % (login) _results = self._search( domain, filterstr=search_filter, attrlist=[ 'dn' ] ) if len(_results) == 1: (_user_dn, _user_attrs) = _results[0] else: # Overall fail return False else: return False return _user_dn def _kolab_domain_root_dn(self, domain): log.debug(_("Searching root dn for domain %r") % (domain), level=8) if not hasattr(self, 'domain_rootdns'): self.domain_rootdns = {} if self.domain_rootdns.has_key(domain): log.debug(_("Returning from cache: %r") % (self.domain_rootdns[domain]), level=8) return self.domain_rootdns[domain] self._bind() log.debug(_("Finding domain root dn for domain %s") % (domain), level=8) domain_base_dn = conf.get('ldap', 'domain_base_dn', quiet=True) domain_filter = conf.get('ldap', 'domain_filter') if not domain_filter == None: if not domain == None: domain_filter = domain_filter.replace('*', domain) if not domain_base_dn == "": _results = self._search( domain_base_dn, ldap.SCOPE_SUBTREE, domain_filter, override_search='_regular_search' ) domains = [] for _domain in _results: (domain_dn, _domain_attrs) = _domain domain_rootdn_attribute = conf.get( 'ldap', 'domain_rootdn_attribute' ) _domain_attrs = utils.normalize(_domain_attrs) if _domain_attrs.has_key(domain_rootdn_attribute): log.debug(_("Setting domain root dn from LDAP for domain %r: %r") % (domain, _domain_attrs[domain_rootdn_attribute]), level=8) self.domain_rootdns[domain] = _domain_attrs[domain_rootdn_attribute] return _domain_attrs[domain_rootdn_attribute] else: domain_name_attribute = self.config_get('domain_name_attribute') if domain_name_attribute == None: domain_name_attribute = 'associateddomain' if isinstance(_domain_attrs[domain_name_attribute], list): domain = _domain_attrs[domain_name_attribute][0] else: domain = _domain_attrs[domain_name_attribute] else: if conf.has_option('ldap', 'base_dn'): return conf.get('ldap', 'base_dn') self.domain_rootdns[domain] = utils.standard_root_dn(domain) return self.domain_rootdns[domain] def _kolab_filter(self): """ Compose a filter using the relevant settings from configuration. """ _filter = "(|" for _type in ['user', 'group', 'resource', 'sharedfolder']: __filter = self.config_get('kolab_%s_filter' % (_type)) if __filter == None: __filter = self.config_get('%s_filter' % (_type)) if not __filter == None: _filter = "%s%s" % (_filter,__filter) _filter = "%s)" % (_filter) return _filter def _list_domains(self, domain=None): """ Find the domains related to this Kolab setup, and return a list of DNS domain names. Returns a list of tuples, each tuple containing the primary domain name and a list of secondary domain names. This function should only be called by the primary instance of Auth. """ log.debug(_("Listing domains..."), level=8) self.connect() self._bind() domain_base_dn = conf.get('ldap', 'domain_base_dn', quiet=True) if domain_base_dn == "": # No domains are to be found in LDAP, return an empty list. # Note that the Auth() base itself handles this case. return [] # If we haven't returned already, let's continue searching domain_filter = conf.get('ldap', 'domain_filter') if not domain == None: domain_filter = domain_filter.replace('*', domain) if domain_base_dn == None or domain_filter == None: return [] dna = self.config_get('domain_name_attribute') if dna == None: dna = 'associateddomain' try: _search = self._search( domain_base_dn, ldap.SCOPE_SUBTREE, domain_filter, # TODO: Where we use associateddomain is actually # configurable [ dna ], override_search='_regular_search' ) except: return [] domains = [] for domain_dn, domain_attrs in _search: primary_domain = None secondary_domains = [] domain_attrs = utils.normalize(domain_attrs) # TODO: Where we use associateddomain is actually configurable if type(domain_attrs[dna]) == list: primary_domain = domain_attrs[dna].pop(0).lower() secondary_domains = [x.lower() for x in domain_attrs[dna]] else: primary_domain = domain_attrs[dna].lower() domains.append((primary_domain,secondary_domains)) return domains def _synchronize_callback(self, *args, **kw): """ Determine the characteristics of the callback being placed, and what data is contained within *args and **kw exactly. The exact form and shape of the feedback very much depends on the supportedControl used to even get the data. """ log.debug( "auth.ldap.LDAP._synchronize_callback(args %r, kw %r)" % ( args, kw ), level=9 ) # Typical for Persistent Change Control EntryChangeNotification if kw.has_key('change_type'): change_type = None change_dict = { 'change_type': kw['change_type'], 'previous_dn': kw['previous_dn'], 'change_number': kw['change_number'], 'dn': kw['dn'] } entry = utils.normalize(kw['entry']) entry['dn'] = kw['dn'] unique_attr = self.config_get('unique_attribute') entry['id'] = entry[unique_attr] try: entry['type'] = self._entry_type(entry) except: entry['type'] = None log.debug(_("Entry type: %s") % (entry['type']), level=8) if change_dict['change_type'] == None: # This entry was in the start result set eval("self._change_none_%s(entry, change_dict)" % (entry['type'])) else: if isinstance(change_dict['change_type'], int): change = psearch.CHANGE_TYPES_STR[change_dict['change_type']] change = change.lower() else: change = change_dict['change_type'] # See if we can find the cache entry - this way we can get to # the value of a (former, on a deleted entry) result_attribute result_attribute = conf.get('cyrus-sasl', 'result_attribute') if not entry.has_key(result_attribute): cache_entry = cache.get_entry(self.domain, entry, update=False) if hasattr(cache_entry, 'result_attribute') and change == 'delete': entry[result_attribute] = cache_entry.result_attribute eval( "self._change_%s_%s(entry, change_dict)" % ( change, entry['type'] ) ) # Typical for Paged Results Control elif kw.has_key('entry') and isinstance(kw['entry'], list): for entry_dn,entry_attrs in kw['entry']: # This is a referral if entry_dn == None: continue entry = { 'dn': entry_dn } entry_attrs = utils.normalize(entry_attrs) for attr in entry_attrs.keys(): entry[attr.lower()] = entry_attrs[attr] unique_attr = self.config_get('unique_attribute') entry['id'] = entry[unique_attr] try: entry['type'] = self._entry_type(entry) except: entry['type'] = "unknown" log.debug(_("Entry type: %s") % (entry['type']), level=8) eval("self._change_none_%s(entry, None)" % (entry['type'])) # result_attribute = conf.get('cyrus-sasl', 'result_attribute') # # rcpt_addrs = self.recipient_policy(entry) # # log.debug(_("Recipient Addresses: %r") % (rcpt_addrs), level=9) # # for key in rcpt_addrs.keys(): # entry[key] = rcpt_addrs[key] # # cache.get_entry(self.domain, entry) # # self.imap.connect(domain=self.domain) # # if not self.imap.user_mailbox_exists(entry[result_attribute]): # folder = self.imap.user_mailbox_create( # entry[result_attribute] # ) # # server = self.imap.user_mailbox_server(folder) ### ### Backend search functions ### def _persistent_search(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1, callback=False, primary_domain=None, secondary_domains=[] ): _results = [] psearch_server_controls = [] psearch_server_controls.append(psearch.PersistentSearchControl( criticality=True, changeTypes=[ 'add', 'delete', 'modify', 'modDN' ], changesOnly=False, returnECs=True ) ) _search = self.ldap.search_ext( base_dn, scope=scope, filterstr=filterstr, attrlist=attrlist, attrsonly=attrsonly, timeout=timeout, serverctrls=psearch_server_controls ) ecnc = psearch.EntryChangeNotificationControl while True: res_type,res_data,res_msgid,_None,_None,_None = self.ldap.result4( _search, all=0, add_ctrls=1, add_intermediates=1, resp_ctrl_classes={ecnc.controlType:ecnc} ) change_type = None previous_dn = None change_number = None for dn,entry,srv_ctrls in res_data: log.debug(_("LDAP Search Result Data Entry:"), level=8) log.debug(" DN: %r" % (dn), level=8) log.debug(" Entry: %r" % (entry), level=8) ecn_ctrls = [ c for c in srv_ctrls if c.controlType == ecnc.controlType ] if ecn_ctrls: change_type = ecn_ctrls[0].changeType previous_dn = ecn_ctrls[0].previousDN change_number = ecn_ctrls[0].changeNumber change_type_desc = psearch.CHANGE_TYPES_STR[change_type] log.debug( _("Entry Change Notification attributes:"), level=8 ) log.debug( " " + _("Change Type: %r (%r)") % ( change_type, change_type_desc ), level=8 ) log.debug( " " + _("Previous DN: %r") % (previous_dn), level=8 ) if callback: callback( dn=dn, entry=entry, previous_dn=previous_dn, change_type=change_type, change_number=change_number, primary_domain=primary_domain, secondary_domains=secondary_domains ) def _paged_search(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1, callback=False, primary_domain=None, secondary_domains=[] ): page_size = 500 critical = True _results = [] server_page_control = SimplePagedResultsControl(page_size=page_size) _search = self.ldap.search_ext( base_dn, scope=scope, filterstr=filterstr, attrlist=attrlist, attrsonly=attrsonly, serverctrls=[server_page_control] ) pages = 0 while True: pages += 1 try: ( _result_type, _result_data, _result_msgid, _result_controls ) = self.ldap.result3(_search) except ldap.NO_SUCH_OBJECT, e: log.warning( _("Object %s searched no longer exists") % (base_dn) ) break if callback: callback(entry=_result_data) _results.extend(_result_data) if (pages % 2) == 0: log.debug(_("%d results...") % (len(_results))) pctrls = [ c for c in _result_controls if c.controlType == LDAP_CONTROL_PAGED_RESULTS ] if pctrls: if hasattr(pctrls[0], 'size'): size = pctrls[0].size cookie = pctrls[0].cookie else: size, cookie = pctrls[0].controlValue if cookie: server_page_control.cookie = cookie _search = self.ldap.search_ext( base_dn, scope=scope, filterstr=filterstr, attrlist=attrlist, attrsonly=attrsonly, serverctrls=[server_page_control] ) else: # TODO: Error out more verbose break else: # TODO: Error out more verbose print "Warning: Server ignores RFC 2696 control." break return _results def _vlv_search(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1, callback=False, primary_domain=None, secondary_domains=[] ): pass def _sync_repl(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1, callback=False, primary_domain=None, secondary_domains=[] ): import ldapurl import syncrepl ldap_url = ldapurl.LDAPUrl(self.config_get('ldap_uri')) ldap_sync_conn = syncrepl.DNSync( '/var/lib/kolab/syncrepl_%s.db' % (self.domain), ldap_url.initializeUrl(), trace_level=2, callback=self._synchronize_callback ) bind_dn = self.config_get('bind_dn') bind_pw = self.config_get('bind_pw') ldap_sync_conn.simple_bind_s(bind_dn, bind_pw) msgid = ldap_sync_conn.syncrepl_search( base_dn, scope, mode='refreshAndPersist', filterstr=filterstr, attrlist=attrlist, ) try: # Here's where returns need to be taken into account... while ldap_sync_conn.syncrepl_poll(all=1, msgid=msgid): pass except KeyboardInterrupt: pass def _regular_search(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1, callback=False, primary_domain=None, secondary_domains=[] ): log.debug(_("Searching with filter %r") % (filterstr), level=8) _search = self.ldap.search( base_dn, scope=scope, filterstr=filterstr, attrlist=attrlist, attrsonly=attrsonly ) _results = [] _result_type = None while not _result_type == ldap.RES_SEARCH_RESULT: (_result_type, _result) = self.ldap.result(_search, False, 0) if not _result == None: for result in _result: _results.append(result) return _results def _search(self, base_dn, scope=ldap.SCOPE_SUBTREE, filterstr="(objectClass=*)", attrlist=None, attrsonly=0, timeout=-1, override_search=False, callback=False, primary_domain=None, secondary_domains=[] ): """ Search LDAP. Use the priority ordered SUPPORTED_LDAP_CONTROLS and use the first one supported. """ supported_controls = conf.get_list('ldap', 'supported_controls') if not supported_controls == None and not len(supported_controls) < 1: for control_num in [(int)(x) for x in supported_controls]: self.ldap.supported_controls.append( SUPPORTED_LDAP_CONTROLS[control_num]['func'] ) if len(self.ldap.supported_controls) < 1: for control_num in SUPPORTED_LDAP_CONTROLS.keys(): log.debug( _("Checking for support for %s on %s") % ( SUPPORTED_LDAP_CONTROLS[control_num]['desc'], self.domain ), level=8 ) _search = self.ldap.search_s( '', scope=ldap.SCOPE_BASE, attrlist=['supportedControl'] ) for (_result,_supported_controls) in _search: supported_controls = _supported_controls.values()[0] for control_num in SUPPORTED_LDAP_CONTROLS.keys(): if SUPPORTED_LDAP_CONTROLS[control_num]['oid'] in \ supported_controls: log.debug(_("Found support for %s") % ( SUPPORTED_LDAP_CONTROLS[control_num]['desc'], ), level=8 ) self.ldap.supported_controls.append( SUPPORTED_LDAP_CONTROLS[control_num]['func'] ) _results = [] if not override_search == False: _use_ldap_controls = [ override_search ] else: _use_ldap_controls = self.ldap.supported_controls for supported_control in _use_ldap_controls: # Repeat the same supported control until # a failure (Exception) occurs that been # recognized as not an error related to the # supported control (such as ldap.SERVER_DOWN). failed_ok = False while not failed_ok: try: exec("""_results = self.%s( %r, scope=%r, filterstr=%r, attrlist=%r, attrsonly=%r, timeout=%r, callback=callback, primary_domain=%r, secondary_domains=%r )""" % ( supported_control, base_dn, scope, filterstr, attrlist, attrsonly, timeout, primary_domain, secondary_domains ) ) break except ldap.SERVER_DOWN, errmsg: log.error(_("LDAP server unavailable: %r") % (errmsg)) log.error(_("%s") % (traceback.format_exc())) log.error(_("-- reconnecting in 10 seconds.")) time.sleep(10) self.reconnect() except Exception, errmsg: failed_ok = True log.error(_("An error occured using %s: %r") % (supported_control, errmsg)) log.error(_("%s") % (traceback.format_exc())) continue return _results def _parse_acl(self, acl): """ Parse LDAP ACL specification for use in IMAP """ results = [] if acl is not None: if not isinstance(acl, list): acl = [ acl ] for acl_entry in acl: # entry already converted to IMAP format? if acl_entry[0] == "(": results.append(acl_entry) continue acl_access = acl_entry.split()[-1] acl_subject = acl_entry.split(', ') if len(acl_subject) > 1: acl_subject = ', '.join(acl_subject[:-1]) else: acl_subject = acl_entry.split()[0] results.append("(%r, %r)" % (acl_subject, acl_access)) return results diff --git a/wallace/module_resources.py b/wallace/module_resources.py index f51285a..ed2baf8 100644 --- a/wallace/module_resources.py +++ b/wallace/module_resources.py @@ -1,1453 +1,1456 @@ # -*- coding: utf-8 -*- # Copyright 2010-2015 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 datetime import icalendar import os import pytz import random import tempfile import time from urlparse import urlparse from dateutil.tz import tzlocal import base64 import uuid 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.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 event_from_message from pykolab.xml import participant_status_label from pykolab.itip import events_from_message from pykolab.itip import check_event_conflict from pykolab.translate import _ # define some contstants used in the code below COND_NOTIFY = 256 ACT_MANUAL = 1 ACT_ACCEPT = 2 ACT_REJECT = 8 ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY policy_name_map = { 'ACT_MANUAL': ACT_MANUAL, 'ACT_ACCEPT': ACT_ACCEPT, 'ACT_REJECT': ACT_REJECT, 'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY } log = pykolab.getLogger('pykolab.wallace') conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/resources/' auth = None imap = None def __init__(): modules.register('resources', execute, description=description(), heartbeat=heartbeat) 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)' % ('resources',filepath)) def description(): return """Resource management module.""" def cleanup(): global auth, imap log.debug("cleanup(): %r, %r" % (auth, imap), level=9) auth.disconnect() del auth # Disconnect IMAP or we lock the mailbox almost constantly imap.disconnect() del imap def execute(*args, **kw): global auth, imap # (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' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) log.debug(_("Resource Management called for %r, %r") % (args, kw), level=8) auth = Auth() imap = IMAP() # TODO: Test for correct call. filepath = args[0] if kw.has_key('stage'): 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'], 'resources', 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 any_resources = False possibly_any_resources = False reference_uid = 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 = events_from_message(message, ['REQUEST', 'REPLY', 'CANCEL']) except Exception, e: log.error(_("Failed to parse iTip events from message: %r" % (e))) itip_events = [] if not len(itip_events) > 0: log.info( _("Message is not an iTip message or does not contain any " + \ "(valid) iTip.") ) else: any_itips = True log.debug( _("iTip events attached to this message contain the " + \ "following information: %r") % (itip_events), level=9 ) if any_itips: # See if any iTip actually allocates a resource. if len([x['resources'] for x in itip_events if x.has_key('resources')]) > 0 \ or len([x['attendees'] for x in itip_events if x.has_key('attendees')]) > 0: possibly_any_resources = True if possibly_any_resources: auth.connect() for recipient in recipients: # extract reference UID from recipients like resource+UID@domain.org if re.match('.+\+[A-Za-z0-9=/-]+@', recipient): try: (prefix, host) = recipient.split('@') (local, uid) = prefix.split('+') reference_uid = base64.b64decode(uid, '-/') recipient = local + '@' + host except: continue if not len(resource_record_from_email_address(recipient)) == 0: resource_recipient = recipient any_resources = True if any_resources: if not any_itips: log.debug(_("Not an iTip message, but sent to resource nonetheless. Reject message"), level=5) reject(filepath) return False else: # Continue. Resources and iTips. We like. pass else: if not any_itips: log.debug(_("No itips, no resources, pass along %r") % (filepath), level=5) return filepath else: log.debug(_("iTips, but no resources, pass along %r") % (filepath), level=5) return filepath # A simple list of merely resource entry IDs that hold any relevance to the # iTip events resource_dns = resource_records_from_itip_events(itip_events, resource_recipient) # check if resource attendees match the envelope recipient if len(resource_dns) == 0: log.info(_("No resource attendees matching envelope recipient %s, Reject message") % (resource_recipient)) log.debug("%r" % (itip_events), level=8) reject(filepath) return False # Get the resource details, which includes details on the IMAP folder # This may append resource collection members to recource_dns resources = get_resource_records(resource_dns) log.debug(_("Resources: %r; %r") % (resource_dns, resources), level=8) imap.connect() done = False receiving_resource = resources[resource_dns[0]] for itip_event in itip_events: if itip_event['method'] == 'REPLY': done = True # find initial reservation referenced by the reply if reference_uid: (event, master) = find_existing_event(reference_uid, itip_event['recurrence-id'], receiving_resource) log.debug(_("iTip REPLY to %r, %r; matches %r") % (reference_uid, itip_event['recurrence-id'], type(event)), level=8) if event: try: sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email) owner_reply = sender_attendee.get_participant_status() log.debug(_("Sender Attendee: %r => %r") % (sender_attendee, owner_reply), level=9) except Exception, e: log.error(_("Could not find envelope sender attendee: %r") % (e)) continue # compare sequence number to avoid outdated replies if not itip_event['sequence'] == event.get_sequence(): log.info(_("The iTip reply sequence (%r) doesn't match the referred event version (%r). Ignoring.") % ( itip_event['sequence'], event.get_sequence() )) continue # forward owner response comment comment = itip_event['xml'].get_comment() if comment: event.set_comment(str(comment)) _itip_event = dict(xml=event, uid=event.get_uid(), _master=master) _itip_event['recurrence-id'] = event.get_recurrence_id() if owner_reply == kolabformat.PartAccepted: event.set_status(kolabformat.StatusConfirmed) accept_reservation_request(_itip_event, receiving_resource, confirmed=True) elif owner_reply == kolabformat.PartDeclined: decline_reservation_request(_itip_event, receiving_resource) else: log.info(_("Invalid response (%r) received from resource owner for event %r") % ( sender_attendee.get_participant_status(True), reference_uid )) else: log.info(_("Event referenced by this REPLY (%r) not found in resource calendar") % (reference_uid)) else: log.info(_("No event reference found in this REPLY. Ignoring.")) # exit for-loop break # else: try: receiving_attendee = itip_event['xml'].get_attendee_by_email(receiving_resource['mail']) log.debug(_("Receiving Resource: %r; %r") % (receiving_resource, receiving_attendee), level=9) except Exception, e: log.error(_("Could not find envelope attendee: %r") % (e)) continue # ignore updates and cancellations to resource collections who already delegated the event if len(receiving_attendee.get_delegated_to()) > 0 or receiving_attendee.get_role() == kolabformat.NonParticipant: done = True log.debug(_("Recipient %r is non-participant, ignoring message") % (receiving_resource['mail']), level=8) # process CANCEL messages if not done and itip_event['method'] == "CANCEL": for resource in resource_dns: if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()] \ and resources[resource].has_key('kolabtargetfolder'): (event, master) = find_existing_event(itip_event['uid'], itip_event['recurrence-id'], resources[resource]) # remove entire event if event and master is None: log.debug(_("Cancellation for entire event %r: deleting") % (itip_event['uid']), level=8) delete_resource_event(itip_event['uid'], resources[resource], event._msguid) # just cancel one single occurrence: add exception with status=cancelled elif master is not None: log.debug(_("Cancellation for a single occurrence %r of %r: updating...") % (itip_event['recurrence-id'], itip_event['uid']), level=8) event.set_status('CANCELLED') event.set_transparency(True) _itip_event = dict(xml=event, uid=event.get_uid(), _master=master) _itip_event['recurrence-id'] = event.get_recurrence_id() save_resource_event(_itip_event, resources[resource]) done = True if done: os.unlink(filepath) cleanup() return # do the magic for the receiving attendee (available_resource, itip_event) = check_availability(itip_events, resource_dns, resources, receiving_attendee) reject = False resource = None original_resource = None # accept reservation if available_resource is not None: if available_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]: # check if reservation was delegated if available_resource['mail'] != receiving_resource['mail'] and receiving_attendee.get_participant_status() == kolabformat.PartDelegated: original_resource = receiving_resource resource = available_resource else: # This must have been a resource collection originally. # We have inserted the reference to the original resource # record in 'memberof'. if available_resource.has_key('memberof'): original_resource = resources[available_resource['memberof']] if original_resource['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]: # # Delegate: # - delegator: the original resource collection # - delegatee: the target resource # itip_event['xml'].delegate(original_resource['mail'], available_resource['mail'], available_resource['cn']) # set delegator to NON-PARTICIPANT and RSVP=FALSE delegator = itip_event['xml'].get_attendee_by_email(original_resource['mail']) delegator.set_role(kolabformat.NonParticipant) delegator.set_rsvp(False) log.debug(_("Delegate invitation for resource collection %r to %r") % (original_resource['mail'], available_resource['mail']), level=8) resource = available_resource # Look for ACT_REJECT policy if resource is not None: invitationpolicy = get_resource_invitationpolicy(resource) log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=9) if invitationpolicy is not None: for policy in invitationpolicy: if policy & ACT_REJECT: reject = True break if resource is not None and not reject: log.debug(_("Accept invitation for individual resource %r / %r") % (resource['dn'], resource['mail']), level=8) accept_reservation_request(itip_event, resource, original_resource, False, invitationpolicy) else: resource = resources[resource_dns[0]] # this is the receiving resource record log.debug(_("Decline invitation for individual resource %r / %r") % (resource['dn'], resource['mail']), level=8) decline_reservation_request(itip_event, resource) cleanup() os.unlink(filepath) def heartbeat(lastrun): global imap # run archival job every hour only now = int(time.time()) if lastrun == 0 or now - heartbeat._lastrun < 3600: return log.debug(_("module_resources.heartbeat(%d)") % (heartbeat._lastrun), level=8) # get a list of resource records from LDAP auth = Auth() auth.connect() resource_dns = auth.find_resource('*') + # Remove referrals + resource_dns = [dn for dn in resource_dns if dn is not None] + # filter by resource_base_dn resource_base_dn = conf.get('ldap', 'resource_base_dn', None) if resource_base_dn is not None: resource_dns = [dn for dn in resource_dns if resource_base_dn in dn] if len(resource_dns) > 0: imap = IMAP() imap.connect() for resource_dn in resource_dns: resource_attrs = auth.get_entry_attributes(None, resource_dn, ['kolabtargetfolder']) if resource_attrs.has_key('kolabtargetfolder'): try: expunge_resource_calendar(resource_attrs['kolabtargetfolder']) except Exception, e: log.error(_("Expunge resource calendar for %s (%s) failed: %r") % ( resource_dn, resource_attrs['kolabtargetfolder'], e )) imap.disconnect() auth.disconnect() heartbeat._lastrun = now heartbeat._lastrun = 0 def expunge_resource_calendar(mailbox): """ Cleanup routine to remove events older than 100 days from the given resource calendar """ global imap days = int(conf.get('wallace', 'resource_calendar_expire_days')) now = datetime.datetime.now(tzlocal()) expire_date = now - datetime.timedelta(days=days) log.debug( _("Expunge events in resource folder %r older than %d days") % (mailbox, days), level=8 ) # might raise an exception, let that bubble targetfolder = imap.folder_quote(mailbox) imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda") imap.imap.m.select(targetfolder) typ, data = imap.imap.m.search(None, 'UNDELETED') for num in data[0].split(): log.debug( _("Fetching message ID %r from folder %r") % (num, mailbox), level=9 ) typ, data = imap.imap.m.fetch(num, '(RFC822)') event_message = message_from_string(data[0][1]) try: event = event_from_message(message_from_string(data[0][1])) except Exception, e: log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e)) continue if event: dt_end = to_dt(event.get_end()) # consider recurring events and get real end date if event.is_recurring(): dt_end = to_dt(event.get_last_occurrence()) if dt_end is None: # skip if recurring forever continue if dt_end and dt_end < expire_date: age = now - dt_end log.debug(_("Flag event %s from message %s/%s as deleted (age = %d days)") % (event.uid, mailbox, num, age.days), level=8) imap.imap.m.store(num, '+FLAGS', '\\Deleted') imap.imap.m.expunge() def check_availability(itip_events, resource_dns, resources, receiving_attendee=None): """ For each resource, determine if any of the events in question are in conflict. """ # Store the (first) conflicting event(s) alongside the resource information. start = time.time() num_messages = 0 available_resource = None for resource in resources.keys(): # skip this for resource collections if not resources[resource].has_key('kolabtargetfolder'): continue # sets the 'conflicting' flag and adds a list of conflicting events found try: num_messages += read_resource_calendar(resources[resource], itip_events) except Exception, e: log.error(_("Failed to read resource calendar for %r: %r") % (resource, e)) end = time.time() log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=9) # For each resource (collections are first!) # check conflicts and either accept or decline the reservation request for resource in resource_dns: log.debug(_("Polling for resource %r") % (resource), level=9) if not resources.has_key(resource): log.debug(_("Resource %r has been popped from the list") % (resource), level=9) continue if not resources[resource].has_key('conflicting_events'): log.debug(_("Resource is a collection"), level=9) # check if there are non-conflicting collection members conflicting_members = [x for x in resources[resource]['uniquemember'] if resources[x]['conflict']] # found at least one non-conflicting member, remove the conflicting ones and continue if len(conflicting_members) < len(resources[resource]['uniquemember']): for member in conflicting_members: resources[resource]['uniquemember'] = [x for x in resources[resource]['uniquemember'] if x != member] del resources[member] log.debug(_("Removed conflicting resources from %r: (%r) => %r") % ( resource, conflicting_members, resources[resource]['uniquemember'] ), level=9) else: # TODO: shuffle existing bookings of collection members in order # to make one available for the requested time pass continue if len(resources[resource]['conflicting_events']) > 0: log.debug(_("Conflicting events: %r for resource %r") % (resources[resource]['conflicting_events'], resource), level=9) done = False # This is the event being conflicted with! for itip_event in itip_events: # do not re-assign single occurrences to another resource if itip_event['recurrence-id'] is not None: continue # Now we have the event that was conflicting if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]: # this resource initially was delegated from a collection ? if receiving_attendee and receiving_attendee.get_email() == resources[resource]['mail'] \ and len(receiving_attendee.get_delegated_from()) > 0: for delegator in receiving_attendee.get_delegated_from(): collection_data = get_resource_collection(delegator.email()) if collection_data is not None: # check if another collection member is available (available_resource, dummy) = check_availability(itip_events, collection_data[0], collection_data[1]) break if available_resource is not None: log.debug(_("Delegate to another resource collection member: %r to %r") % \ (resources[resource]['mail'], available_resource['mail']), level=8) # set this new resource as delegate for the receiving_attendee itip_event['xml'].delegate(resources[resource]['mail'], available_resource['mail'], available_resource['cn']) # set delegator to NON-PARTICIPANT and RSVP=FALSE receiving_attendee.set_role(kolabformat.NonParticipant) receiving_attendee.set_rsvp(False) receiving_attendee.setDelegatedFrom([]) # remove existing_events as we now delegated back to the collection if len(resources[resource]['existing_events']) > 0: for existing in resources[resource]['existing_events']: delete_resource_event(existing.uid, resources[resource], existing._msguid) done = True if done: break else: # No conflicts, go accept for itip_event in itip_events: # directly invited resource if resources[resource]['mail'] in [a.get_email() for a in itip_event['xml'].get_attendees()]: available_resource = resources[resource] done = True else: # This must have been a resource collection originally. # We have inserted the reference to the original resource # record in 'memberof'. if resources[resource].has_key('memberof'): original_resource = resources[resources[resource]['memberof']] # Randomly select a target resource from the resource collection. available_resource = resources[original_resource['uniquemember'][random.randint(0,(len(original_resource['uniquemember'])-1))]] done = True if done: break # end for resource in resource_dns: return (available_resource, itip_event) def read_resource_calendar(resource_rec, itip_events): """ Read all booked events from the given resource's calendar and check for conflicts with the given list if itip events """ global imap resource_rec['conflict'] = False resource_rec['conflicting_events'] = [] resource_rec['existing_events'] = [] mailbox = resource_rec['kolabtargetfolder'] log.debug( _("Checking events in resource folder %r") % (mailbox), level=9 ) # set read ACLs for admin user imap.set_acl(mailbox, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrs") # might raise an exception, let that bubble imap.imap.m.select(imap.folder_quote(mailbox)) typ, data = imap.imap.m.search(None, 'UNDELETED') num_messages = len(data[0].split()) for num in data[0].split(): # For efficiency, makes the routine non-deterministic if resource_rec['conflict']: continue log.debug( _("Fetching message UID %r from folder %r") % (num, mailbox), level=9 ) typ, data = imap.imap.m.fetch(num, '(UID RFC822)') try: msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1) except Exception, e: log.error(_("No UID found in IMAP response: %r") % (data[0][0])) continue event_message = message_from_string(data[0][1]) try: event = event_from_message(message_from_string(data[0][1])) except Exception, e: log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e)) continue if event: for itip in itip_events: conflict = check_event_conflict(event, itip) if event.get_uid() == itip['uid']: setattr(event, '_msguid', msguid) if event.is_recurring() or itip['recurrence-id']: resource_rec['existing_master'] = event else: resource_rec['existing_events'].append(event) if conflict: log.info( _("Event %r conflicts with event %r") % ( itip['xml'].get_uid(), event.get_uid() ) ) resource_rec['conflicting_events'].append(event.get_uid()) resource_rec['conflict'] = True return num_messages def find_existing_event(uid, recurrence_id, resource_rec): """ Search the resources's calendar folder for the given event (by UID) """ global imap event = None master = None mailbox = resource_rec['kolabtargetfolder'] log.debug(_("Searching %r for event %r") % (mailbox, uid), level=9) try: imap.imap.m.select(imap.folder_quote(mailbox)) typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) except Exception, e: log.error(_("Failed to access resource calendar:: %r") % (e)) return event for num in reversed(data[0].split()): typ, data = imap.imap.m.fetch(num, '(UID RFC822)') try: msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1) except Exception, e: log.error(_("No UID found in IMAP response: %r") % (data[0][0])) continue try: 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()): master = event event = master.get_instance(recurrence_id) setattr(master, '_msguid', msguid) # return master, even if instance is not found if not event and master.uid == uid: return (event, master) # compare recurrence-id and skip to next message if not matching elif recurrence_id and not xmlutils.dates_equal(recurrence_id, event.get_recurrence_id()): log.debug(_("Recurrence-ID not matching on message %s, skipping: %r != %r") % ( msguid, recurrence_id, event.get_recurrence_id() ), level=8) continue if event is not None: setattr(event, '_msguid', msguid) except Exception, e: log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, e)) event = None master = None continue if event and event.uid == uid: return (event, master) return (event, master) def accept_reservation_request(itip_event, resource, delegator=None, confirmed=False, invitationpolicy=None): """ Accepts the given iTip event by booking it into the resource's calendar. Then set the attendee status of the given resource to ACCEPTED and sends an iTip reply message to the organizer. """ owner = get_resource_owner(resource) confirmation_required = False if not confirmed and owner: if invitationpolicy is None: invitationpolicy = get_resource_invitationpolicy(resource) log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=9) if invitationpolicy is not None: for policy in invitationpolicy: if policy & ACT_MANUAL and owner['mail']: confirmation_required = True break partstat = 'TENTATIVE' if confirmation_required else 'ACCEPTED' itip_event['xml'].set_transparency(False); itip_event['xml'].set_attendee_participant_status( itip_event['xml'].get_attendee_by_email(resource['mail']), partstat ) saved = save_resource_event(itip_event, resource) log.debug( _("Adding event to %r: %r") % (resource['kolabtargetfolder'], saved), level=8 ) if saved: send_response(delegator['mail'] if delegator else resource['mail'], itip_event, owner) if owner and confirmation_required: send_owner_confirmation(resource, owner, itip_event) elif owner: send_owner_notification(resource, owner, itip_event, saved) def decline_reservation_request(itip_event, resource): """ Set the attendee status of the given resource to DECLINED and send an according iTip reply to the organizer. """ itip_event['xml'].set_attendee_participant_status( itip_event['xml'].get_attendee_by_email(resource['mail']), "DECLINED" ) # update master event if resource.get('existing_master') is not None or itip_event.get('_master') is not None: save_resource_event(itip_event, resource) # remove old copy of the reservation elif resource.get('existing_events', []) and len(resource['existing_events']) > 0: for existing in resource['existing_events']: delete_resource_event(existing.uid, resource, existing._msguid) # delete old event referenced by itip_event (from owner confirmation) elif hasattr(itip_event['xml'], '_msguid'): delete_resource_event(itip_event['xml'].uid, resource, itip_event['xml']._msguid) # send response and notification owner = get_resource_owner(resource) send_response(resource['mail'], itip_event, owner) if owner: send_owner_notification(resource, owner, itip_event, True) def save_resource_event(itip_event, resource): """ Append the given event object to the resource's calendar """ try: save_event = itip_event['xml'] targetfolder = imap.folder_quote(resource['kolabtargetfolder']) # add exception to existing recurring main event if resource.get('existing_master') is not None: save_event = resource['existing_master'] save_event.add_exception(itip_event['xml']) elif itip_event.get('_master') is not None: save_event = itip_event['_master'] save_event.add_exception(itip_event['xml']) # remove old copy of the reservation (also sets ACLs) if resource.has_key('existing_events') and len(resource['existing_events']) > 0: for existing in resource['existing_events']: delete_resource_event(existing.uid, resource, existing._msguid) # delete old version referenced save_event elif hasattr(save_event, '_msguid'): delete_resource_event(save_event.uid, resource, save_event._msguid) else: imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda") # append new version result = imap.imap.m.append( targetfolder, None, None, save_event.to_message(creator="Kolab Server ").as_string() ) return result except Exception, e: log.error(_("Failed to save event to resource calendar at %r: %r") % ( resource['kolabtargetfolder'], e )) return False def delete_resource_event(uid, resource, msguid=None): """ Removes the IMAP object with the given UID from a resource's calendar folder """ targetfolder = imap.folder_quote(resource['kolabtargetfolder']) try: imap.set_acl(targetfolder, conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda") imap.imap.m.select(targetfolder) # delete by IMAP UID if msguid is not None: log.debug(_("Delete resource calendar object from %r by UID %r") % ( targetfolder, msguid ), level=8) imap.imap.m.uid('store', msguid, '+FLAGS', '(\\Deleted)') else: typ, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % uid) log.debug(_("Delete resource calendar object %r in %r: %r") % ( uid, resource['kolabtargetfolder'], data ), level=9) for num in data[0].split(): imap.imap.m.store(num, '+FLAGS', '\\Deleted') imap.imap.m.expunge() return True except Exception, e: log.error(_("Failed to delete calendar object %r from folder %r: %r") % ( uid, targetfolder, e )) return False 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)' % ('resources',filepath)) def resource_record_from_email_address(email_address): """ Resolves the given email address to a resource entity """ global auth if not auth: auth = Auth() auth.connect() resource_records = [] local_domains = auth.list_domains() if not local_domains == None: local_domains = list(set(local_domains.keys())) if not email_address.split('@')[1] in local_domains: return [] log.debug( _("Checking if email address %r belongs to a resource (collection)") % (email_address), level=8 ) resource_records = auth.find_resource(email_address) if isinstance(resource_records, list): if len(resource_records) > 0: log.debug(_("Resource record(s): %r") % (resource_records), level=8) else: log.debug(_("No resource (collection) records found for %r") % (email_address), level=9) elif isinstance(resource_records, basestring): resource_records = [ resource_records ] log.debug(_("Resource record: %r") % (resource_records), level=8) return resource_records def resource_records_from_itip_events(itip_events, recipient_email=None): """ Given a list of itip_events, determine which resources have been invited as attendees and/or resources. """ global auth if not auth: auth = Auth() auth.connect() resource_records = [] log.debug(_("Raw itip_events: %r") % (itip_events), level=9) attendees_raw = [] for list_attendees_raw in [x for x in [y['attendees'] for y in itip_events if y.has_key('attendees') and isinstance(y['attendees'], list)]]: attendees_raw.extend(list_attendees_raw) for list_attendees_raw in [y['attendees'] for y in itip_events if y.has_key('attendees') and isinstance(y['attendees'], basestring)]: attendees_raw.append(list_attendees_raw) log.debug(_("Raw set of attendees: %r") % (attendees_raw), level=9) # TODO: Resources are actually not implemented in the format. We reset this # list later. resources_raw = [] for list_resources_raw in [x for x in [y['resources'] for y in itip_events if y.has_key('resources')]]: resources_raw.extend(list_resources_raw) log.debug(_("Raw set of resources: %r") % (resources_raw), level=9) # consider organizer (in REPLY messages), too organizers_raw = [re.sub('\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if y.has_key('organizer')] log.debug(_("Raw set of organizers: %r") % (organizers_raw), level=8) # TODO: We expect the format of an attendee line to literally be: # # ATTENDEE:RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers@kolabsys.com # # which makes the attendees_raw contain: # # RSVP=TRUE;ROLE=REQ-PARTICIPANT;MAILTO:lydia.bossers@kolabsys.com # attendees = [x.split(':')[-1] for x in attendees_raw + organizers_raw] # Limit the attendee resources to the one that is actually invited # with the current message. Considering all invited resources would result in # duplicate responses from every iTip message sent to a resource. if recipient_email is not None: attendees = [a for a in attendees if a == recipient_email] for attendee in attendees: log.debug(_("Checking if attendee %r is a resource (collection)") % (attendee), level=8) _resource_records = auth.find_resource(attendee) if isinstance(_resource_records, list): if len(_resource_records) > 0: resource_records.extend(_resource_records) log.debug(_("Resource record(s): %r") % (_resource_records), level=8) else: log.debug(_("No resource (collection) records found for %r") % (attendee), level=9) elif isinstance(_resource_records, basestring): resource_records.append(_resource_records) log.debug(_("Resource record: %r") % (_resource_records), level=8) else: log.warning(_("Resource reservation made but no resource records found")) # Escape the non-implementation of the free-form, undefined RESOURCES # list(s) in iTip. if len(resource_records) == 0: # TODO: We don't know how to handle this yet! # We expect the format of an resource line to literally be: # RESOURCES:MAILTO:resource-car@kolabsys.com resources_raw = [] resources = [x.split(':')[-1] for x in resources_raw] # Limit the attendee resources to the one that is actually invited # with the current message. if recipient_email is not None: resources = [a for a in resources if a == recipient_email] for resource in resources: log.debug(_("Checking if resource %r is a resource (collection)") % (resource), level=8) _resource_records = auth.find_resource(resource) if isinstance(_resource_records, list): if len(_resource_records) > 0: resource_records.extend(_resource_records) log.debug(_("Resource record(s): %r") % (_resource_records), level=8) else: log.debug(_("No resource (collection) records found for %r") % (resource), level=8) elif isinstance(_resource_records, basestring): resource_records.append(_resource_records) log.debug(_("Resource record: %r") % (_resource_records), level=8) else: log.warning(_("Resource reservation made but no resource records found")) log.debug(_("The following resources are being referred to in the " + \ "iTip: %r") % (resource_records), level=8) return resource_records def get_resource_records(resource_dns): """ Get the resource details, which includes details on the IMAP folder """ global auth resources = {} for resource_dn in list(set(resource_dns)): # Get the attributes for the record # See if it is a resource collection # If it is, expand to individual resources # If it is not, ... resource_attrs = auth.get_entry_attributes(None, resource_dn, ['*']) resource_attrs['dn'] = resource_dn parse_kolabinvitationpolicy(resource_attrs) if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]: if resource_attrs.has_key('uniquemember'): if not isinstance(resource_attrs['uniquemember'], list): resource_attrs['uniquemember'] = [ resource_attrs['uniquemember'] ] resources[resource_dn] = resource_attrs for uniquemember in resource_attrs['uniquemember']: member_attrs = auth.get_entry_attributes( None, uniquemember, ['*'] ) if 'kolabsharedfolder' in [x.lower() for x in member_attrs['objectclass']]: member_attrs['dn'] = uniquemember parse_kolabinvitationpolicy(member_attrs, resource_attrs) resources[uniquemember] = member_attrs resources[uniquemember]['memberof'] = resource_dn if not member_attrs.has_key('owner') and resources[resource_dn].has_key('owner'): resources[uniquemember]['owner'] = resources[resource_dn]['owner'] resource_dns.append(uniquemember) else: resources[resource_dn] = resource_attrs return resources def parse_kolabinvitationpolicy(attrs, parent=None): if attrs.has_key('kolabinvitationpolicy'): if not isinstance(attrs['kolabinvitationpolicy'], list): attrs['kolabinvitationpolicy'] = [attrs['kolabinvitationpolicy']] attrs['kolabinvitationpolicy'] = [policy_name_map[p] for p in attrs['kolabinvitationpolicy'] if policy_name_map.has_key(p)] elif isinstance(parent, dict) and parent.has_key('kolabinvitationpolicy'): attrs['kolabinvitationpolicy'] = parent['kolabinvitationpolicy'] def get_resource_collection(email_address): """ """ resource_dns = resource_record_from_email_address(email_address) if len(resource_dns) == 1: resource_attrs = auth.get_entry_attributes(None, resource_dns[0], ['objectclass']) if not 'kolabsharedfolder' in [x.lower() for x in resource_attrs['objectclass']]: resources = get_resource_records(resource_dns) return (resource_dns, resources) return None def get_resource_owner(resource): """ Get this resource's owner record """ global auth if not auth: auth = Auth() auth.connect() owners = [] if resource.has_key('owner'): if not isinstance(resource['owner'], list): owners = [ resource['owner'] ] else: owners = resource['owner'] else: # get owner attribute from collection collections = auth.search_entry_by_attribute('uniquemember', resource['dn']) if not isinstance(collections, list): collections = [ collections ] for dn,collection in collections: if collection.has_key('owner') and isinstance(collection['owner'], list): owners += collection['owner'] elif collection.has_key('owner'): owners.append(collection['owner']) for dn in owners: owner = auth.get_entry_attributes(None, dn, ['cn','mail','telephoneNumber']) if owner is not None: return owner return None def get_resource_invitationpolicy(resource): """ Get this resource's kolabinvitationpolicy configuration """ global auth if not resource.has_key('kolabinvitationpolicy') or resource['kolabinvitationpolicy'] is None: if not auth: auth = Auth() auth.connect() # get kolabinvitationpolicy attribute from collection collections = auth.search_entry_by_attribute('uniquemember', resource['dn']) if not isinstance(collections, list): collections = [ (collections['dn'],collections) ] log.debug(_("Check collections %r for kolabinvitationpolicy attributes") % (collections), level=9) for dn,collection in collections: # ldap.search_entry_by_attribute() doesn't return the attributes lower-cased if collection.has_key('kolabInvitationPolicy'): collection['kolabinvitationpolicy'] = collection['kolabInvitationPolicy'] if collection.has_key('kolabinvitationpolicy'): parse_kolabinvitationpolicy(collection) resource['kolabinvitationpolicy'] = collection['kolabinvitationpolicy'] break return resource['kolabinvitationpolicy'] if resource.has_key('kolabinvitationpolicy') else None def send_response(from_address, itip_events, owner=None): """ Send the given iCal events as a valid iTip response to the organizer. In case the invited resource coolection was delegated to a concrete resource, this will send an additional DELEGATED response message. """ if isinstance(itip_events, dict): itip_events = [ itip_events ] for itip_event in itip_events: attendee = itip_event['xml'].get_attendee_by_email(from_address) participant_status = itip_event['xml'].get_ical_attendee_participant_status(attendee) # TODO: look-up event organizer in LDAP and change localization to its preferredlanguage message_text = reservation_response_text(participant_status, owner) subject_template = _("Reservation Request for %(summary)s was %(status)s") # Extra actions to take: send delegated reply if participant_status == "DELEGATED": delegatee = [a for a in itip_event['xml'].get_attendees() if from_address in a.get_delegated_from(True)][0] delegated_message_text = _(""" *** This is an automated response, please do not reply! *** Your reservation was delegated to "%s" which is available for the requested time. """) % (delegatee.get_name()) pykolab.itip.send_reply(from_address, itip_event, delegated_message_text, subject=subject_template) # adjust some vars for the regular reply from the delegatee message_text = reservation_response_text(delegatee.get_participant_status(True), owner) from_address = delegatee.get_email() time.sleep(2) pykolab.itip.send_reply(from_address, itip_event, message_text, subject=subject_template) def reservation_response_text(status, owner): message_text = _(""" *** This is an automated response, please do not reply! *** We hereby inform you that your reservation was %s. """) % (participant_status_label(status)) if owner: message_text += _(""" If you have questions about this reservation, please contact %s <%s> %s """) % (owner['cn'], owner['mail'], owner['telephoneNumber'] if owner.has_key('telephoneNumber') else '') return message_text def send_owner_notification(resource, owner, itip_event, success=True): """ Send a reservation notification to the resource owner """ import smtplib from pykolab import utils from email.MIMEText import MIMEText from email.Utils import formatdate # encode unicode strings with quoted-printable from email import charset charset.add_charset('utf-8', charset.SHORTEST, charset.QP) notify = False status = itip_event['xml'].get_attendee_by_email(resource['mail']).get_participant_status(True) invitationpolicy = get_resource_invitationpolicy(resource) if invitationpolicy is not None: for policy in invitationpolicy: # TODO: distingish ACCEPTED / DECLINED status notifications? if policy & COND_NOTIFY and owner['mail']: notify = True break if notify or not success: log.debug( _("Sending booking notification for event %r to %r from %r") % ( itip_event['uid'], owner['mail'], resource['cn'] ), level=8 ) # change gettext language to the preferredlanguage setting of the resource owner if owner.has_key('preferredlanguage'): pykolab.translate.setUserLanguage(owner['preferredlanguage']) message_text = owner_notification_text(resource, owner, itip_event['xml'], success) msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') msg['To'] = owner['mail'] msg['From'] = resource['mail'] msg['Date'] = formatdate(localtime=True) msg['Subject'] = utils.str2unicode(_('Booking for %s has been %s') % ( resource['cn'], participant_status_label(status) if success else _('failed') )) smtp = smtplib.SMTP("localhost", 10027) if conf.debuglevel > 8: smtp.set_debuglevel(True) try: smtp.sendmail(resource['mail'], owner['mail'], msg.as_string()) except Exception, e: log.error(_("SMTP sendmail error: %r") % (e)) smtp.quit() def owner_notification_text(resource, owner, event, success): organizer = event.get_organizer() status = event.get_attendee_by_email(resource['mail']).get_participant_status(True) if success: message_text = _(""" The resource booking for %(resource)s by %(orgname)s <%(orgemail)s> has been %(status)s for %(date)s. *** This is an automated message, sent to you as the resource owner. *** """) else: message_text = _(""" A reservation request for %(resource)s could not be processed automatically. Please contact %(orgname)s <%(orgemail)s> who requested this resource for %(date)s. Subject: %(summary)s. *** This is an automated message, sent to you as the resource owner. *** """) return message_text % { 'resource': resource['cn'], 'summary': event.get_summary(), 'date': event.get_date_text(), 'status': participant_status_label(status), 'orgname': organizer.name(), 'orgemail': organizer.email() } def send_owner_confirmation(resource, owner, itip_event): """ Send a reservation request to the resource owner for manual confirmation (ACCEPT or DECLINE) This clones the given invtation with a new UID and setting the resource as organizer in order to receive the reply from the owner. """ uid = itip_event['uid'] event = itip_event['xml'] organizer = event.get_organizer() event_attendees = [a.get_displayname() for a in event.get_attendees() if not a.get_cutype() == kolabformat.CutypeResource] log.debug( _("Clone invitation for owner confirmation: %r from %r") % ( itip_event['uid'], event.get_organizer().email() ), level=8 ) # generate new UID and set the resource as organizer (mail, domain) = resource['mail'].split('@') event.set_uid(str(uuid.uuid4())) event.set_organizer(mail + '+' + base64.b64encode(uid, '-/') + '@' + domain, resource['cn']) itip_event['uid'] = event.get_uid() # add resource owner as (the sole) attendee event._attendees = [] event.add_attendee(owner['mail'], owner['cn'], rsvp=True, role=kolabformat.Required, participant_status=kolabformat.PartNeedsAction) # flag this iTip message as confirmation type event.add_custom_property('X-Kolab-InvitationType', 'CONFIRMATION') message_text = _(""" A reservation request for %(resource)s requires your approval! Please either accept or decline this invitation without saving it to your calendar. The reservation request was sent from %(orgname)s <%(orgemail)s>. Subject: %(summary)s. Date: %(date)s Participants: %(attendees)s *** This is an automated message, please don't reply by email. *** """)% { 'resource': resource['cn'], 'orgname': organizer.name(), 'orgemail': organizer.email(), 'summary': event.get_summary(), 'date': event.get_date_text(), 'attendees': ",\n+ ".join(event_attendees) } pykolab.itip.send_request(owner['mail'], itip_event, message_text, subject=_('Booking request for %s requires confirmation') % (resource['cn']), direct=True)