diff --git a/pykolab/__init__.py b/pykolab/__init__.py index 09e3267..fdbc3a6 100644 --- a/pykolab/__init__.py +++ b/pykolab/__init__.py @@ -1,55 +1,56 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # """ pyKolab, the interface to Kolab Groupware Solution management in Python. """ import logging import shutil import sys import threading import traceback from pykolab.logger import Logger logging.setLoggerClass(Logger) def getLogger(name): """ Return the correct logger class. """ logging.setLoggerClass(Logger) log = logging.getLogger(name=name) return log from pykolab.conf import Conf conf = Conf() def getConf(): _data = threading.local() if hasattr(_data, 'conf'): log.debug(_("Returning thread local configuration")) return _data.conf return conf -import base +from . import base + diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py index a2edf12..2b3e56a 100644 --- a/pykolab/auth/ldap/__init__.py +++ b/pykolab/auth/ldap/__init__.py @@ -1,3272 +1,3272 @@ # 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 . # # pylint: disable=too-many-lines from __future__ import print_function import datetime # Catch python-ldap-2.4 changes from distutils import version import logging import time import traceback import ldap import ldap.controls try: from ldap.controls import psearch except ImportError: pass from ldap.dn import explode_dn import ldap.filter from six import string_types import _ldap import pykolab from pykolab import utils from pykolab.base import Base from pykolab.constants import SUPPORTED_LDAP_CONTROLS from pykolab.errors import * from pykolab.translate import _ as _l -import auth_cache -import cache +from . import auth_cache +from . import cache # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.auth') conf = pykolab.getConf() class LDAP(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. """ Base.__init__(self, domain=domain) self.ldap = None self.ldap_priv = None self.bind = None if domain is None: self.domain = conf.get('kolab', 'primary_domain') else: self.domain = domain # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-return-statements # pylint: disable=too-many-statements 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( _l("Attempting to authenticate user %s in realm %s") % ( login[0], realm ), level=8 ) except Exception: 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 as errmsg: log.error(_l("Authentication cache failed: %r") % (errmsg)) if base_dn is None: config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if ldap_base_dn is not 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 as errmsg: log.error(_l("Authentication cache failed: %r") % (errmsg)) try: user_filter = self.config_get_raw('user_filter') % ( {'base_dn': base_dn} ) except TypeError: 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 as errmsg: log.error(_l("Authentication cache failed: %r") % (errmsg)) retval = False timeout = float(self.config_get('ldap', 'timeout', default=10)) if entry_dn is None: _search = self.ldap.search_ext( base_dn, ldap.SCOPE_SUBTREE, filterstr=_filter, attrlist=['entrydn'], attrsonly=True, timeout=timeout ) try: ( _result_type, _result_data, _result_msgid, _result_controls ) = self.ldap.result3(_search) except ldap.INVALID_CREDENTIALS: log.error( _l("Invalid DN, username and/or password for '%s'.") % ( _filter ) ) return False except ldap.NO_SUCH_OBJECT: log.error( _l("Invalid DN, username and/or password for '%s'.") % ( _filter ) ) return False except ldap.SERVER_DOWN as errmsg: log.error(_l("LDAP server unavailable: %r") % (errmsg)) log.error(traceback.format_exc()) self._disconnect() return False except ldap.TIMEOUT: log.error(_l("LDAP timeout.")) self._disconnect() return False except Exception as errmsg: log.error(_l("Exception occurred: %r") % (errmsg)) log.error(traceback.format_exc()) self._disconnect() return False log.debug( _l("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, _) = _result_data[0] elif len(_result_data) > 1: try: log.info( _l("Authentication for %r failed (multiple entries)") % ( login[0] ) ) except Exception: pass self._disconnect() return False else: try: log.info( _l("Authentication for %r failed (no entry)") % ( login[0] ) ) except Exception: pass self._disconnect() return False if entry_dn is None: try: log.info( _l("Authentication for %r failed (LDAP error?)") % ( login[0] ) ) except Exception: 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( _l("Authentication for %r succeeded") % ( login[0] ) ) except Exception: pass else: try: log.info( _l("Authentication for %r failed (error)") % ( login[0] ) ) except Exception: pass self._disconnect() return False try: auth_cache.set_entry(_filter, entry_dn) except Exception as errmsg: log.error(_l("Authentication cache failed: %r") % (errmsg)) except ldap.SERVER_DOWN: log.error(_l("Authentication failed, LDAP server unavailable")) self._disconnect() return False except Exception: try: log.debug( _l("Failed to authenticate as user %r") % ( login[0] ), level=8 ) except Exception: 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(_l("Authentication for %r succeeded") % (login[0])) else: log.info( _l("Authentication for %r failed (password)") % ( login[0] ) ) self._disconnect() return False except ldap.NO_SUCH_OBJECT as errmsg: log.debug( _l("Error occured, there is no such object: %r") % ( errmsg ), level=8 ) self.bind = None try: auth_cache.del_entry(_filter) except Exception: log.error(_l("Authentication cache failed to clear entry")) retval = self.authenticate(login, realm) except Exception as errmsg: log.debug(_l("Exception occured: %r") % (errmsg)) try: log.debug( _l("Failed to authenticate as user %r") % ( login[0] ), level=8 ) except Exception: 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(_l("Connecting to LDAP..."), level=8) uri = self.config_get('ldap_uri') log.debug(_l("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, trace_file=pykolab.logger.StderrToLogger(log), 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 ldap_base_dn is not None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn _filter = "(%s=%s)" % (unique_attribute, ldap.filter.escape_filter_chars(entry_id)) _search = self.ldap.search_ext( base_dn, ldap.SCOPE_SUBTREE, _filter, ['entrydn'] ) ( _result_type, _result_data, _result_msgid, _result_controls ) = self.ldap.result3(_search) if len(_result_data) >= 1: (entry_dn, _) = _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 attribute in entry_attrs: return entry_attrs[attribute] if attribute.lower() in entry_attrs: return entry_attrs[attribute.lower()] return None def get_entry_attributes(self, entry_id, attributes): """ Get multiple attributes for an entry. """ self._bind() log.debug(_l("Entry ID: %r") % (entry_id), level=8) entry_dn = self.entry_dn(entry_id) log.debug(_l("Entry DN: %r") % (entry_dn), level=8) log.debug( _l("ldap search: (%r, %r, filterstr='(objectclass=*)', attrlist=[ 'dn' ] + %r") % ( entry_dn, ldap.SCOPE_BASE, attributes ), level=8 ) _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 [] # pylint: disable=no-self-use 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 attr in entry: if isinstance(entry[attr], list): recipient_addresses.extend(entry[attr]) elif isinstance(entry[attr], string_types): 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 is None: mailbox_attribute = 'mail' for __delegator in self.search_entry_by_attribute('kolabDelegate', entry_id): (_dn, _delegator) = __delegator _delegator['dn'] = _dn if mailbox_attribute in _delegator: _delegator['_mailbox_basename'] = _delegator[mailbox_attribute] else: _delegator['_mailbox_basename'] = 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 exclude_entry_id is not 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 resource_filter is not 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, string_types): _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(_l("Finding resource with filter %r") % (_filter), level=8) if len(_filter) <= 6: return None resource_base_dn = self._object_base_dn('resource') _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 exclude_entry_id is not None: __filter_prefix = "(&" __filter_suffix = "(!(%s=%s)))" % ( self.config_get('unique_attribute'), ldap.filter.escape_filter_chars(exclude_entry_id) ) else: __filter_prefix = "" __filter_suffix = "" 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, string_types): _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(_l("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 ldap_base_dn is not 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 _entry_id is not 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 exclude_entry_id is not None: __filter_prefix = "(&" __filter_suffix = "(!(%s=%s)))" % ( self.config_get('unique_attribute'), ldap.filter.escape_filter_chars(exclude_entry_id) ) else: __filter_prefix = "" __filter_suffix = "" resource_filter = self.config_get('resource_filter') if resource_filter is not 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, string_types): _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(_l("Finding resource with filter %r") % (_filter), level=8) if len(_filter) <= 6: return None resource_base_dn = self._object_base_dn('resource') _results = self.ldap.search_s( resource_base_dn, scope=ldap.SCOPE_SUBTREE, filterstr=_filter, attrlist=result_attributes, attrsonly=True ) # Remove referrals _entry_dns = [_e[0] for _e in _results if _e[0] is not None] return _entry_dns def get_latest_sync_timestamp(self): timestamp = cache.last_modify_timestamp(self.domain) log.debug(_l("Using timestamp %r") % (timestamp), level=8) return timestamp def list_secondary_domains(self): """ List alias domain name spaces for the current domain name space. """ if self.domains is not None: return [s for s in self.domains if s not in self.domains.values()] 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 daemon_rcpt_policy is not None: log.debug( _l("Not applying recipient policy for %s (disabled through configuration)") % ( entry_dn ), level=1 ) return entry_modifications want_attrs = [] log.debug(_l("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 is None and entry_type == 'user': primary_mail = self.config_get_raw('primary_mail') if secondary_mail_attribute is not None: secondary_mail = self.config_get_raw('%s_secondary_mail' % (entry_type)) if secondary_mail is None and entry_type == 'user': secondary_mail = self.config_get_raw('secondary_mail') log.debug( _l("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 _mail_attr not in entry: log.debug(_l("key %r not in entry") % (_mail_attr), level=8) if _mail_attr == primary_mail_attribute: log.debug(_l("key %r is the prim. mail attr.") % (_mail_attr), level=8) if primary_mail is not None: log.debug(_l("prim. mail pol. is not empty"), level=8) want_attrs.append(_mail_attr) elif _mail_attr == secondary_mail_attribute: log.debug(_l("key %r is the sec. mail attr.") % (_mail_attr), level=8) if secondary_mail is not None: log.debug(_l("sec. mail pol. is not empty"), level=8) want_attrs.append(_mail_attr) if want_attrs: log.debug( _l("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 'preferredlanguage' not in entry: want_attrs.append('preferredlanguage') # If we wanted anything, now is the time to get it. if want_attrs: log.debug( _l("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: entry[attribute] = attributes[attribute] if 'preferredlanguage' not in entry: entry['preferredlanguage'] = conf.get('kolab', 'default_locale') # Primary mail address if primary_mail is not 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 is 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 not results: log.debug( _l("No results for mail address %s found") % ( _primary_mail ), level=8 ) done = True continue if len(results) == 1: log.debug( _l("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( _l( "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(_l("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 primary_mail_address is not None: if primary_mail_attribute not in entry: 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 # pylint: disable=too-many-nested-blocks if secondary_mail is not 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 not results: log.debug( _l("No results for address %s found") % ( __secondary_mail ), level=8 ) done = True continue if len(results) == 1: log.debug( _l("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( _l( "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(_l("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( _l( "Recipient policy composed the following set of secondary email addresses: %r" ) % ( secondary_mail_addresses ), level=8 ) if secondary_mail_attribute in entry: 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 secondary_mail_addresses is not None: log.debug( _l("Secondary mail addresses that we want is not None: %r") % ( secondary_mail_addresses ), level=8 ) secondary_mail_addresses = list(set(secondary_mail_addresses)) # Avoid duplicates while primary_mail_address in secondary_mail_addresses: log.debug( _l( "Avoiding the duplication of the primary mail " + "address %r in the list of secondary mail " + "addresses" ) % (primary_mail_address), level=8 ) secondary_mail_addresses.pop( secondary_mail_addresses.index(primary_mail_address) ) log.debug( _l("Entry is getting secondary mail addresses: %r") % ( secondary_mail_addresses ), level=8 ) if secondary_mail_attribute not in entry: log.debug( _l("Entry did not have any secondary mail addresses in %r") % ( secondary_mail_attribute ), level=8 ) if secondary_mail_addresses: 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], string_types): entry[secondary_mail_attribute] = [entry[secondary_mail_attribute]] log.debug( _l("secondary_mail_addresses: %r") % (secondary_mail_addresses), level=8 ) log.debug( _l("entry[%s]: %r") % ( secondary_mail_attribute, entry[secondary_mail_attribute] ), level=8 ) secondary_mail_addresses.sort() entry[secondary_mail_attribute].sort() log.debug( _l("secondary_mail_addresses: %r") % (secondary_mail_addresses), level=8 ) log.debug( _l("entry[%s]: %r") % ( secondary_mail_attribute, entry[secondary_mail_attribute] ), level=8 ) smas = list(set(secondary_mail_addresses)) if smas != list(set(entry[secondary_mail_attribute])): self.set_entry_attribute( entry, secondary_mail_attribute, smas ) entry_modifications[secondary_mail_attribute] = smas log.debug(_l("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 ldap_base_dn is not None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn _results = self._search( base_dn, filterstr=_filter, attrlist=[ '*', ], override_search='_regular_search' ) # Remove referrals _entry_dns = [_e for _e in _results if _e[0] is not None] return _entry_dns def set_entry_attribute(self, entry_id, attribute, value): log.debug( _l("Setting entry attribute %r to %r for %r") % (attribute, value, entry_id), level=8 ) 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: attrs[attribute.lower()] = attributes[attribute] modlist = [] for attribute, value in attrs.items(): if attribute not in entry: entry[attribute] = self.get_entry_attribute(entry_id, attribute) if attribute in entry and entry[attribute] is None: modlist.append((ldap.MOD_ADD, attribute, value)) elif attribute in entry and entry[attribute] is not None: if value is None: modlist.append((ldap.MOD_DELETE, attribute, entry[attribute])) else: modlist.append((ldap.MOD_REPLACE, attribute, value)) dn = entry_dn if modlist and self._bind_priv() is True: try: self.ldap_priv.modify_s(dn, modlist) except Exception as errmsg: log.error( _l("Could not update dn:\nDN: %r\nModlist: %r\nError Message: %r") % ( dn, modlist, errmsg ) ) log.error(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', default="%Y%m%d%H%M%SZ" ).replace('%%', '%') modified_after = datetime.datetime(1900, 1, 1, 00, 00, 00).strftime( modifytimestamp_format ) else: modified_after = self.get_latest_sync_timestamp() _filter = "(&%s(modifytimestamp>=%s))" % (_filter, modified_after) log.debug(_l("Synchronization is using filter %r") % (_filter), level=8) if 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 ldap_base_dn is not None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn log.debug(_l("Synchronization is searching against base DN: %s") % (base_dn), level=8) if callback is 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 as errmsg: log.error("An error occurred: %r" % (errmsg)) log.error(_l("%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 is None: return # The default quota may be None, but LDAP quota could still be set if default_quota is 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 is None: used = None current_imap_quota = None else: (used, current_imap_quota) = _imap_quota log.debug( _l( "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=8 ) 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 Exception: current_ldap_quota = None # If the new quota is zero, get out if new_quota == 0: return if current_ldap_quota is not None: if not new_quota == (int)(current_ldap_quota): self.set_entry_attribute( entry_dn, quota_attribute, "%s" % (new_quota) ) else: if new_quota is not None: self.set_entry_attribute( entry_dn, quota_attribute, "%s" % (new_quota) ) if current_imap_quota is not None: if not new_quota == current_imap_quota: self.imap.set_quota(folder, new_quota) else: if new_quota is not 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(_l("Attempting to bind without a DN but with a password")) return False # and the same vice-versa if bind_dn is None and bind_pw is not None: log.error(_l("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( _l("Binding with bind_dn: %s and password: %s") % ( bind_dn, '*' * len(bind_pw) ), level=8 ) # 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 as errmsg: log.error(_l("LDAP server unavailable: %r") % (errmsg)) log.error(_l("%s") % (traceback.format_exc())) return False except ldap.NO_SUCH_OBJECT: log.error( _l("Invalid DN, username and/or password for '%s'.") % ( bind_dn ) ) return False except ldap.INVALID_CREDENTIALS: log.error( _l("Invalid DN, username and/or password for '%s'.") % ( bind_dn ) ) return False else: log.debug(_l("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 as errmsg: log.error(_l("LDAP server unavailable: %r") % (errmsg)) log.error(_l("%s") % (traceback.format_exc())) return False except ldap.INVALID_CREDENTIALS: log.error( _l("Invalid DN, username and/or password for '%s'.") % ( bind_dn ) ) return False else: log.debug(_l("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 mailserver_attribute in entry: server = entry[mailserver_attribute] foldertype_attribute = self.config_get('sharedfolder_type_attribute') if foldertype_attribute is not None: if foldertype_attribute not in entry: entry[foldertype_attribute] = self.get_entry_attribute( entry['id'], foldertype_attribute ) if entry[foldertype_attribute] is not None: entry['kolabfoldertype'] = entry[foldertype_attribute] if 'kolabfoldertype' not in entry: 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 is None: delivery_address_attribute = 'mail' if delivery_address_attribute not in entry: entry[delivery_address_attribute] = self.get_entry_attribute( entry['id'], delivery_address_attribute ) if entry[delivery_address_attribute] is not None: if len(entry[delivery_address_attribute].split('+')) > 1: entry['kolabtargetfolder'] = entry[delivery_address_attribute].split('+')[1] if 'kolabtargetfolder' not in entry: entry['kolabtargetfolder'] = self.get_entry_attribute( entry['id'], 'kolabtargetfolder' ) if 'kolabtargetfolder' in entry and entry['kolabtargetfolder'] is not 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 is None: folderacl_entry_attribute = 'acl' if folderacl_entry_attribute not in entry: 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 'kolabfoldertype' in entry and entry['kolabfoldertype'] is not None: self.imap.shared_folder_set_type(folder_path, entry['kolabfoldertype']) entry['kolabfolderaclentry'] = self._parse_acl(entry[folderacl_entry_attribute]) # pylint: disable=protected-access self.imap._set_kolab_mailfolder_acls(entry['kolabfolderaclentry'], folder_path) if delivery_address_attribute in entry: if entry[delivery_address_attribute] is not None: self.imap.set_acl(folder_path, 'anyone', '+p') # if server is 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. """ success = None result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute not in entry: return None if entry[result_attribute] is None: return None for _type in ['user', 'group', 'role', 'sharedfolder']: try: func = getattr(self, '_change_add_%s' % (_type)) func(entry, change) success = True except Exception: success = False if success: break return success def _change_add_user(self, entry, change): """ An entry of type user was added. """ mailserver_attribute = self.config_get('mailserver_attribute') if mailserver_attribute is None: mailserver_attribute = 'mailhost' mailserver_attribute = mailserver_attribute.lower() result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute is None: result_attribute = 'mail' result_attribute = result_attribute.lower() if mailserver_attribute not in entry: 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 result_attribute not in entry: entry[result_attribute] = self.get_entry_attribute(entry, result_attribute) return if result_attribute not in entry: return if not entry[result_attribute]: 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( _l("Entry %s attribute value: %r") % ( mailserver_attribute, entry[mailserver_attribute] ), level=8 ) log.debug( _l("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 result_attribute not in entry: return None if entry[result_attribute] is None: return None return 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 result_attribute not in entry: return None if entry[result_attribute] is None: return None success = True for _type in ['user', 'group', 'resource', 'role', 'sharedfolder']: try: func = getattr(self, '_change_delete_%s' % (_type)) success = func(entry, change) except Exception as errmsg: log.error(_l("An error occured: %r") % (errmsg)) log.error(_l("%s") % (traceback.format_exc())) success = False if success: break return success def _change_delete_user(self, entry, change): """ An entry of type user was deleted. """ result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute not in entry: return None if entry[result_attribute] is 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 } ) return True 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'] old_rdn = explode_dn(old_dn)[0].split('=')[0] new_rdn = 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 cache_entry is not 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, value in entry_changes.items(): entry[key] = value if result_attribute not in entry: return if entry[result_attribute] is 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 result_attribute in entry_changes: if old_canon_attr is not 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): result_attribute = 'cn' old_cn = explode_dn(change['previous_dn'], True)[0] if 'kolabtargetfolder' in entry and entry['kolabtargetfolder'] is not None: new_folder_path = entry['kolabtargetfolder'] old_folder_path = old_cn if '@' in entry['kolabtargetfolder']: old_folder_path = "%s@%s" % ( old_folder_path, entry['kolabtargetfolder'].split('@')[1] ) else: result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute in ['mail']: new_folder_path = "%s@%s" % (entry['cn'], self.domain) old_folder_path = "%s@%s" % (old_cn, self.domain) else: new_folder_path = "%s" % (entry['cn']) old_folder_path = old_cn if not new_folder_path.startswith('shared/'): new_folder_path = "shared/%s" % (new_folder_path) if not old_folder_path.startswith('shared/'): old_folder_path = "shared/%s" % (old_folder_path) log.debug("old folder path: %r" % (old_folder_path)) log.debug("new folder path: %r" % (new_folder_path)) self.imap.shared_folder_rename(old_folder_path, new_folder_path) 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 mailserver_attribute in entry: server = entry[mailserver_attribute] foldertype_attribute = self.config_get('sharedfolder_type_attribute') if foldertype_attribute is not None: if foldertype_attribute not in entry: entry[foldertype_attribute] = self.get_entry_attribute( entry['id'], foldertype_attribute ) if entry[foldertype_attribute] is not None: entry['kolabfoldertype'] = entry[foldertype_attribute] if 'kolabfoldertype' not in entry: 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 is not None: if delivery_address_attribute not in entry: entry[delivery_address_attribute] = self.get_entry_attribute( entry['id'], delivery_address_attribute ) if entry[delivery_address_attribute] is not None: if len(entry[delivery_address_attribute].split('+')) > 1: entry['kolabtargetfolder'] = entry[delivery_address_attribute].split('+')[1] if 'kolabtargetfolder' not in entry: entry['kolabtargetfolder'] = self.get_entry_attribute( entry['id'], 'kolabtargetfolder' ) if 'kolabtargetfolder' in entry and entry['kolabtargetfolder'] is not 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 is None: folderacl_entry_attribute = 'acl' if folderacl_entry_attribute not in entry: 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 'kolabfoldertype' in entry and entry['kolabfoldertype'] is not None: self.imap.shared_folder_set_type( folder_path, entry['kolabfoldertype'] ) entry['kolabfolderaclentry'] = self._parse_acl(entry[folderacl_entry_attribute]) # pylint: disable=protected-access self.imap._set_kolab_mailfolder_acls(entry['kolabfolderaclentry'], folder_path, True) if delivery_address_attribute in entry and entry[delivery_address_attribute] is not 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 _entry is not None: if 'result_attribute' in _entry.__dict__ and not _entry.result_attribute == '': old_canon_attr = _entry.result_attribute entry_changes = self.recipient_policy(entry) log.debug( _l("Result from recipient policy: %r") % (entry_changes), level=8 ) if result_attribute in entry_changes: if not entry_changes[result_attribute] == old_canon_attr: if old_canon_attr is 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 result_attribute in entry: if not entry[result_attribute] == old_canon_attr: if old_canon_attr is 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: if not self.imap.user_mailbox_exists(entry[result_attribute]): 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 mailserver_attribute in entry: server = entry[mailserver_attribute] if 'kolabtargetfolder' not in entry: entry['kolabtargetfolder'] = self.get_entry_attribute( entry['id'], 'kolabtargetfolder' ) if 'kolabfoldertype' not in entry: entry['kolabfoldertype'] = self.get_entry_attribute( entry['id'], 'kolabfoldertype' ) folderacl_entry_attribute = conf.get('ldap', 'sharedfolder_acl_entry_attribute') if folderacl_entry_attribute is None: folderacl_entry_attribute = 'acl' if folderacl_entry_attribute not in entry: entry['kolabfolderaclentry'] = self.get_entry_attribute( entry['id'], folderacl_entry_attribute ) else: entry['kolabfolderaclentry'] = entry[folderacl_entry_attribute] del entry[folderacl_entry_attribute] if 'kolabtargetfolder' in entry and entry['kolabtargetfolder'] is not 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 'kolabfoldertype' in entry and entry['kolabfoldertype'] is not 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 delivery_address_attribute in entry and \ entry[delivery_address_attribute] is not None: self.imap.set_acl(folder_path, 'anyone', '+p') # if server is 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 is None: mailserver_attribute = 'mailhost' mailserver_attribute = mailserver_attribute.lower() result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute is None: result_attribute = 'mail' result_attribute = result_attribute.lower() old_canon_attr = None _entry = cache.get_entry(self.domain, entry, update=False) if _entry is not None and \ 'result_attribute' in _entry.__dict__ and \ not _entry.result_attribute == '': old_canon_attr = _entry.result_attribute entry_changes = self.recipient_policy(entry) if result_attribute in entry and result_attribute in entry_changes: if not entry[result_attribute] == entry_changes[result_attribute]: old_canon_attr = entry[result_attribute] log.debug( _l("Result from recipient policy: %r") % (entry_changes), level=8 ) if result_attribute in entry_changes and old_canon_attr is not 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: 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 mailserver_attribute not in entry: entry[mailserver_attribute] = self.get_entry_attribute(entry, mailserver_attribute) if entry[mailserver_attribute] == "" or entry[mailserver_attribute] is None: server = None else: server = entry[mailserver_attribute].lower() if result_attribute in entry and entry[result_attribute] is not 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 mailserver_attr not in entry: 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( _l("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']) # Lower case of naming contexts - primarily for AD naming_contexts = utils.normalize(attrs['namingcontexts']) if isinstance(naming_contexts, string_types): naming_contexts = [naming_contexts] log.debug( _l("Naming contexts found: %r") % (naming_contexts), level=8 ) self._kolab_domain_root_dn(domain) log.debug( _l("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].lower().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 'dn' in value: 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, string_types): return False try: 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 ldap_base_dn is not 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 is None: __filter = self.config_get('%s_filter' % (_type)) if __filter is not None: try: result = self._regular_search(entry_dn, filterstr=__filter) except Exception: result = self._regular_search( base_dn, filterstr="(%s=%s)" % ( self.config_get('unique_attribute'), entry_id['id'] ) ) if not result: continue else: return _type return None 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 '' user_base_dn = self._object_base_dn('user', conf_prefix) 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(_l("Searching root dn for domain %r") % (domain), level=8) if not hasattr(self, 'domain_rootdns'): self.domain_rootdns = {} if domain in self.domain_rootdns: log.debug(_l("Returning from cache: %r") % (self.domain_rootdns[domain]), level=8) return self.domain_rootdns[domain] self._bind() log.debug(_l("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 domain_filter is not None: if domain is not 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' ) 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_rootdn_attribute in _domain_attrs: log.debug( _l("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 is 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 is None: __filter = self.config_get('%s_filter' % (_type)) if __filter is not 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(_l("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 domain is not None: domain_filter = domain_filter.replace('*', domain) if domain_base_dn is None or domain_filter is None: return [] dna = self.config_get('domain_name_attribute') if dna is 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 Exception: 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 _object_base_dn(self, objectType, prefix=''): """ Get configured base DN for specified Kolab object type """ object_base_dn = self.config_get(prefix + objectType + '_base_dn') config_base_dn = self.config_get('base_dn') ldap_base_dn = self._kolab_domain_root_dn(self.domain) if ldap_base_dn is not None and not ldap_base_dn == config_base_dn: base_dn = ldap_base_dn else: base_dn = config_base_dn if object_base_dn is None: object_base_dn = base_dn else: object_base_dn = object_base_dn % ({'base_dn': base_dn}) return object_base_dn 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=8 ) # Typical for Persistent Change Control EntryChangeNotification if 'change_type' in kw: log.debug( _l( "change_type defined, typical for Persistent Change " + "Control EntryChangeNotification" ), level=5 ) 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']) # Ignore nstombstone objects if 'objectclass' in entry: if 'nstombstone' in entry['objectclass']: return None entry['dn'] = kw['dn'] unique_attr = self.config_get('unique_attribute') entry['id'] = entry[unique_attr] try: entry['type'] = self._entry_type(entry) except Exception: entry['type'] = None log.debug(_l("Entry type: %s") % (entry['type']), level=8) if change_dict['change_type'] is 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 result_attribute not in entry: 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 'entry' in kw and isinstance(kw['entry'], list): log.debug(_l("No change_type, typical for Paged Results Control"), level=5) for entry_dn, entry_attrs in kw['entry']: # This is a referral if entry_dn is None: continue entry = {'dn': entry_dn} entry_attrs = utils.normalize(entry_attrs) for attr in entry_attrs: entry[attr.lower()] = entry_attrs[attr] # Ignore nstombstone objects if 'objectclass' in entry: if 'nstombstone' in entry['objectclass']: return None unique_attr = self.config_get('unique_attribute').lower() entry['id'] = entry[unique_attr] try: entry['type'] = self._entry_type(entry) except Exception: entry['type'] = "unknown" log.debug(_l("Entry type for dn: %s is: %s") % (entry['dn'], 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(_l("Recipient Addresses: %r") % (rcpt_addrs), level=8) # # for key in rcpt_addrs: # 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=[] ): psearch_server_controls = [] psearch_server_controls.append( ldap.controls.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(_l("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( _l("Entry Change Notification attributes:"), level=8 ) log.debug( " " + _l("Change Type: %r (%r)") % ( change_type, change_type_desc ), level=8 ) log.debug( " " + _l("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 _results = [] server_page_control = ldap.controls.libldap.SimplePagedResultsControl(size=page_size,cookie='') _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: log.warning( _l("Object %s searched no longer exists") % (base_dn) ) break # Remove referrals _result_data = [_e for _e in _result_data if _e[0] is not None] if callback: callback(entry=_result_data) _results.extend(_result_data) if (pages % 2) == 0: log.debug(_l("%d results...") % (len(_results))) pctrls = [ c for c in _result_controls if c.controlType == ldap.controls.libldap.SimplePagedResultsControl.controlType ] 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 + from . 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, trace_file=pykolab.logger.StderrToLogger(log), 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=None, callback=False, primary_domain=None, secondary_domains=[] ): if timeout is None: timeout = float(self.config_get('ldap', 'timeout', default=10)) log.debug(_l("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 _result is not 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=None, 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. """ if timeout is None: timeout = float(self.config_get('ldap', 'timeout', default=10)) supported_controls = conf.get_list('ldap', 'supported_controls') if supported_controls is not 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: log.debug( _l("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: if SUPPORTED_LDAP_CONTROLS[control_num]['oid'] in \ supported_controls: log.debug( _l("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 override_search is not 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 as errmsg: log.error(_l("LDAP server unavailable: %r") % (errmsg)) log.error(_l("%s") % (traceback.format_exc())) log.error(_l("-- reconnecting in 10 seconds.")) self._disconnect() time.sleep(10) self.reconnect() except ldap.TIMEOUT: log.error(_l("LDAP timeout in searching for '%s'") % (filterstr)) self._disconnect() time.sleep(10) self.reconnect() except Exception as errmsg: failed_ok = True log.error(_l("An error occured using %s: %r") % (supported_control, errmsg)) log.error(_l("%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/pykolab/cli/__init__.py b/pykolab/cli/__init__.py index 46c9930..3aafd03 100644 --- a/pykolab/cli/__init__.py +++ b/pykolab/cli/__init__.py @@ -1,76 +1,76 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import ldap import ldif import logging import traceback import shutil import sys import time import codecs import locale from ldap.modlist import addModlist import pykolab import pykolab.plugins from pykolab import utils from pykolab import conf from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() class Cli(object): def __init__(self): - import commands + from . import commands commands.__init__() to_execute = [] arg_num = 0 for arg in sys.argv[1:]: arg_num += 1 if not arg.startswith('-') and len(sys.argv) >= arg_num: if sys.argv[arg_num].replace('-','_') in commands.commands: to_execute.append(sys.argv[arg_num].replace('-','_')) if "%s_%s" % ( '_'.join(to_execute),sys.argv[arg_num].replace('-','_') ) in commands.commands: to_execute.append(sys.argv[arg_num].replace('-','_')) for cmd_component in to_execute: sys.argv.pop(sys.argv.index(cmd_component.replace('_','-'))) # force default encoding to match the locale encoding (T249) reload(sys) sys.setdefaultencoding(locale.getpreferredencoding() or 'utf-8') # wrap sys.stdout in a locale-aware StreamWriter (#3983) sys.stdout = codecs.getwriter(locale.getpreferredencoding())(sys.stdout) commands.execute('_'.join(to_execute)) def run(self): pass diff --git a/pykolab/cli/cmd_acl_cleanup.py b/pykolab/cli/cmd_acl_cleanup.py index 2d6bd6e..54cfd38 100644 --- a/pykolab/cli/cmd_acl_cleanup.py +++ b/pykolab/cli/cmd_acl_cleanup.py @@ -1,68 +1,68 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('acl_cleanup', execute, description=description()) def description(): return _("Clean up ACLs that use identifiers that no longer exist") def execute(*args, **kw): """ List mailboxes """ try: aci_subject = conf.cli_args.pop(0) except: aci_subject = None imap = IMAP() imap.connect() folders = imap.lm() for folder in folders: acls = imap.list_acls(folder) if not aci_subject == None: if aci_subject in acls: log.debug(_("Deleting ACL %s for subject %s on folder %s") % ( acls[aci_subject], aci_subject, folder ), level=8) imap.set_acl(folder, aci_subject, '') #else: #for _aci_subject in acls: # connect to auth(!) # find recipient result_attr=aci_subject # if no entry, expire acl diff --git a/pykolab/cli/cmd_add_alias.py b/pykolab/cli/cmd_add_alias.py index 81ca005..8f90032 100644 --- a/pykolab/cli/cmd_add_alias.py +++ b/pykolab/cli/cmd_add_alias.py @@ -1,132 +1,132 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function from six import string_types import sys -import commands +from . import commands import pykolab from pykolab.auth import Auth from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('add_alias', execute, description="Add alias.") def execute(*args, **kw): try: primary_rcpt_address = conf.cli_args.pop(0) try: secondary_rcpt_address = conf.cli_args.pop(0) except: print(_("Specify the (new) alias address"), file=sys.stderr) sys.exit(1) except: print(_("Specify the existing recipient address"), file=sys.stderr) sys.exit(1) if len(primary_rcpt_address.split('@')) > 1: primary_rcpt_domain = primary_rcpt_address.split('@')[-1] else: primary_rcpt_domain = conf.get('kolab', 'primary_domain') auth = Auth(domain=primary_rcpt_domain) domains = auth.list_domains() #print domains if len(secondary_rcpt_address.split('@')) > 1: secondary_rcpt_domain = secondary_rcpt_address.split('@')[-1] else: secondary_rcpt_domain = conf.get('kolab', 'primary_domain') # Check if either is in fact a domain if not primary_rcpt_domain.lower() in domains: print(_("Domain %r is not a local domain") % (primary_rcpt_domain), file=sys.stderr) sys.exit(1) if not secondary_rcpt_domain.lower() in domains: print(_("Domain %r is not a local domain") % (secondary_rcpt_domain), file=sys.stderr) sys.exit(1) if not primary_rcpt_domain == secondary_rcpt_domain: if not domains[primary_rcpt_domain] == domains[secondary_rcpt_domain]: print(_("Primary and secondary domain do not have the same parent domain"), file=sys.stderr) sys.exit(1) primary_recipient_dn = auth.find_recipient(primary_rcpt_address) if primary_recipient_dn == [] or len(primary_recipient_dn) == 0: print(_("No such recipient %r") % (primary_rcpt_address), file=sys.stderr) sys.exit(1) secondary_recipient_dn = auth.find_recipient(secondary_rcpt_address) if not secondary_recipient_dn == [] and not len(secondary_recipient_dn) == 0: print(_("Recipient for alias %r already exists") % (secondary_rcpt_address), file=sys.stderr) sys.exit(1) rcpt_attrs = conf.get_list('ldap', 'mail_attributes') primary_rcpt_attr = rcpt_attrs[0] if len(rcpt_attrs) >= 2: secondary_rcpt_attr = rcpt_attrs[1] else: print(_("Environment is not configured for " + \ "users to hold secondary mail attributes"), file=sys.stderr) sys.exit(1) primary_recipient = auth.get_entry_attributes(primary_rcpt_domain, primary_recipient_dn, rcpt_attrs) if primary_rcpt_attr not in primary_recipient: print(_("Recipient %r is not the primary recipient for address %r") % (primary_recipient, primary_rcpt_address), file=sys.stderr) sys.exit(1) if secondary_rcpt_attr not in primary_recipient: auth.set_entry_attributes(primary_rcpt_domain, primary_recipient_dn, {secondary_rcpt_attr: [ secondary_rcpt_address ] }) else: if isinstance(primary_recipient[secondary_rcpt_attr], string_types): new_secondary_rcpt_attrs = [ primary_recipient[secondary_rcpt_attr], secondary_rcpt_address ] else: new_secondary_rcpt_attrs = \ primary_recipient[secondary_rcpt_attr] + \ [ secondary_rcpt_address ] auth.set_entry_attributes( primary_rcpt_domain, primary_recipient_dn, { secondary_rcpt_attr: new_secondary_rcpt_attrs } ) diff --git a/pykolab/cli/cmd_add_domain.py b/pykolab/cli/cmd_add_domain.py index fecd82a..8034281 100644 --- a/pykolab/cli/cmd_add_domain.py +++ b/pykolab/cli/cmd_add_domain.py @@ -1,69 +1,69 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import sys -import commands +from . import commands import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('add_domain', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--alias', dest = "domains", action = "append", default = [], help = _("Add alias domain."), metavar = "DOMAIN", ) def description(): return _("Add a new domain.") def execute(*args, **kw): from pykolab import wap_client # Use uber-administrative privileges username = conf.get('ldap', 'bind_dn') if username == None: log.error(_("Could not find credentials with sufficient permissions" + \ "to add a domain name space.")) sys.exit(1) wap_client.authenticate(username=username) dna = conf.get('ldap', 'domain_name_attribute') try: domain = conf.cli_args.pop(0) except IndexError: domain = utils.ask_question(_("Domain name")) wap_client.domain_add(domain, conf.domains) diff --git a/pykolab/cli/cmd_add_user.py b/pykolab/cli/cmd_add_user.py index 09cc08c..b01b9ad 100644 --- a/pykolab/cli/cmd_add_user.py +++ b/pykolab/cli/cmd_add_user.py @@ -1,38 +1,38 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('add_user', execute, description="Add a user.") def execute(*args, **kw): from pykolab import wap_client # Create the authentication object. # TODO: Binds with superuser credentials! wap_client.authenticate() wap_client.user_add() diff --git a/pykolab/cli/cmd_add_user_subscription.py b/pykolab/cli/cmd_add_user_subscription.py index 06ac0e2..50ba9af 100644 --- a/pykolab/cli/cmd_add_user_subscription.py +++ b/pykolab/cli/cmd_add_user_subscription.py @@ -1,87 +1,87 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register( 'add_user_subscription', execute, aliases=['aus', 'subscribe'], description=description() ) def description(): return _("Subscribe a user to a folder.") def execute(*args, **kw): folder_pattern = "*" try: user = conf.cli_args.pop(0) try: folder_pattern = conf.cli_args.pop(0) except IndexError: folder_pattern = utils.ask_question(_("Folder pattern")) except IndexError: user = utils.ask_question(_("User ID")) folder_pattern = utils.ask_question(_("Folder pattern")) if len(user.split('@')) > 1: domain = user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, user) if not imap.has_folder(folder_pattern): print(_("Cannot subscribe user to folder %r:") % (folder_pattern), \ _("No such folder"), \ file=sys.stderr) sys.exit(1) _folders = imap.lm(folder_pattern) for _folder in _folders: imap.subscribe(_folder) diff --git a/pykolab/cli/cmd_check_quota.py b/pykolab/cli/cmd_check_quota.py index cae5077..c102735 100644 --- a/pykolab/cli/cmd_check_quota.py +++ b/pykolab/cli/cmd_check_quota.py @@ -1,110 +1,110 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('check_quota', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--dry-run', dest = "dryrun", action = "store", default = False, help = _("Do not apply any changes.") ) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.") ) def description(): return _("Compare existing IMAP quota with LDAP quota.") def execute(*args, **kw): """ List mailboxes """ imap = IMAP() imap.connect(server=conf.connect_server) auth = Auth() auth.connect() domains = auth.list_domains() folders = [] for domain in domains: folders = imap.lm("user/%%@%s" % (domain)) domain_auth = Auth(domain=domain) domain_auth.connect(domain=domain) for folder in folders: login = folder.split('/')[1] user_dn = domain_auth.find_recipient(login) if user_dn == None: print(_("No such user %s") % (login), file=sys.stderr) continue if len(login.split('@')) > 1: domain = login.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') try: user_quota = auth.get_entry_attribute(domain, user_dn, 'mailquota') except: user_quota = None if user_quota == None: print(_("No quota for user %s") % (login), file=sys.stderr) continue try: (used, quota) = imap.get_quota(folder) if not (int)(quota) == (int)(user_quota): print(_("user quota does not match for %s (IMAP: %d, LDAP: %d)") % (login, (int)(quota), (int)(user_quota)), file=sys.stderr) except: pass diff --git a/pykolab/cli/cmd_count_domain_mailboxes.py b/pykolab/cli/cmd_count_domain_mailboxes.py index 47bbcb1..0559bcc 100644 --- a/pykolab/cli/cmd_count_domain_mailboxes.py +++ b/pykolab/cli/cmd_count_domain_mailboxes.py @@ -1,66 +1,66 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import datetime -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('count_domain_mailboxes', execute) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.")) def execute(*args, **kw): """ List deleted mailboxes """ imap = IMAP() imap.connect() auth = Auth() auth.connect() domains = auth.list_domains() folders = [] for domain in domains: print("%s: %d" % (domain,len(imap.lm("user/%%@%s" % (domain))))) null_realm = len(imap.lm("user/%%")) if null_realm > 0: print("null: %d" % (null_realm)) diff --git a/pykolab/cli/cmd_create_mailbox.py b/pykolab/cli/cmd_create_mailbox.py index 9e91800..4da5c47 100644 --- a/pykolab/cli/cmd_create_mailbox.py +++ b/pykolab/cli/cmd_create_mailbox.py @@ -1,78 +1,78 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('create_mailbox', execute, description=description(), aliases='cm') def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--metadata', dest = "metadata", action = "store", default = None, help = _("Set metadata for folder to ANNOTATION=VALUE") ) my_option_group.add_option( '--partition', dest = "partition", action = "store", default = None, help = _("Create folder on PARTITION.") ) def description(): return """Create a mailbox or sub-folder of an existing mailbox.""" def execute(*args, **kw): try: mailbox = conf.cli_args.pop(0) except IndexError: log.error(_("Invalid argument")) sys.exit(1) if not conf.metadata == None: if len(conf.metadata.split('=')) == 2: annotation = conf.metadata.split('=')[0] annotation_value = conf.metadata.split('=')[1] else: log.error(_("Invalid argument for metadata")) sys.exit(1) imap = IMAP() imap.connect() imap.create_folder(mailbox, partition=conf.partition) if not conf.metadata == None: imap.set_metadata(mailbox, conf.metadata.split('=')[0], conf.metadata.split('=')[1]) diff --git a/pykolab/cli/cmd_delete_domain.py b/pykolab/cli/cmd_delete_domain.py index 60ca55b..28bbcaf 100644 --- a/pykolab/cli/cmd_delete_domain.py +++ b/pykolab/cli/cmd_delete_domain.py @@ -1,74 +1,74 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import sys -import commands +from . import commands import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('delete_domain', execute, description=description()) def description(): return _("Delete a domain.") def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--force', dest = "force", action = "store_true", default = False, help = _("Force deleting the domain even if it contains user accounts") ) def execute(*args, **kw): from pykolab import wap_client # Use uber-administrative privileges username = conf.get('ldap', 'bind_dn') if username == None: log.error(_("Could not find credentials with sufficient permissions" + \ "to add a domain name space.")) sys.exit(1) wap_client.authenticate(username=username) dna = conf.get('ldap', 'domain_name_attribute') try: domain = conf.cli_args.pop(0) except IndexError: domain = utils.ask_question(_("Domain name")) if wap_client.domain_delete(domain, conf.force): print("Domain %s has been marked as deleted." % domain) print("Please run this command to actually delete the domain: ") print(" php /usr/share/kolab-webadmin/bin/domain_delete.php") else: print("Domain %s has not been deleted." % domain) diff --git a/pykolab/cli/cmd_delete_mailbox.py b/pykolab/cli/cmd_delete_mailbox.py index 40f8b44..66ec534 100644 --- a/pykolab/cli/cmd_delete_mailbox.py +++ b/pykolab/cli/cmd_delete_mailbox.py @@ -1,72 +1,72 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('delete_mailbox', execute, description=description(), aliases=['dm']) def description(): return """Delete a mailbox or sub-folder. Note that the mailbox or folder is removed recursively.""" def execute(*args, **kw): """ Delete mailbox """ if len(conf.cli_args) < 1: print(_("No mailbox specified"), file=sys.stderr) sys.exit(1) imap = IMAP() imap.connect() delete_folders = [] while len(conf.cli_args) > 0: folder = conf.cli_args.pop(0) folders = imap.list_folders(folder) if len(folders) < 1: print(_("No such folder(s): %s") % (folder), file=sys.stderr) delete_folders.extend(folders) if len(delete_folders) == 0: print(_("No folders to delete."), file=sys.stderr) sys.exit(1) for delete_folder in delete_folders: try: imap.delete_mailfolder(delete_folder) except Exception: log.error(_("Could not delete mailbox '%s'") % (delete_folder)) diff --git a/pykolab/cli/cmd_delete_mailbox_acl.py b/pykolab/cli/cmd_delete_mailbox_acl.py index d146aac..c15f85b 100644 --- a/pykolab/cli/cmd_delete_mailbox_acl.py +++ b/pykolab/cli/cmd_delete_mailbox_acl.py @@ -1,71 +1,71 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('delete_mailbox_acl', execute, description=description(), aliases=['dam']) def description(): return """Delete an ACL entry for a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) try: identifier = conf.cli_args.pop(0) except IndexError: identifier = utils.ask_question(_("ACI Subject")) except IndexError: folder = utils.ask_question(_("Folder name")) quota = utils.ask_question(_("ACI Subject")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: folders = imap.list_folders(folder) for folder in folders: try: imap.set_acl(folder, identifier, '') except: # Mailbox no longer exists? pass diff --git a/pykolab/cli/cmd_delete_message.py b/pykolab/cli/cmd_delete_message.py index 5c7c242..5f68ee8 100644 --- a/pykolab/cli/cmd_delete_message.py +++ b/pykolab/cli/cmd_delete_message.py @@ -1,69 +1,69 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('delete_message', execute, description=description()) def description(): return _("Delete a message from a folder") def execute(*args, **kw): """ Delete a message from a mail folder """ try: folder = conf.cli_args.pop(0) try: uid = conf.cli_args.pop(0) except: log.error(_("Specify a UID")) sys.exit(1) except: log.error(_("Specify a folder")) sys.exit(1) imap = IMAP() imap.connect() _folder = imap.lm(folder) if _folder == None or _folder == []: log.error(_("No such folder")) sys.exit(1) imap.set_acl(folder, 'cyrus-admin', 'lrswt') imap.select(folder) imap.store(uid, '+FLAGS', '\\Deleted') diff --git a/pykolab/cli/cmd_export_mailbox.py b/pykolab/cli/cmd_export_mailbox.py index 78d55f1..f37c7b2 100644 --- a/pykolab/cli/cmd_export_mailbox.py +++ b/pykolab/cli/cmd_export_mailbox.py @@ -1,124 +1,124 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function -import commands +from . import commands import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('export_mailbox', execute) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--all', dest = "all", action = "store_true", default = False, help = _("All folders this user has access to")) def execute(*args, **kw): import os import subprocess user = conf.cli_args.pop(0) # TODO: /etc/imapd.conf is not the definitive location for the # imapd.conf configuration file. partition_proc = subprocess.Popen( ['grep', '^partition', '/etc/imapd.conf'], stdout=subprocess.PIPE ) partitions = [ x.split(':')[1].strip() for x in partition_proc.communicate()[0].split('\n') if len(x.split(':')) > 1 ] # TODO: ctl_mboxlist is not necessarily in this location. ctl_mboxlist_args = [ '/usr/lib/cyrus-imapd/ctl_mboxlist', '-d' ] ctl_mboxlist = subprocess.Popen( ctl_mboxlist_args, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) mboxlist_proc = subprocess.Popen( ['grep', '-E', '\s*%s\s*.*i.*p.*' % (user)], stdin=ctl_mboxlist.stdout, stdout=subprocess.PIPE ) ctl_mboxlist.stdout.close() # TODO: Handle errors from ctl_mboxlist process (stderr) mboxlist_output = mboxlist_proc.communicate()[0] zipper_args = [ 'zip', '-r', '%s.zip' % (user) ] directories = [] for mbox_internal in mboxlist_output.split('\n'): if len(mbox_internal.split('\t')[0].split('!')) > 1: domain = mbox_internal.split('\t')[0].split('!')[0] mailbox = '/'.join( mbox_internal.split( '\t' )[0].split( '!' )[1].split( '.' )[1:] ) for partition in partitions: mbox_dir = '%s/domain/%s/%s/%s/user/%s/' % ( partition, domain[0], domain, user[0], mailbox ) if os.path.isdir(mbox_dir): directories.append(mbox_dir) else: log.debug( _('%s is not a directory') % (mbox_dir), level=5 ) if not len(directories) == 0: zipper_output = subprocess.Popen( zipper_args + directories, stdout=subprocess.PIPE ).communicate()[0] print(_("ZIP file at %s.zip") % (user), file=sys.stderr) else: print(_("No directories found for user %s") % (user), file=sys.stderr) sys.exit(1) diff --git a/pykolab/cli/cmd_find_domain.py b/pykolab/cli/cmd_find_domain.py index f65af3f..64dc2de 100644 --- a/pykolab/cli/cmd_find_domain.py +++ b/pykolab/cli/cmd_find_domain.py @@ -1,58 +1,58 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import sys -import commands +from . import commands import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('find_domain', execute, description=description()) def description(): return _("Find a domain.") def execute(*args, **kw): from pykolab import wap_client # Use uber-administrative privileges username = conf.get('ldap', 'bind_dn') if username == None: log.error(_("Could not find credentials with sufficient permissions" + \ "to add a domain name space.")) sys.exit(1) wap_client.authenticate(username=username) dna = conf.get('ldap', 'domain_name_attribute') try: domain = conf.cli_args.pop(0) except IndexError: domain = utils.ask_question(_("Domain name")) wap_client.domain_find(domain) diff --git a/pykolab/cli/cmd_list_deleted_mailboxes.py b/pykolab/cli/cmd_list_deleted_mailboxes.py index 55b6b9f..6596142 100644 --- a/pykolab/cli/cmd_list_deleted_mailboxes.py +++ b/pykolab/cli/cmd_list_deleted_mailboxes.py @@ -1,114 +1,114 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import datetime -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_deleted_mailboxes', execute) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--raw', dest="raw", action="store_true", default=False, help=_("Display raw IMAP UTF-7 folder names") ) my_option_group.add_option( '--server', dest="connect_server", action="store", default=None, metavar="SERVER", help=_("List mailboxes on server SERVER only.") ) def execute(*args, **kw): """ List deleted mailboxes """ folders = [] searches = [] imap = IMAP() if conf.connect_server is not None: imap.connect(server=conf.connect_server) else: imap.connect() # See if conf.cli_args components make sense. for arg in conf.cli_args: if arg == '*': searches.append(arg) if arg.startswith('user'): searches.append(arg) if arg.startswith('shared'): searches.append(arg) if arg.startswith('DELETED'): searches.append(arg) if arg.startswith('news'): searches.append(arg) if len(searches) == 0: auth = Auth() auth.connect() domains = auth.list_domains() folders = [] for domain in list(set(domains.keys())): folders.extend(imap.lm("DELETED/*@%s" % (domain))) folders.extend(imap.lm("DELETED/*")) else: for search in searches: log.debug(_("Appending folder search for %r") % (search), level=8) folders.extend(imap.lm(imap_utf7.encode(search))) print("Deleted folders:") for folder in folders: utf8_folder = imap_utf7.decode(folder).encode('utf-8') mbox_parts = imap.parse_mailfolder(utf8_folder) ts = datetime.datetime.fromtimestamp(int(mbox_parts['hex_timestamp'], 16)) if not conf.raw: print("%s (Deleted at %s)" % (utf8_folder, ts)) else: print("%s (Deleted at %s)" % (folder, ts)) diff --git a/pykolab/cli/cmd_list_domain_mailboxes.py b/pykolab/cli/cmd_list_domain_mailboxes.py index fd9a7fd..602fc5d 100644 --- a/pykolab/cli/cmd_list_domain_mailboxes.py +++ b/pykolab/cli/cmd_list_domain_mailboxes.py @@ -1,84 +1,84 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import utils from pykolab import imap_utf7 from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_domain_mailboxes', execute) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--raw', dest = "raw", action = "store_true", default = False, help = _("Display raw IMAP UTF-7 folder names")) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.")) def execute(*args, **kw): """ List deleted mailboxes """ try: domain = conf.cli_args.pop(0) except: domain = utils.ask_question(_("Domain")) imap = IMAP() imap.connect() auth = Auth() auth.connect() domains = auth.list_domains() folders = [] for primary,secondaries in domains: if not domain == primary and not domain in secondaries: continue folders.extend(imap.lm("user/%%@%s" % (primary))) for secondary in secondaries: folders.extend(imap.lm("user/%%@%s" % (secondary))) print("Deleted folders:") for folder in folders: if not conf.raw: print(imap_utf7.decode(folder)) else: print(folder) diff --git a/pykolab/cli/cmd_list_domains.py b/pykolab/cli/cmd_list_domains.py index e76edca..d72ed71 100644 --- a/pykolab/cli/cmd_list_domains.py +++ b/pykolab/cli/cmd_list_domains.py @@ -1,52 +1,52 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_domains', execute, description="List Kolab domains.") def execute(*args, **kw): from pykolab import wap_client # Create the authentication object. # TODO: Binds with superuser credentials! wap_client.authenticate() domains = wap_client.domains_list() dna = conf.get('ldap', 'domain_name_attribute') print("%-39s %-40s" % ("Primary Domain Name Space","Secondary Domain Name Space(s)")) # TODO: Take a hint in --quiet, and otherwise print out a nice table # with headers and such. if isinstance(domains['list'], dict): for domain_dn in domains['list']: if isinstance(domains['list'][domain_dn][dna], list): print(domains['list'][domain_dn][dna][0]) for domain_alias in domains['list'][domain_dn][dna][1:]: print("%-39s %-40s" % ('', domain_alias)) else: print(domains['list'][domain_dn][dna]) diff --git a/pykolab/cli/cmd_list_mailbox_acls.py b/pykolab/cli/cmd_list_mailbox_acls.py index dc86e47..d4f76d5 100644 --- a/pykolab/cli/cmd_list_mailbox_acls.py +++ b/pykolab/cli/cmd_list_mailbox_acls.py @@ -1,67 +1,67 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_mailbox_acls', execute, description=description(), aliases=['lam']) def description(): return """Obtain a list of ACL entries on a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) except IndexError: folder = utils.ask_question(_("Folder name")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: acls = [] folders = imap.list_folders(folder) for folder in folders: print("Folder", folder) acls = imap.list_acls(folder) for acl in acls: print(" %-13s %s" %(acls[acl], acl)) diff --git a/pykolab/cli/cmd_list_mailbox_metadata.py b/pykolab/cli/cmd_list_mailbox_metadata.py index 27d881c..7734770 100644 --- a/pykolab/cli/cmd_list_mailbox_metadata.py +++ b/pykolab/cli/cmd_list_mailbox_metadata.py @@ -1,97 +1,97 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_mailbox_metadata', execute, aliases='lmm', description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--user', dest = "user", action = "store", default = None, metavar = "USER", help = _("List annotations as user USER") ) def description(): return """Obtain a list of metadata entries on a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) except IndexError: folder = utils.ask_question(_("Folder name")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] elif not conf.user == None and len(conf.user.split('@')) > 1: domain = conf.user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() if not conf.user == None: imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, conf.user) else: imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: metadata = [] folders = imap.list_folders(folder) for folder in folders: print("Folder", folder) metadata = imap.get_metadata(folder) if folder in metadata: for annotation in metadata[folder]: print(" %-49s %s" % ( annotation, metadata[folder][annotation] )) diff --git a/pykolab/cli/cmd_list_mailboxes.py b/pykolab/cli/cmd_list_mailboxes.py index 738cd86..3cb7f03 100644 --- a/pykolab/cli/cmd_list_mailboxes.py +++ b/pykolab/cli/cmd_list_mailboxes.py @@ -1,103 +1,103 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_mailboxes', execute, description=description(), aliases='lm') def description(): return "List mailboxes.\n" + \ "%-28s" % ('') + \ "Use wildcards '*' and '%' for more control.\n" def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--raw', dest="raw", action="store_true", default=False, help=_("Display raw IMAP UTF-7 folder names") ) my_option_group.add_option( '--server', dest="connect_server", action="store", default=None, metavar="SERVER", help=_("List mailboxes on server SERVER only.") ) def execute(*args, **kw): """ List mailboxes """ searches = [] # See if conf.cli_args components make sense. for arg in conf.cli_args: if arg == '*': searches.append(arg) if arg.startswith('user'): searches.append(arg) if arg.startswith('shared'): searches.append(arg) if arg.startswith('DELETED'): searches.append(arg) if arg.startswith('news'): searches.append(arg) if len(searches) == 0: searches = [''] imap = IMAP() if not conf.connect_server == None: imap.connect(server=conf.connect_server) else: imap.connect() folders = [] for search in searches: log.debug(_("Appending folder search for %r") % (search), level=8) folders.extend(imap.lm(imap_utf7.encode(search))) for folder in folders: if not conf.raw: print(imap_utf7.decode(folder)) else: print(folder) diff --git a/pykolab/cli/cmd_list_messages.py b/pykolab/cli/cmd_list_messages.py index 786a5f0..af2c447 100644 --- a/pykolab/cli/cmd_list_messages.py +++ b/pykolab/cli/cmd_list_messages.py @@ -1,134 +1,134 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import sys -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_messages', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--deleted', dest = "list_deleted", action = "store_true", default = False, help = _("Include messages flagged as \Deleted") ) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.") ) my_option_group.add_option( '--user', dest = "user", action = "store", default = None, metavar = "USER", help = _("List messages as user USER") ) def description(): return _("List messages in a folder") def execute(*args, **kw): """ List messages in a folder """ try: folder = conf.cli_args.pop(0) except: log.error(_("Specify a folder")) sys.exit(1) domain = None imap = IMAP() if not conf.user == None: imap.connect(domain=domain, login=False, server=conf.connect_server) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, conf.user) else: imap.connect(domain=domain, server=conf.connect_server) _folder = imap.lm(imap_utf7.encode(folder)) if _folder == None or _folder == []: log.error(_("No such folder")) sys.exit(1) if conf.user == None: imap.set_acl(folder, 'cyrus-admin', 'lrs') imap.select(imap_utf7.encode(folder)) if conf.list_deleted: typ, data = imap.search(None, 'ALL') else: typ, data = imap.search(None, '(ALL UNDELETED)') num_messages = len(data[0].split()) for num in data[0].split(): typ, flags = imap.fetch(num, 'FLAGS') flags = flags[0].split() if len(flags) >= 3: # Any flags are set if flags[2] == '(\\Deleted))': print(num, '\Deleted') elif flags[2] == '(\\Deleted': print(num, '\Deleted') elif '\\Deleted' in flags[3:]: print(num, '\Deleted') elif '\\Deleted))' in flags[3:]: print(num, '\Deleted') else: print(num) else: print(num) if conf.user == None: imap.set_acl(folder, 'cyrus-admin', '') diff --git a/pykolab/cli/cmd_list_ous.py b/pykolab/cli/cmd_list_ous.py index 61a4cb9..46b991f 100644 --- a/pykolab/cli/cmd_list_ous.py +++ b/pykolab/cli/cmd_list_ous.py @@ -1,39 +1,39 @@ # -*- coding: utf-8 -*- # Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_ous', execute, description="List organizational units.") def execute(*args, **kw): from pykolab import wap_client wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw")) ous = wap_client.ous_list() print('\n'.join(ous['list'].keys())) diff --git a/pykolab/cli/cmd_list_quota.py b/pykolab/cli/cmd_list_quota.py index d956168..7dd6b9d 100644 --- a/pykolab/cli/cmd_list_quota.py +++ b/pykolab/cli/cmd_list_quota.py @@ -1,104 +1,104 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_quota', execute, description=description(), aliases=['lq']) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.")) def description(): return """List quota for a folder.""" def execute(*args, **kw): """ List quota for a mailbox """ try: quota_folder = conf.cli_args.pop(0) except IndexError: quota_folder = '*' imap = IMAP() if not conf.connect_server == None: imap.connect(server=conf.connect_server) else: imap.connect() folders = [] quota_folders = imap.list_folders(quota_folder) for quota_folder in quota_folders: try: (used, quota) = imap.get_quota(quota_folder) print("Folder: %s" % (quota_folder)) if not used == None and not quota == None: if quota == 0: print(_("The quota for folder %s is set to literally allow 0KB of storage.") % (quota_folder), file=sys.stderr) print("%d (Used: %d, Percentage: %s)" % (quota, used, u'\u221E')) else: percentage = round(((float)(used)/(float)(quota)) * 100.0, 1) print("%d (Used: %d, Percentage: %d)" % (quota, used, percentage)) else: if used == None: print("%d (Used: %d, Percentage: %d)" % (quota, 0, 0)) else: print("No quota") except: try: (quota_root, used, quota) = imap.get_quota_root(quota_folder) print("Folder: %s" % (quota_folder)) if not quota_root == None and not used == None and not quota == None: if quota == 0: print(_("The quota for folder %s is set to literally allow 0KB of storage.") % (quota_folder), file=sys.stderr) print("%d (Used: %d, Percentage: %d)" % (quota, used, u'\u221E')) else: percentage = round(((float)(used)/(float)(quota)) * 100.0, 1) print("%d (Root: %s, Used: %d, Percentage: %d)" % (quota, quota_root, used, percentage)) else: if used == None and not quota_root == None: print("%d (Root: %s, Used: %d, Percentage: %d)" % (quota, quota_root, 0, 0)) else: print("No quota") except: print("Folder: %s" % (quota_folder)) print("No quota root") diff --git a/pykolab/cli/cmd_list_user_subscriptions.py b/pykolab/cli/cmd_list_user_subscriptions.py index ad6e549..3d90812 100644 --- a/pykolab/cli/cmd_list_user_subscriptions.py +++ b/pykolab/cli/cmd_list_user_subscriptions.py @@ -1,104 +1,104 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_user_subscriptions', execute, aliases='lus', description=description()) def cli_options(*args, **kw): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--raw', dest = "raw", action = "store_true", default = False, help = _("Display raw IMAP UTF-7 folder names")) my_option_group.add_option( '--unsubscribed', dest = "unsubscribed", action = "store_true", default = False, help = _("List unsubscribed folders")) def description(): return _("List the folders a user is subscribed to.") def execute(*args, **kw): folder_pattern = "*" try: user = conf.cli_args.pop(0) try: folder_pattern = conf.cli_args.pop(0) except IndexError: pass except IndexError: user = utils.ask_question(_("User ID")) if len(user.split('@')) > 1: domain = user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, user) subscribed_folders = imap.lsub(folder_pattern) if conf.unsubscribed: unsubscribed_folders = [] all_folders = imap.lm(folder_pattern) for folder in all_folders: if not folder in subscribed_folders: unsubscribed_folders.append(folder) if len(unsubscribed_folders) > 0: if not conf.raw: print("\n".join([imap_utf7.decode(x) for x in unsubscribed_folders])) else: print("\n".join(unsubscribed_folders)) else: print(_("No unsubscribed folders for user %s") % (user)) else: if not conf.raw: print("\n".join([imap_utf7.decode(x) for x in subscribed_folders])) else: print("\n".join(subscribed_folders)) diff --git a/pykolab/cli/cmd_list_users.py b/pykolab/cli/cmd_list_users.py index a17eeba..bb5506e 100644 --- a/pykolab/cli/cmd_list_users.py +++ b/pykolab/cli/cmd_list_users.py @@ -1,39 +1,39 @@ # -*- coding: utf-8 -*- # Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_users', execute, description="List organizational units.") def execute(*args, **kw): from pykolab import wap_client wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw")) users = wap_client.users_list() print('\n'.join(users['list'].keys())) diff --git a/pykolab/cli/cmd_mailbox_cleanup.py b/pykolab/cli/cmd_mailbox_cleanup.py index 656a978..ccc964d 100644 --- a/pykolab/cli/cmd_mailbox_cleanup.py +++ b/pykolab/cli/cmd_mailbox_cleanup.py @@ -1,228 +1,228 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('mailbox_cleanup', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--dry-run', dest="dryrun", action="store_true", default=False, help=_( "Do not actually delete mailboxes, but report what mailboxes would have been deleted." ) ) my_option_group.add_option( '--server', dest="connect_server", action="store", default=None, metavar="SERVER", help=_("Evaluate mailboxes on server SERVER only.") ) my_option_group.add_option( '--with-acls', dest="with_acls", action="store", default=False, help=_("Evaluate ACLs on mailboxes as well.") ) def description(): return _("Clean up mailboxes that do no longer have an owner.") # pylint: disable=too-many-branches,too-many-locals,too-many-statements def execute(*args, **kw): """ List mailboxes """ auth = Auth() domains = auth.list_domains() imap = IMAP() if conf.connect_server is not None: imap.connect(server=conf.connect_server) else: imap.connect() domain_folders = {} subjects = [] # Placeholder for subjects that would have already been deleted subjects_deleted = [] for domain in domains: domain_folders[domain] = imap.lm("user/%%@%s" % (domain)) for domain in domain_folders: auth = Auth(domain=domain) auth.connect(domain) for folder in domain_folders[domain]: user = folder.replace('user/', '') try: recipient = auth.find_recipient(user) # pylint: disable=bare-except except: if user not in subjects_deleted and conf.dryrun: subjects_deleted.append(user) if conf.dryrun: log.info(_("Would have deleted folder 'user/%s' (dryrun)") % (user)) else: log.info(_("Deleting folder 'user/%s'") % (user)) continue if len(recipient) == 0 or recipient == []: if user not in subjects_deleted and conf.dryrun: subjects_deleted.append(user) if conf.dryrun: log.info(_("Would have deleted folder 'user/%s' (dryrun)") % (user)) else: log.info(_("Deleting folder 'user/%s'") % (user)) try: imap.dm(folder) # pylint: disable=bare-except except: log.error(_("Error deleting folder 'user/%s'") % (user)) else: log.debug(_("Valid recipient found for 'user/%s'") % (user), level=6) if user not in subjects: subjects.append(user) imap_domains = [] folders = imap.lm() for folder in folders: components = folder.split('/') if len(components) < 2: log.error("Not enough components for folder %s" % (folder)) continue mailbox = folder.split('/')[1] if len(mailbox.split('@')) > 1: domain = mailbox.split('@')[1] if domain not in domains and domain not in imap_domains: imap_domains.append(domain) for domain in imap_domains: for folder in imap.lm('user/%%@%s' % (domain)): user = folder.replace('user/', '') if user not in subjects_deleted and conf.dryrun: subjects_deleted.append(user) if conf.dryrun: log.info(_("Would have deleted folder '%s' (dryrun)") % (folder)) else: log.info(_("Deleting folder '%s'") % (folder)) try: imap.dm(folder) # pylint: disable=bare-except except: log.error(_("Error deleting folder '%s'") % (folder)) for folder in imap.lm('shared/%%@%s' % (domain)): if conf.dryrun: log.info(_("Would have deleted folder '%s' (dryrun)") % (folder)) else: log.info(_("Deleting folder '%s'") % (folder)) try: imap.dm(folder) # pylint: disable=bare-except except: log.error(_("Error deleting folder '%s'") % (folder)) if not conf.with_acls: return for folder in [x for x in imap.lm() if not x.startswith('DELETED/')]: folder = imap_utf7.decode(folder) acls = imap.list_acls(folder) for subject in acls: if subject == 'anyone': log.info( _("Skipping removal of ACL %s for subject %s on folder %s") % ( acls[subject], subject, folder ) ) continue if subject not in subjects and subject not in subjects_deleted: if conf.dryrun: log.info( _("Would have deleted ACL %s for subject %s on folder %s") % ( acls[subject], subject, folder ) ) else: log.info( _("Deleting ACL %s for subject %s on folder %s") % ( acls[subject], subject, folder ) ) try: imap.set_acl(folder, subject, '') # pylint: disable=bare-except except: log.error( _("Error removing ACL %s for subject %s from folder %s") % ( acls[subject], subject, folder ) ) diff --git a/pykolab/cli/cmd_remove_mailaddress.py b/pykolab/cli/cmd_remove_mailaddress.py index 120c2d9..cfc4f89 100644 --- a/pykolab/cli/cmd_remove_mailaddress.py +++ b/pykolab/cli/cmd_remove_mailaddress.py @@ -1,96 +1,96 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function from six import string_types import sys -import commands +from . import commands import pykolab from pykolab.auth import Auth from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('remove_mail', execute, description=description()) def description(): return """Remove a recipient's mail address.""" def execute(*args, **kw): try: email_address = conf.cli_args.pop(0) except IndexError: email_address = utils.ask_question("Email address to remove") # Get the domain from the email address if len(email_address.split('@')) > 1: domain = email_address.split('@')[1] else: log.error(_("Invalid or unqualified email address.")) sys.exit(1) auth = Auth() auth.connect(domain=domain) recipients = auth.find_recipient(email_address) if len(recipients) == 0: log.error(_("No recipient found for email address %r") % (email_address)) sys.exit(1) log.debug(_("Found the following recipient(s): %r") % (recipients), level=8) mail_attributes = conf.get_list(domain, 'mail_attributes') if mail_attributes == None or len(mail_attributes) < 1: mail_attributes = conf.get_list(conf.get('kolab', 'auth_mechanism'), 'mail_attributes') log.debug(_("Using the following mail attributes: %r") % (mail_attributes), level=8) if isinstance(recipients, string_types): recipient = recipients # Only a single recipient found, remove the address attributes = auth.get_entry_attributes(domain, recipient, mail_attributes) # See which attribute holds the value we're trying to remove for attribute in attributes: if isinstance(attributes[attribute], list): if email_address in attributes[attribute]: attributes[attribute].pop(attributes[attribute].index(email_address)) replace_attributes = { attribute: attributes[attribute] } auth.set_entry_attributes(domain, recipient, replace_attributes) else: if email_address == attributes[attribute]: auth.set_entry_attributes(domain, recipient, {attribute: None}) pass else: print(_("Found the following recipients:"), file=sys.stderr) for recipient in recipients: print(recipient) diff --git a/pykolab/cli/cmd_remove_user_subscription.py b/pykolab/cli/cmd_remove_user_subscription.py index b16249c..208ff4a 100644 --- a/pykolab/cli/cmd_remove_user_subscription.py +++ b/pykolab/cli/cmd_remove_user_subscription.py @@ -1,103 +1,103 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register( 'remove_user_subscription', execute, aliases=['rus', 'unsubscribe'], description=description() ) def description(): return _("Unsubscribe a user from a folder.") def execute(*args, **kw): folder_pattern = "*" try: user = conf.cli_args.pop(0) try: folder_pattern = conf.cli_args.pop(0) except IndexError: folder_pattern = utils.ask_question(_("Folder pattern")) except IndexError: user = utils.ask_question(_("User ID")) folder_pattern = utils.ask_question(_("Folder pattern")) if len(user.split('@')) > 1: domain = user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, user) if not imap.has_folder(folder_pattern): print(_("Cannot subscribe user to folder %r:") % (folder_pattern), \ _("No such folder"), \ file=sys.stderr) sys.exit(1) _folders = imap.lm(folder_pattern) _subscribed_folders = imap.lsub() unsubscribed_folders = [] for _folder in _folders: if _folder in _subscribed_folders: imap.unsubscribe(_folder) unsubscribed_folders.append(_folder) if len(unsubscribed_folders) > 0: print(_("Successfully unsubscribed user %s from the following folders:") % ( user )) print("\n".join(unsubscribed_folders)) else: print(_("User %s was not unsubscribed from any folders.") % ( user ), file=sys.stderr) sys.exit(1) diff --git a/pykolab/cli/cmd_rename_mailbox.py b/pykolab/cli/cmd_rename_mailbox.py index 4373acf..b46c79d 100644 --- a/pykolab/cli/cmd_rename_mailbox.py +++ b/pykolab/cli/cmd_rename_mailbox.py @@ -1,76 +1,76 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('rename_mailbox', execute, description=description(), aliases=['rm']) def description(): return """Rename a mailbox or sub-folder.""" def execute(*args, **kw): """ Rename mailbox """ try: source_folder = conf.cli_args.pop(0) try: target_folder = conf.cli_args.pop(0) try: partition = conf.cli_args.pop(0) except IndexError: partition = None except IndexError: print(_("No target mailbox name specified"), file=sys.stderr) except IndexError: print(_("No source mailbox name specified"), file=sys.stderr) sys.exit(1) if len(source_folder.split('@')) > 1: domain = source_folder.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain) if not imap.has_folder(source_folder): print(_("Source folder %r does not exist") % (source_folder), file=sys.stderr) sys.exit(1) if imap.has_folder(target_folder) and partition == None: print(_("Target folder %r already exists") % (target_folder), file=sys.stderr) sys.exit(1) imap.imap.rename(imap.folder_utf7(source_folder), imap.folder_utf7(target_folder), partition) diff --git a/pykolab/cli/cmd_server_info.py b/pykolab/cli/cmd_server_info.py index 0ae0c0e..f0f867a 100644 --- a/pykolab/cli/cmd_server_info.py +++ b/pykolab/cli/cmd_server_info.py @@ -1,58 +1,58 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('server_info', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.")) def description(): return "Display server info.\n" def execute(*args, **kw): """ List mailboxes """ imap = IMAP() if not conf.connect_server == None: imap.connect(server=conf.connect_server) else: imap.connect() print(imap.get_metadata("")) diff --git a/pykolab/cli/cmd_set_language.py b/pykolab/cli/cmd_set_language.py index 83b5d95..df575c5 100644 --- a/pykolab/cli/cmd_set_language.py +++ b/pykolab/cli/cmd_set_language.py @@ -1,41 +1,41 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('set_language', execute, description=description()) def description(): return """Set the user's preferred language.""" def execute(*args, **kw): uid = conf.cli_args.pop(0) language = conf.cli_args.pop(0) user = auth.find_user('uid', uid) auth.set_user_attribute('klab.cc', user, 'preferredlanguage', language) diff --git a/pykolab/cli/cmd_set_mail.py b/pykolab/cli/cmd_set_mail.py index 0b457cb..97512fd 100644 --- a/pykolab/cli/cmd_set_mail.py +++ b/pykolab/cli/cmd_set_mail.py @@ -1,41 +1,41 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('set_mail', execute, description=description()) def description(): return """Set the user's primary email address.""" def execute(*args, **kw): uid = conf.cli_args.pop(0) primary_mail = conf.cli_args.pop(0) user = auth.find_user('uid', uid) auth.set_user_attribute('klab.cc', user, 'mail', primary_mail) diff --git a/pykolab/cli/cmd_set_mailbox_acl.py b/pykolab/cli/cmd_set_mailbox_acl.py index 63b5c2d..a18c48b 100644 --- a/pykolab/cli/cmd_set_mailbox_acl.py +++ b/pykolab/cli/cmd_set_mailbox_acl.py @@ -1,74 +1,74 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('set_mailbox_acl', execute, description=description(), aliases=['sam']) def description(): return """Set an ACL for a identifier on a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) try: identifier = conf.cli_args.pop(0) try: acl = conf.cli_args.pop(0) except IndexError: acl = utils.ask_question(_("ACI Permissions")) except IndexError: identifier = utils.ask_question(_("ACI Subject")) acl = utils.ask_question(_("ACI Permissions")) except IndexError: folder = utils.ask_question(_("Folder name")) identifier = utils.ask_question(_("ACI Subject")) acl = utils.ask_question(_("ACI Permissions")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: folders = imap.list_folders(folder) for folder in folders: imap.set_acl(folder, identifier, acl) diff --git a/pykolab/cli/cmd_set_mailbox_metadata.py b/pykolab/cli/cmd_set_mailbox_metadata.py index 91aec50..339b664 100644 --- a/pykolab/cli/cmd_set_mailbox_metadata.py +++ b/pykolab/cli/cmd_set_mailbox_metadata.py @@ -1,101 +1,101 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import imap_utf7 from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('set_mailbox_metadata', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--user', dest = "user", action = "store", default = None, metavar = "USER", help = _("Set annotation as user USER") ) def description(): return """Set an metadata entry on a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) try: metadata_path = conf.cli_args.pop(0) try: metadata_value = conf.cli_args.pop(0) except IndexError: metadata_value = utils.ask_question(_("Metadata value")) except IndexError: metadata_path = utils.ask_question(_("Metadata path")) metadata_value = utils.ask_question(_("Metadata value")) except IndexError: folder = utils.ask_question(_("Folder name")) metadata_path = utils.ask_question(_("Metadata path")) metadata_value = utils.ask_question(_("Metadata value")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] elif not conf.user == None and len(conf.user.split('@')) > 1: domain = conf.user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() if not conf.user == None: imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, conf.user) else: imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: folders = imap.lm(imap_utf7.encode(folder)) for folder in folders: imap.set_metadata(imap_utf7.decode(folder), metadata_path, metadata_value) diff --git a/pykolab/cli/cmd_set_quota.py b/pykolab/cli/cmd_set_quota.py index b6c89c1..3692f10 100644 --- a/pykolab/cli/cmd_set_quota.py +++ b/pykolab/cli/cmd_set_quota.py @@ -1,68 +1,68 @@ # -*- coding: utf-8 -*- # Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3 or, at your option, any later version # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('set_quota', execute, description=description(), aliases=['sq']) def description(): return """Configure quota for a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) try: quota = conf.cli_args.pop(0) except IndexError: quota = utils.ask_question(_("New quota")) except IndexError: folder = utils.ask_question(_("Folder name")) quota = utils.ask_question(_("New quota")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) sys.exit(1) for _folder in imap.lm(imap.folder_utf7(folder)): imap.set_quota(_folder, quota) print("Quota for folder '%s' set to %d" % (_folder, int(quota)), file=sys.stdout) diff --git a/pykolab/cli/cmd_sync.py b/pykolab/cli/cmd_sync.py index 2aaf9df..85952cb 100644 --- a/pykolab/cli/cmd_sync.py +++ b/pykolab/cli/cmd_sync.py @@ -1,150 +1,150 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands from distutils import version import multiprocessing import sys import time import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() imap = None pool = None def __init__(): commands.register('sync', execute, description="Synchronize Kolab Users with IMAP.") def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--threads', dest = "threads", action = "store", default = 20, type = int, help = _("Synchronize LDAP and IMAP") ) my_option_group.add_option( '--resync', dest = "resync", action = "store_true", default = False, help = _("Resync from the beginning") ) my_option_group.add_option( '--domain', dest = "domain", action = "store", default = "all", help = _("Only sync the specified domain") ) def execute(*args, **kw): global imap, pool auth = Auth() if conf.domain == "all": log.debug(_("Listing domains..."), level=5) start_time = time.time() domains = auth.list_domains() end_time = time.time() log.debug( _("Found %d domains in %d seconds") % ( len(domains), (end_time-start_time) ), level=8 ) else: domains = {} domains[conf.domain] = conf.domain if version.StrictVersion(sys.version[:3]) >= version.StrictVersion("2.7"): pool = multiprocessing.Pool(conf.threads, worker_process, (), 1) else: pool = multiprocessing.Pool(conf.threads, worker_process, ()) for primary_domain in list(set(domains.values())): log.debug(_("Running for domain %s") % (primary_domain), level=8) auth = Auth(primary_domain) auth.connect(primary_domain) start_time = time.time() auth.synchronize(mode='_paged_search', callback=queue_add) end_time = time.time() log.info(_("Synchronizing users for %s took %d seconds") % (primary_domain, (end_time-start_time)) ) while not pool._taskqueue.empty(): time.sleep(1) def queue_add(*args, **kw): global pool for dn, entry in kw['entry']: entry['dn'] = dn r = pool.apply_async(_synchronize, (), dict(**entry)) r.wait() def worker_process(*args, **kw): pass def _synchronize(*args, **kw): log.info(_("Worker process %s handling %s") % (multiprocessing.current_process().name, kw['dn'])) entry = utils.normalize(kw) mailbox_attribute = conf.get('cyrus-sasl', 'result_attribute') if mailbox_attribute == None: mailbox_attribute = 'mail' if mailbox_attribute not in entry: return if not 'kolabinetorgperson' in entry['objectclass']: return imap = IMAP() imap.connect() if not imap.user_mailbox_exists(entry[mailbox_attribute]): if 'mailhost' in entry: server = entry['mailhost'] else: server = None imap.user_mailbox_create(entry[mailbox_attribute], server=server) imap.disconnect() diff --git a/pykolab/cli/cmd_sync_mailhost_attrs.py b/pykolab/cli/cmd_sync_mailhost_attrs.py index 4d9663f..a3e200f 100644 --- a/pykolab/cli/cmd_sync_mailhost_attrs.py +++ b/pykolab/cli/cmd_sync_mailhost_attrs.py @@ -1,196 +1,196 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab import imap_utf7 from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('sync_mailhost_attrs', execute, description=description()) def description(): return "Synchronize mailHost attribute values with the actual mailserver in a Cyrus IMAP Murder.\n" def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--delete', dest = "delete", action = "store_true", default = False, help = _("Delete mailboxes for recipients that do not appear to exist in LDAP.")) my_option_group.add_option( '--dry-run', dest = "dry_run", action = "store_true", default = False, help = _("Display changes, do not apply them.")) my_option_group.add_option( '--server', dest = "connect_server", action = "store", default = None, metavar = "SERVER", help = _("List mailboxes on server SERVER only.")) def execute(*args, **kw): """ Synchronize or display changes """ imap = IMAP() if not conf.connect_server == None: imap.connect(server=conf.connect_server) else: imap.connect() auth = Auth() auth.connect() result_attribute = conf.get('cyrus-sasl', 'result_attribute') if result_attribute is None: result_attribute = 'mail' domains = auth.list_domains() folders = imap.lm() imap_domains_not_domains = [] for folder in folders: if len(folder.split('@')) > 1 and not folder.startswith('DELETED'): _folder_domain = folder.split('@')[-1] if not _folder_domain in list(set(domains.keys() + domains.values())): imap_domains_not_domains.append(folder.split('@')[-1]) imap_domains_not_domains = list(set(imap_domains_not_domains)) log.debug(_("Domains in IMAP not in LDAP: %r") % (imap_domains_not_domains), level=8) if len(imap_domains_not_domains) > 0: for domain in imap_domains_not_domains: folders = [] folders.extend(imap.lm('shared/%%@%s' % (domain))) folders.extend(imap.lm('user/%%@%s' % (domain))) for folder in folders: r_folder = folder if not folder.startswith('shared/'): r_folder = '/'.join(folder.split('/')[1:]) if conf.delete: if conf.dry_run: if not folder.startswith('shared/'): log.warning(_("No recipients for '%s' (would have deleted the mailbox if not for --dry-run)!") % (r_folder)) else: continue else: if not folder.startswith('shared/'): log.info(_("Deleting mailbox '%s' because it has no recipients") % (folder)) try: imap.dm(folder) except Exception as errmsg: log.error(_("An error occurred removing mailbox %r: %r") % (folder, errmsg)) else: log.info(_("Not automatically deleting shared folder '%s'") % (folder)) else: log.warning(_("No recipients for '%s' (use --delete to delete)!") % (r_folder)) for primary in list(set(domains.values())): secondaries = [x for x in domains if domains[x] == primary] folders = [] folders.extend(imap.lm('shared/%%@%s' % (primary))) folders.extend(imap.lm('user/%%@%s' % (primary))) for secondary in secondaries: folders.extend(imap.lm('shared/%%@%s' % (secondary))) folders.extend(imap.lm('user/%%@%s' % (secondary))) auth = Auth(domain=primary) auth.connect() for folder in folders: server = imap.user_mailbox_server(folder) r_folder = folder if folder.startswith('shared/'): recipient = auth.find_folder_resource(folder) else: r_folder = '/'.join(folder.split('/')[1:]) recipient = auth.find_recipient(r_folder, search_attrs=[result_attribute]) if (isinstance(recipient, list)): if len(recipient) > 1: log.warning(_("Multiple recipients for '%s'!") % (r_folder)) continue elif len(recipient) == 0: if conf.delete: if conf.dry_run: if not folder.startswith('shared/'): log.warning(_("No recipients for '%s' (would have deleted the mailbox if not for --dry-run)!") % (r_folder)) else: continue else: if not folder.startswith('shared/'): log.info(_("Deleting mailbox '%s' because it has no recipients") % (folder)) try: imap.dm(folder) except Exception as errmsg: log.error(_("An error occurred removing mailbox %r: %r") % (folder, errmsg)) else: log.info(_("Not automatically deleting shared folder '%s'") % (folder)) else: log.warning(_("No recipients for '%s' (use --delete to delete)!") % (r_folder)) continue else: mailhost = auth.get_entry_attribute(primary, recipient, 'mailhost') if not server == mailhost: if conf.dry_run: print(folder, server, mailhost) else: auth.set_entry_attribute(primary, recipient, 'mailhost', server) folders = [] folders.extend(imap.lm("shared/%%")) folders.extend(imap.lm("user/%%")) auth = Auth() auth.connect() for folder in folders: server = imap.user_mailbox_server(folder) if folder.startswith('shared/'): recipient = auth.find_folder_resource(folder) else: recipient = auth.find_recipient('/'.join(folder.split('/')[1:]), search_attrs=[result_attribute]) print(folder, server, recipient) diff --git a/pykolab/cli/cmd_transfer_mailbox.py b/pykolab/cli/cmd_transfer_mailbox.py index 1593eea..071eda4 100644 --- a/pykolab/cli/cmd_transfer_mailbox.py +++ b/pykolab/cli/cmd_transfer_mailbox.py @@ -1,68 +1,68 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.auth import Auth from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('transfer_mailbox', execute, description="Transfer a mailbox to another server.") def execute(*args, **kw): """ Transfer mailbox """ if len(conf.cli_args) > 1: mailfolder = conf.cli_args.pop(0) target_server = conf.cli_args.pop(0) if len(conf.cli_args) > 0: target_partition = conf.cli_args.pop(0) imap = IMAP() imap.connect() mbox_parts = imap.parse_mailfolder(mailfolder) if mbox_parts['domain'] == None: domain = conf.get('kolab', 'primary_domain') user_identifier = mbox_parts['path_parts'][1] else: domain = mbox_parts['domain'] user_identifier = "%s@%s" % (mbox_parts['path_parts'][1], mbox_parts['domain']) auth = Auth(domain=domain) auth.connect() user = auth.find_recipient(user_identifier) source_server = imap.user_mailbox_server(mailfolder) imap.connect(server=source_server) imap.imap.xfer(mailfolder, target_server) if not user == None and not len(user) < 1: auth.set_entry_attributes(domain, user, {'mailhost': target_server}) diff --git a/pykolab/cli/cmd_undelete_mailbox.py b/pykolab/cli/cmd_undelete_mailbox.py index 5b74a64..35b7a2f 100644 --- a/pykolab/cli/cmd_undelete_mailbox.py +++ b/pykolab/cli/cmd_undelete_mailbox.py @@ -1,58 +1,58 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import commands +from . import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('undelete_mailbox', execute, description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--dry-run', dest = "dry_run", action = "store_true", default = False, help = _("Do not actually execute, but state what would have been executed.")) def description(*args, **kw): return _("Recover mailboxes previously deleted.") def execute(*args, **kw): """ Undelete mailbox """ target_folder = None undelete_folder = conf.cli_args.pop(0) if len(conf.cli_args) > 0: target_folder = conf.cli_args.pop(0) imap = IMAP() imap.connect() imap.undelete_mailfolder(undelete_folder, target_folder) diff --git a/pykolab/cli/cmd_user_info.py b/pykolab/cli/cmd_user_info.py index beaeb40..eb7dfcb 100644 --- a/pykolab/cli/cmd_user_info.py +++ b/pykolab/cli/cmd_user_info.py @@ -1,62 +1,62 @@ # -*- coding: utf-8 -*- # Copyright 2010-2012 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys -import commands +from . import commands import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('user_info', execute, description="Display user information.") def execute(*args, **kw): from pykolab import wap_client try: user = conf.cli_args.pop(0) except IndexError: user = utils.ask_question(_("Email address")) result = wap_client.authenticate(username=conf.get("ldap", "bind_dn"), password=conf.get("ldap", "bind_pw")) if len(user.split('@')) > 1: wap_client.system_select_domain(user.split('@')[1]) user_info = wap_client.user_find({'mail':user}) if user_info == None or not user_info: print(_("No such user %s") % (user), file=sys.stderr) sys.exit(0) unic_attrs = ['displayname', 'givenname', 'cn', 'sn', 'ou', 'entrydn'] for (k,v) in user_info.items(): if k in unic_attrs: print("%s: %s" % (k,v)) else: print("%s: %r" % (k,v)) diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py index e5ecfa8..171e4e5 100644 --- a/pykolab/imap/__init__.py +++ b/pykolab/imap/__init__.py @@ -1,1261 +1,1261 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from six import string_types import logging import re import time import socket import sys try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.imap') conf = pykolab.getConf() class IMAP(object): def __init__(self): # Pool of named IMAP connections, by hostname self._imap = {} # Place holder for the current IMAP connection self.imap = None def cleanup_acls(self, aci_subject): log.info( _("Cleaning up ACL entries for %s across all folders") % ( aci_subject ) ) lm_suffix = "" if len(aci_subject.split('@')) > 1: lm_suffix = "@%s" % (aci_subject.split('@')[1]) shared_folders = self.imap.lm("shared/*%s" % (lm_suffix)) user_folders = self.imap.lm("user/*%s" % (lm_suffix)) # For all folders (shared and user), ... folders = user_folders + shared_folders log.debug(_("Iterating over %d folders") % (len(folders)), level=5) # ... loop through them and ... for folder in folders: try: # ... list the ACL entries acls = self.imap.lam(folder) # For each ACL entry, see if we think it is a current, valid # entry for acl_entry in acls: # If the key 'acl_entry' does not exist in the dictionary # of valid ACL entries, this ACL entry has got to go. if acl_entry == aci_subject: # Set the ACL to '' (effectively deleting the ACL # entry) log.debug( _( "Removing acl %r for subject %r from folder %r" ) % ( acls[acl_entry], acl_entry, folder ), level=8 ) self.set_acl(folder, acl_entry, '') except Exception as errmsg: log.error( _("Failed to read/set ACL on folder %s: %r") % ( folder, errmsg ) ) def connect(self, uri=None, server=None, domain=None, login=True): """ Connect to the appropriate IMAP backend. Supply a domain (name space) configured in the configuration file as a section, with a setting 'imap_uri' to connect to a domain specific IMAP server, or specify an URI to connect to that particular IMAP server (in that order). Routines sitting behind this will take into account Cyrus IMAP Murder capabilities, brokering actions to take place against the correct server (such as a 'xfer' which needs to happen against the source backend). """ # TODO: We are currently compatible with one IMAP backend technology per # deployment. backend = conf.get('kolab', 'imap_backend') if domain is not None: self.domain = domain if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'): backend = conf.get(domain, 'imap_backend') if uri is None: if conf.has_section(domain) and conf.has_option(domain, 'imap_uri'): uri = conf.get(domain, 'imap_uri') else: self.domain = None scheme = None hostname = None port = None if uri is None: uri = conf.get(backend, 'uri') result = urlparse(uri) if hasattr(result, 'netloc'): scheme = result.scheme if len(result.netloc.split(':')) > 1: hostname = result.netloc.split(':')[0] port = result.netloc.split(':')[1] else: hostname = result.netloc elif hasattr(result, 'hostname'): hostname = result.hostname else: scheme = uri.split(':')[0] (hostname, port) = uri.split('/')[2].split(':') if server is not None: hostname = server if scheme is None or scheme == "": scheme = 'imaps' if port is None: if scheme == "imaps": port = 993 elif scheme == "imap": port = 143 else: port = 993 uri = '%s://%s:%s' % (scheme, hostname, port) # Get the credentials admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') if admin_password is None or admin_password == '': log.error(_("No administrator password is available.")) if hostname not in self._imap: if backend == 'cyrus-imap': - import cyrus + from . import cyrus self._imap[hostname] = cyrus.Cyrus(uri) # Actually connect if login: log.debug(_("Logging on to Cyrus IMAP server %s") % (hostname), level=8) self._imap[hostname].login(admin_login, admin_password) self._imap[hostname].logged_in = True elif backend == 'dovecot': - import dovecot + from . import dovecot self._imap[hostname] = dovecot.Dovecot(uri) # Actually connect if login: log.debug(_("Logging on to Dovecot IMAP server %s") % (hostname), level=8) self._imap[hostname].login(admin_login, admin_password) self._imap[hostname].logged_in = True else: import imaplib self._imap[hostname] = imaplib.IMAP4(hostname, port) # Actually connect if login: log.debug(_("Logging on to generic IMAP server %s") % (hostname), level=8) self._imap[hostname].login(admin_login, admin_password) self._imap[hostname].logged_in = True else: if not login: self.disconnect(hostname) self.connect(uri=uri, login=False) elif login and not hasattr(self._imap[hostname], 'logged_in'): self.disconnect(hostname) self.connect(uri=uri) else: try: if hasattr(self._imap[hostname], 'm'): self._imap[hostname].m.noop() elif hasattr(self._imap[hostname], 'noop') \ and callable(self._imap[hostname].noop): self._imap[hostname].noop() log.debug( _("Reusing existing IMAP server connection to %s") % (hostname), level=8 ) except Exception: log.debug(_("Reconnecting to IMAP server %s") % (hostname), level=8) self.disconnect(hostname) self.connect() # Set the newly created technology specific IMAP library as the current # IMAP connection to be used. self.imap = self._imap[hostname] if hasattr(self.imap, 'm') and hasattr(self.imap.m, 'sock'): self._set_socket_keepalive(self.imap.m.sock) elif hasattr(self.imap, 'sock'): self._set_socket_keepalive(self.imap.sock) def disconnect(self, server=None): if server is None: # No server specified, but make sure self.imap is None anyways if hasattr(self, 'imap'): del self.imap # Empty out self._imap as well for key in list(self._imap): del self._imap[key] else: if server in self._imap: del self._imap[server] else: log.warning( _("Called imap.disconnect() on a server that we had no connection to.") ) def create_folder(self, folder_path, server=None, partition=None): folder_path = self.folder_utf7(folder_path) if server is not None: self.connect(server=server) try: self._imap[server].cm(folder_path, partition=partition) return True except Exception as excpt: log.error( _("Could not create folder %r on server %r: %r") % (folder_path, server, excpt) ) else: try: self.imap.cm(folder_path, partition=partition) return True except Exception as excpt: log.error(_("Could not create folder %r: %r") % (folder_path, excpt)) return False def __getattr__(self, name): if hasattr(self.imap, name): return getattr(self.imap, name) if hasattr(self.imap, 'm'): if hasattr(self.imap.m, name): return getattr(self.imap.m, name) raise AttributeError(_("%r has no attribute %s") % (self, name)) def append(self, folder, message): return self.imap.m.append(self.folder_utf7(folder), None, None, message) def folder_utf7(self, folder): from pykolab import imap_utf7 return imap_utf7.encode(folder) def folder_utf8(self, folder): from pykolab import imap_utf7 return imap_utf7.decode(folder) def folder_quote(self, folder): return u'"' + str(folder).strip('"') + '"' def get_metadata(self, folder): """ Obtain all metadata entries on a folder """ metadata = {} _metadata = self.imap.getannotation(self.folder_utf7(folder), '*') for (k, v) in _metadata.items(): metadata[self.folder_utf8(k)] = v return metadata def get_separator(self): if not hasattr(self, 'imap') or self.imap is None: self.connect() if hasattr(self.imap, 'separator'): return self.imap.separator elif hasattr(self.imap, 'm') and hasattr(self.imap.m, 'separator'): return self.imap.m.separator else: return '/' def imap_murder(self): if hasattr(self.imap, 'murder') and self.imap.murder: return True else: return False def namespaces(self): """ Obtain the namespaces. Returns a tuple of: (str(personal) [, str(other users) [, list(shared)]]) """ _personal = None _other_users = None _shared = None (_response, _namespaces) = self.imap.m.namespace() if len(_namespaces) == 1: _namespaces = _namespaces[0] _namespaces = re.split(r"\)\)\s\(\(", _namespaces) if len(_namespaces) >= 3: _shared = [] _shared.append(' '.join(_namespaces[2].replace('((', '').replace('))', '').split()[:-1]).replace('"', '')) if len(_namespaces) >= 2: _other_users = ' '.join(_namespaces[1].replace('((', '').replace('))', '').split()[:-1]).replace('"', '') if len(_namespaces) >= 1: _personal = _namespaces[0].replace('((', '').replace('))', '').split()[0].replace('"', '') return (_personal, _other_users, _shared) def set_acl(self, folder, identifier, acl): """ Set an ACL entry on a folder. """ _acl = '' short_rights = { 'all': 'lrsedntxakcpiw', 'append': 'wip', 'full': 'lrswipkxtecdn', 'read': 'lrs', 'read-only': 'lrs', 'read-write': 'lrswitedn', 'post': 'p', 'semi-full': 'lrswit', 'write': 'lrswite', } if acl in short_rights: acl = short_rights[acl] else: for char in acl: if char in "-+": continue if not char in short_rights['all']: log.error(_("Invalid access identifier %r for subject %r") % (acl, identifier)) return False # Special treatment for '-' and '+' characters if '+' in acl or '-' in acl: acl_map = { 'set': '', 'subtract': '', 'add': '' } mode = 'set' for char in acl: if char == '-': mode = 'subtract' continue if char == '+': mode = 'add' continue acl_map[mode] += char current_acls = self.imap.lam(self.folder_utf7(folder)) for current_acl in current_acls: if current_acl == identifier: _acl = current_acls[current_acl] break _acl = _acl + acl_map['set'] + acl_map['add'] _acl = [x for x in _acl.split() if x not in acl_map['subtract'].split()] acl = ''.join(list(set(_acl))) try: self.imap.sam(self.folder_utf7(folder), identifier, acl) except Exception as errmsg: log.error( _("Could not set ACL for %s on folder %s: %r") % ( identifier, folder, errmsg ) ) def set_metadata(self, folder, metadata_path, metadata_value, shared=True): """ Set a metadata entry on a folder """ if metadata_path.startswith('/shared/'): shared = True metadata_path = metadata_path.replace('/shared/', '/') elif metadata_path.startswith('/private/'): shared = False metadata_path = metadata_path.replace('/private/', '/') self.imap._setannotation(self.folder_utf7(folder), metadata_path, metadata_value, shared) def shared_folder_create(self, folder_path, server=None): """ Create a shared folder. """ folder_name = "shared%s%s" % (self.get_separator(), folder_path) # Correct folder_path being supplied with "shared/shared/" for example if folder_name.startswith("shared%s" % (self.get_separator()) * 2): folder_name = folder_name[7:] log.info(_("Creating new shared folder %s") % (folder_name)) self.create_folder(folder_name, server) def shared_folder_exists(self, folder_path): """ Check if a shared mailbox exists. """ folder_name = 'shared%s%s' % (self.get_separator(), folder_path) # Correct folder_path being supplied with "shared/shared/" for example if folder_name.startswith("shared%s" % (self.get_separator()) * 2): folder_name = folder_name[7:] return self.has_folder(folder_name) def shared_folder_rename(self, old, new): if not self.has_folder(old): log.error("Shared Folder rename error: Folder %s does not exist" % (old)) return if self.has_folder(new): log.error("Shared Folder rename error: Folder %s already exists" % (new)) return self.imap._rename(old, new) def shared_folder_set_type(self, folder_path, folder_type): folder_name = 'shared%s%s' % (self.get_separator(), folder_path) # Correct folder_path being supplied with "shared/shared/" for example if folder_name.startswith("shared%s" % (self.get_separator()) * 2): folder_name = folder_name[7:] self.set_metadata(folder_name, '/shared/vendor/kolab/folder-type', folder_type) def shared_mailbox_create(self, mailbox_base_name, server=None): """ Create a shared folder. """ self.connect() folder_name = "shared%s%s" % (self.get_separator(), mailbox_base_name) # Correct folder_path being supplied with "shared/shared/" for example if folder_name.startswith("shared%s" % (self.get_separator()) * 2): folder_name = folder_name[7:] log.info(_("Creating new shared folder %s") %(mailbox_base_name)) self.create_folder(folder_name, server) def shared_mailbox_exists(self, mailbox_base_name): """ Check if a shared mailbox exists. """ self.connect() folder_name = "shared%s%s" % (self.get_separator(), mailbox_base_name) # Correct folder_path being supplied with "shared/shared/" for example if folder_name.startswith("shared%s" % (self.get_separator()) * 2): folder_name = folder_name[7:] return self.has_folder(folder_name) def user_mailbox_create(self, mailbox_base_name, server=None): """ Create a user mailbox. Returns the full path to the new mailbox folder. """ # TODO: Whether or not to lowercase the mailbox name is really up to the # IMAP server setting username_tolower (normalize_uid, lmtp_downcase_rcpt). self.connect() if not mailbox_base_name == mailbox_base_name.lower(): log.warning(_("Downcasing mailbox name %r") % (mailbox_base_name)) mailbox_base_name = mailbox_base_name.lower() folder_name = "user%s%s" % (self.get_separator(), mailbox_base_name) log.info(_("Creating new mailbox for user %s") %(mailbox_base_name)) success = self._create_folder_waiting(folder_name, server) if not success: log.error(_("Could not create the mailbox for user %s, aborting." % (mailbox_base_name))) return False _additional_folders = None if not hasattr(self, 'domain'): self.domain = None if self.domain is None and len(mailbox_base_name.split('@')) > 1: self.domain = mailbox_base_name.split('@')[1] if not self.domain is None: if conf.has_option(self.domain, "autocreate_folders"): _additional_folders = conf.get_raw( self.domain, "autocreate_folders" ) else: from pykolab.auth import Auth auth = Auth() auth.connect() domains = auth.list_domains(self.domain) auth.disconnect() if len(domains) > 0: if self.domain in domains: primary = domains[self.domain] if conf.has_option(primary, "autocreate_folders"): _additional_folders = conf.get_raw( primary, "autocreate_folders" ) if _additional_folders is None: if conf.has_option('kolab', "autocreate_folders"): _additional_folders = conf.get_raw( 'kolab', "autocreate_folders" ) additional_folders = conf.plugins.exec_hook( "create_user_folders", kw={ 'folder': folder_name, 'additional_folders': _additional_folders } ) if additional_folders is not None: self.user_mailbox_create_additional_folders( mailbox_base_name, additional_folders ) if not self.domain is None: 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': mailbox_base_name } ) return folder_name def user_mailbox_create_additional_folders(self, user, additional_folders): log.debug( _("Creating additional folders for user %s") % (user), level=8 ) backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') folder = 'user%s%s' % (self.get_separator(), user) if self.imap_murder(): server = self.user_mailbox_server(folder) else: server = None success = False last_log = time.time() while not success: try: self.disconnect() self.connect(login=False, server=server) self.login_plain(admin_login, admin_password, user) (personal, other, shared) = self.namespaces() success = True except Exception as errmsg: if time.time() - last_log > 5 and self.imap_murder(): log.debug(_("Waiting for the Cyrus murder to settle... %r") % (errmsg)) last_log = time.time() if conf.debuglevel > 8: import traceback traceback.print_exc() time.sleep(0.5) for additional_folder in additional_folders: _add_folder = {} folder_name = additional_folder if not folder_name.startswith(personal): log.error(_("Correcting additional folder name from %r to %r") % (folder_name, "%s%s" % (personal, folder_name))) folder_name = "%s%s" % (personal, folder_name) success = self._create_folder_waiting(folder_name) if not success: log.warning(_("Failed to create folder: %s") % (folder_name)) continue if "annotations" in additional_folders[additional_folder]: for annotation in additional_folders[additional_folder]["annotations"]: self.set_metadata( folder_name, "%s" % (annotation), "%s" % (additional_folders[additional_folder]["annotations"][annotation]) ) if "acls" in additional_folders[additional_folder]: for acl in additional_folders[additional_folder]["acls"]: self.set_acl( folder_name, "%s" % (acl), "%s" % (additional_folders[additional_folder]["acls"][acl]) ) if len(user.split('@')) > 1: localpart = user.split('@')[0] domain = user.split('@')[1] domain_suffix = "@%s" % (domain) else: localpart = user domain = None domain_suffix = "" if domain is not None: if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'): backend = conf.get(domain, 'imap_backend') if conf.has_section(domain) and conf.has_option(domain, 'imap_uri'): uri = conf.get(domain, 'imap_uri') else: uri = None log.debug(_("Subscribing user to the additional folders"), level=8) _tests = [] # Subscribe only to personal folders (personal, other, shared) = self.namespaces() if other is not None: _tests.append(other) if shared is not None: for _shared in shared: _tests.append(_shared) log.debug(_("Using the following tests for folder subscriptions:"), level=8) for _test in _tests: log.debug(_(" %r") % (_test), level=8) for _folder in self.lm(): log.debug(_("Folder %s") % (_folder), level=8) _subscribe = True for _test in _tests: if not _subscribe: continue if _folder.startswith(_test): _subscribe = False if _subscribe: log.debug(_("Subscribing %s to folder %s") % (user, _folder), level=8) try: self.subscribe(_folder) except Exception as errmsg: log.error(_("Subscribing %s to folder %s failed: %r") % (user, _folder, errmsg)) self.logout() self.connect(domain=self.domain) for additional_folder in additional_folders: if additional_folder.startswith(personal) and not personal == '': folder_name = additional_folder.replace(personal, '') else: folder_name = additional_folder folder_name = "user%s%s%s%s%s" % ( self.get_separator(), localpart, self.get_separator(), folder_name, domain_suffix ) if "quota" in additional_folders[additional_folder]: try: self.imap.sq( folder_name, additional_folders[additional_folder]['quota'] ) except Exception as errmsg: log.error(_("Could not set quota on %s") % (additional_folder)) if "partition" in additional_folders[additional_folder]: partition = additional_folders[additional_folder]["partition"] try: self.imap._rename(folder_name, folder_name, partition) except: log.error(_("Could not rename %s to reside on partition %s") % (folder_name, partition)) def _create_folder_waiting(self, folder_name, server=None): """ Create a folder and wait to make sure it exists """ created = False try: max_tries = 10 while not created and max_tries > 0: created = self.create_folder(folder_name, server) if not created: self.disconnect() max_tries -= 1 time.sleep(1) self.connect() # In a Cyrus IMAP Murder topology, wait for the murder to have settled if created and self.imap_murder(): success = False last_log = time.time() reconnect_counter = 0 while not success: success = self.has_folder(folder_name) if not success: if time.time() - last_log > 5: reconnect_counter += 1 log.info(_("Waiting for the Cyrus IMAP Murder to settle...")) if reconnect_counter == 6: log.warning(_("Waited for 15 seconds, going to reconnect")) reconnect_counter = 0 self.disconnect() self.connect() last_log = time.time() time.sleep(0.5) except: if conf.debuglevel > 8: import traceback traceback.print_exc() return created def user_mailbox_delete(self, mailbox_base_name): """ Delete a user mailbox. """ self.connect() folder = "user%s%s" %(self.get_separator(), mailbox_base_name) self.delete_mailfolder(folder) self.cleanup_acls(mailbox_base_name) def user_mailbox_exists(self, mailbox_base_name): """ Check if a user mailbox exists. """ self.connect() if not mailbox_base_name == mailbox_base_name.lower(): log.warning(_("Downcasing mailbox name %r") % (mailbox_base_name)) mailbox_base_name = mailbox_base_name.lower() return self.has_folder('user%s%s' %(self.get_separator(), mailbox_base_name)) def user_mailbox_quota(self, mailbox_quota): pass def user_mailbox_rename(self, old_name, new_name, partition=None): self.connect() old_name = "user%s%s" % (self.get_separator(), old_name) new_name = "user%s%s" % (self.get_separator(), new_name) if old_name == new_name and partition is None: return if not self.has_folder(old_name): log.error(_("INBOX folder to rename (%s) does not exist") % (old_name)) if not self.has_folder(new_name) or not partition is None: log.info(_("Renaming INBOX from %s to %s") % (old_name, new_name)) try: self.imap.rename(old_name, new_name, partition) except: log.error(_("Could not rename INBOX folder %s to %s") % (old_name, new_name)) else: log.warning(_("Moving INBOX folder %s won't succeed as target folder %s already exists") % (old_name, new_name)) def user_mailbox_server(self, mailbox): server = self.imap.find_mailfolder_server(mailbox.lower()).lower() log.debug(_("Server for mailbox %r is %r") % (mailbox, server), level=8) return server def has_folder(self, folder): """ Check if the environment has a folder named folder. """ folders = self.imap.lm(self.folder_utf7(folder)) log.debug(_("Looking for folder '%s', we found folders: %r") % (folder, [self.folder_utf8(x) for x in folders]), level=8) # Greater then one, this folder may have subfolders. if len(folders) > 0: return True else: return False def _set_socket_keepalive(self, sock): sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) with open('/proc/sys/net/ipv4/tcp_keepalive_time', 'r') as f: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, (int)(f.read())) with open('/proc/sys/net/ipv4/tcp_keepalive_intvl', 'r') as f: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, (int)(f.read())) with open('/proc/sys/net/ipv4/tcp_keepalive_probes', 'r') as f: sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, (int)(f.read())) def _set_kolab_mailfolder_acls(self, acls, folder=None, update=False): # special case, folder has no ACLs assigned and update was requested, # remove all existing ACL entries if update is True and isinstance(acls, list) and len(acls) == 0: acls = self.list_acls(folder) for subject in acls: log.debug( _("Removing ACL rights %s for subject %s on folder " + \ "%s") % (acls[subject], subject, folder), level=8) self.set_acl(folder, subject, '') return if isinstance(acls, string_types): acls = [ acls ] old_acls = None for acl in acls: exec("acl = %s" % (acl)) subject = acl[0] rights = acl[1] if len(acl) == 3: epoch = acl[2] else: epoch = (int)(time.time()) + 3600 # update mode, check existing entries if update is True: if old_acls is None: old_acls = self.list_acls(folder) for old_subject in old_acls: old_acls[old_subject] = old_acls[old_subject] if subject in old_acls: old_acls[subject] = None if epoch > (int)(time.time()): log.debug( _("Setting ACL rights %s for subject %s on folder " + \ "%s") % (rights, subject, folder), level=8) self.set_acl( folder, "%s" % (subject), "%s" % (rights) ) else: log.debug( _("Removing ACL rights %s for subject %s on folder " + \ "%s") % (rights, subject, folder), level=8) self.set_acl( folder, "%s" % (subject), "" ) # update mode, unset removed ACL entries if old_acls is not None: for subject in old_acls: if old_acls[subject] is not None: log.debug( _("Removing ACL rights %s for subject %s on folder " + \ "%s") % (old_acls[subject], subject, folder), level=8) self.set_acl(folder, subject, '') pass """ Blah functions """ def move_user_folders(self, users=[], domain=None): for user in users: if type(user) == dict: if 'old_mail' in user: inbox = "user/%s" % (user['mail']) old_inbox = "user/%s" % (user['old_mail']) if self.has_folder(old_inbox): log.debug(_("Found old INBOX folder %s") % (old_inbox), level=8) if not self.has_folder(inbox): log.info(_("Renaming INBOX from %s to %s") % (old_inbox, inbox)) self.imap.rename(old_inbox, inbox) self.inbox_folders.append(inbox) else: log.warning(_("Moving INBOX folder %s won't succeed as target folder %s already exists") % (old_inbox, inbox)) else: log.debug(_("Did not find old folder user/%s to rename") % (user['old_mail']), level=8) else: log.debug(_("Value for user is not a dictionary"), level=8) def set_quota(self, folder, quota): i = 0 while i < 10: try: self.imap._setquota(folder, quota) i = 10 except: self.disconnect() self.connect() i += 1 def set_user_folder_quota(self, users=[], primary_domain=None, secondary_domain=[], folders=[]): """ Sets the quota in IMAP using the authentication and authorization database 'quota' attribute for the users listed in parameter 'users' """ if conf.has_option(primary_domain, 'quota_attribute'): _quota_attr = conf.get(primary_domain, 'quota_attribute') else: auth_mechanism = conf.get('kolab', 'auth_mechanism') _quota_attr = conf.get(auth_mechanism, 'quota_attribute') _inbox_folder_attr = conf.get('cyrus-sasl', 'result_attribute') default_quota = auth.domain_default_quota(primary_domain) if default_quota == "" or default_quota is None: default_quota = 0 if len(users) == 0: users = auth.list_users(primary_domain) for user in users: quota = None if type(user) == dict: if _quota_attr in user: if type(user[_quota_attr]) == list: quota = user[_quota_attr].pop(0) elif type(user[_quota_attr]) == str: quota = user[_quota_attr] else: _quota = auth.get_user_attribute(primary_domain, user, _quota_attr) if _quota is None: quota = 0 else: quota = _quota if _inbox_folder_attr not in user: continue else: if type(user[_inbox_folder_attr]) == list: folder = "user/%s" % user[_inbox_folder_attr].pop(0) elif type(user[_inbox_folder_attr]) == str: folder = "user/%s" % user[_inbox_folder_attr] elif type(user) == str: quota = auth.get_user_attribute(user, _quota_attr) folder = "user/%s" % (user) folder = folder.lower() try: (used, current_quota) = self.imap.lq(folder) except: # TODO: Go in fact correct the quota. log.warning(_("Cannot get current IMAP quota for folder %s") % (folder)) used = 0 current_quota = 0 new_quota = conf.plugins.exec_hook("set_user_folder_quota", kw={ 'used': used, 'current_quota': current_quota, 'new_quota': (int)(quota), 'default_quota': (int)(default_quota), 'user': user } ) log.debug(_("Quota for %s currently is %s") % (folder, current_quota), level=7) if new_quota is None: continue if not int(new_quota) == int(quota): log.info(_("Adjusting authentication database quota for folder %s to %d") % (folder, int(new_quota))) quota = int(new_quota) auth.set_user_attribute(primary_domain, user, _quota_attr, new_quota) if not int(current_quota) == int(quota): log.info(_("Correcting quota for %s to %s (currently %s)") % (folder, quota, current_quota)) self.imap._setquota(folder, quota) def set_user_mailhost(self, users=[], primary_domain=None, secondary_domain=[], folders=[]): if conf.has_option(primary_domain, 'mailserver_attribute'): _mailserver_attr = conf.get(primary_domain, 'mailserver_attribute') else: auth_mechanism = conf.get('kolab', 'auth_mechanism') _mailserver_attr = conf.get(auth_mechanism, 'mailserver_attribute') _inbox_folder_attr = conf.get('cyrus-sasl', 'result_attribute') if len(users) == 0: users = auth.list_users(primary_domain) for user in users: mailhost = None if type(user) == dict: if _mailserver_attr in user: if type(user[_mailserver_attr]) == list: _mailserver = user[_mailserver_attr].pop(0) elif type(user[_mailserver_attr]) == str: _mailserver = user[_mailserver_attr] else: _mailserver = auth.get_user_attribute(primary_domain, user, _mailserver_attr) if _inbox_folder_attr not in user: continue else: if type(user[_inbox_folder_attr]) == list: folder = "user/%s" % user[_inbox_folder_attr].pop(0) elif type(user[_inbox_folder_attr]) == str: folder = "user/%s" % user[_inbox_folder_attr] elif type(user) == str: _mailserver = auth.get_user_attribute(user, _mailserver_attr) folder = "user/%s" % (user) folder = folder.lower() _current_mailserver = self.imap.find_mailfolder_server(folder) if _mailserver is not None: # TODO: if not _current_mailserver == _mailserver: self.imap._xfer(folder, _current_mailserver, _mailserver) else: auth.set_user_attribute(primary_domain, user, _mailserver_attr, _current_mailserver) def parse_mailfolder(self, mailfolder): return self.imap.parse_mailfolder(mailfolder) def expunge_user_folders(self, inbox_folders=None): """ Delete folders that have no equivalent user qualifier in the list of users passed to this function, ... TODO: Explain the domain parameter, and actually get it to work properly. This also relates to threading for multi-domain deployments. Parameters: users A list of users. Can be a list of user qualifiers, e.g. [ 'user1', 'user2' ] or a list of user attribute dictionaries, e.g. [ { 'user1': { 'attr': 'value' } } ] primary_domain, secondary_domains """ if inbox_folders is None: inbox_folders = [] folders = self.list_user_folders() for folder in folders: log.debug(_("Checking folder: %s") % (folder), level=1) try: if inbox_folders.index(folder) > -1: continue else: log.info(_("Folder has no corresponding user (1): %s") % (folder)) self.delete_mailfolder("user/%s" % (folder)) except: log.info(_("Folder has no corresponding user (2): %s") % (folder)) try: self.delete_mailfolder("user/%s" % (folder)) except: pass def delete_mailfolder(self, mailfolder_path): """ Deletes a mail folder described by mailfolder_path. """ mbox_parts = self.parse_mailfolder(mailfolder_path) if mbox_parts is None: # We got user identifier only log.error(_("Please don't give us just a user identifier")) return log.info(_("Deleting folder %s") % (mailfolder_path)) self.imap.dm(self.folder_utf7(mailfolder_path)) def get_quota(self, mailfolder_path): try: return self.lq(self.folder_utf7(mailfolder_path)) except: return def get_quota_root(self, mailfolder_path): return self.lqr(self.folder_utf7(mailfolder_path)) def list_acls(self, folder): """ List the ACL entries on a folder """ return self.imap.lam(self.folder_utf7(folder)) def list_folders(self, pattern): return [self.folder_utf8(x) for x in self.lm(self.folder_utf7(pattern))] def list_user_folders(self, primary_domain=None, secondary_domains=[]): """ List the INBOX folders in the IMAP backend. Returns a list of unique base folder names. """ _folders = self.imap.lm("user/%") # TODO: Replace the .* below with a regex representing acceptable DNS # domain names. domain_re = ".*\.?%s$" acceptable_domain_name_res = [] if primary_domain is not None: for domain in [ primary_domain ] + secondary_domains: acceptable_domain_name_res.append(domain_re % (domain)) folders = [] for folder in _folders: folder_name = None if len(folder.split('@')) > 1: # TODO: acceptable domain name spaces #acceptable = False #for domain_name_re in acceptable_domain_name_res: #prog = re.compile(domain_name_re) #if prog.match(folder.split('@')[1]): #print "Acceptable indeed" #acceptable = True #if not acceptable: #print "%s is not acceptable against %s yet using %s" % (folder.split('@')[1], folder, domain_name_re) #if acceptable: #folder_name = "%s@%s" % (folder.split(self.separator)[1].split('@')[0], folder.split('@')[1]) folder_name = "%s@%s" % (folder.split(self.get_separator())[1].split('@')[0], folder.split('@')[1]) else: folder_name = "%s" % (folder.split(self.get_separator())[1]) if folder_name is not None: if not folder_name in folders: folders.append(folder_name) return folders def lm(self, *args, **kw): return self.imap.lm(*args, **kw) def lq(self, *args, **kw): return self.imap.lq(*args, **kw) def lqr(self, *args, **kw): try: return self.imap.lqr(*args, **kw) except: return (None, None, None) def undelete_mailfolder(self, *args, **kw): self.imap.undelete_mailfolder(*args, **kw) diff --git a/pykolab/setup/__init__.py b/pykolab/setup/__init__.py index 26638b0..f23d358 100644 --- a/pykolab/setup/__init__.py +++ b/pykolab/setup/__init__.py @@ -1,78 +1,78 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import sys import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() to_execute = [] class Setup(object): def __init__(self): - import components + from . import components components.__init__() arg_num = 0 for arg in sys.argv[1:]: arg_num += 1 if not arg.startswith('-') and len(sys.argv) >= arg_num: if sys.argv[arg_num].replace('-','_') in components.components: to_execute.append(sys.argv[arg_num].replace('-','_')) def run(self): if os.path.isfile('/sys/fs/selinux/enforce'): if os.access('/sys/fs/selinux/enforce', os.R_OK): # Set a gentle default because strictly speaking, # setup won't fail (run-time does) enforce = "0" with open('/sys/fs/selinux/enforce', 'r') as f: enforce = f.read() if enforce.strip() == "1": log.fatal( _("SELinux currently enforcing. Read " + \ "https://git.kolab.org/u/1") ) sys.exit(1) if os.path.isfile('/etc/selinux/config'): if os.access('/etc/selinux/config', os.R_OK): with open('/etc/selinux/config', 'r') as f: for line in f: if line.strip() == "SELINUX=enforcing": log.fatal( _("SELinux configured to enforce a " + \ "policy on startup. Read " + \ "https://git.kolab.org/u/1") ) sys.exit(1) components.execute('_'.join(to_execute)) if os.path.exists('/tmp/kolab-setup-my.cnf'): os.unlink('/tmp/kolab-setup-my.cnf') diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py index 09203a6..f492f73 100644 --- a/pykolab/setup/setup_freebusy.py +++ b/pykolab/setup/setup_freebusy.py @@ -1,197 +1,197 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # try: from ConfigParser import RawConfigParser except ImportError: from configparser import RawConfigParser import os import sys import time import socket try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register( 'freebusy', execute, description=description(), after=['ldap'] ) def description(): return _("Setup Free/Busy.") def execute(*args, **kw): if not os.path.isfile('/etc/kolab-freebusy/config.ini') and not os.path.isfile('/etc/kolab-freebusy/config.ini.sample'): log.error(_("Free/Busy is not installed on this system")) return if not os.path.isfile('/etc/kolab-freebusy/config.ini'): os.rename('/etc/kolab-freebusy/config.ini.sample', '/etc/kolab-freebusy/config.ini') imap_backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(imap_backend, 'admin_login') admin_password = conf.get(imap_backend, 'admin_password') imap_uri = conf.get(imap_backend, 'imap_uri') if imap_uri == None: imap_uri = conf.get(imap_backend, 'uri') scheme = None hostname = None port = None result = urlparse(imap_uri) if hasattr(result, 'hostname'): hostname = result.hostname if hasattr(result, 'port'): port = result.port if hasattr(result, 'scheme'): scheme = result.scheme else: scheme = imap_uri.split(':')[0] (hostname, port) = imap_uri.split('/')[2].split(':') if scheme == 'imaps' and (port == None or port == ''): port = 993 if scheme == None or scheme == '': scheme = 'imap' if port == None or port == '': port = 143 resources_imap_uri = '%s://%s:%s@%s:%s/%%kolabtargetfolder?acl=lrs' % (scheme, admin_login, admin_password, hostname, port) users_imap_uri = '%s://%%s:%s@%s:%s/?proxy_auth=%s' % (scheme, admin_password, hostname, port, admin_login) freebusy_settings = { 'httpauth': { 'type': 'ldap', 'host': conf.get('ldap', 'ldap_uri'), 'base_dn': conf.get('ldap', 'base_dn'), 'bind_dn': conf.get('ldap', 'service_bind_dn'), 'bind_pw': conf.get('ldap', 'service_bind_pw'), 'filter': '(&(objectClass=kolabInetOrgPerson)(|(mail=%s)(alias=%s)(uid=%s)))', }, 'trustednetworks': { 'allow': ','.join(get_local_ips()) }, 'directory "local"': { 'type': 'static', 'fbsource': 'file:/var/lib/kolab-freebusy/%s.ifb', }, 'directory "local-cache"': { 'type': 'static', 'fbsource': 'file:/var/cache/kolab-freebusy/%s.ifb', 'expires': '15m' }, 'directory "kolab-people"': { 'type': 'ldap', 'host': conf.get('ldap', 'ldap_uri'), 'base_dn': conf.get('ldap', 'base_dn'), 'bind_dn': conf.get('ldap', 'service_bind_dn'), 'bind_pw': conf.get('ldap', 'service_bind_pw'), 'filter': '(&(objectClass=kolabInetOrgPerson)(|(mail=%s)(alias=%s)))', 'attributes': 'mail', 'lc_attributes': 'mail', 'primary_domain': conf.get('kolab', 'primary_domain'), 'fbsource': users_imap_uri, 'cacheto': '/var/cache/kolab-freebusy/%s.ifb', 'expires': '15m', 'loglevel': 300, }, 'directory "kolab-resources"': { 'type': 'ldap', 'host': conf.get('ldap', 'ldap_uri'), 'base_dn': conf.get('ldap', 'resource_base_dn'), 'bind_dn': conf.get('ldap', 'service_bind_dn'), 'bind_pw': conf.get('ldap', 'service_bind_pw'), 'attributes': 'mail, kolabtargetfolder', 'filter': '(&(objectClass=kolabsharedfolder)(kolabfoldertype=event)(mail=%s))', 'primary_domain': conf.get('kolab', 'primary_domain'), 'fbsource': resources_imap_uri, 'cacheto': '/var/cache/kolab-freebusy/%s.ifb', 'expires': '15m', 'loglevel': 300, }, 'directory "kolab-resource-collections"': { 'type': 'ldap', 'host': conf.get('ldap', 'ldap_uri'), 'base_dn': conf.get('ldap', 'resource_base_dn'), 'bind_dn': conf.get('ldap', 'service_bind_dn'), 'bind_pw': conf.get('ldap', 'service_bind_pw'), 'filter': '(&(objectClass=kolabgroupofuniquenames)(mail=%s))', 'attributes': 'uniquemember', 'mail' 'resolve_dn': 'uniquemember', 'resolve_attribute': 'mail', 'primary_domain': conf.get('kolab', 'primary_domain'), 'fbsource': 'aggregate://%uniquemember', 'directories': 'kolab-resources', 'cacheto': '/var/cache/kolab-freebusy/%mail.ifb', 'expires': '15m', 'loglevel': 300, }, } cfg_parser = RawConfigParser() cfg_parser.read('/etc/kolab-freebusy/config.ini') for section in freebusy_settings: if len(freebusy_settings[section]) < 1: cfg_parser.remove_section(section) continue for key in freebusy_settings[section]: if not cfg_parser.has_section(section): cfg_parser.add_section(section) cfg_parser.set(section, key, freebusy_settings[section][key]) fp = open('/etc/kolab-freebusy/config.ini', "w+") cfg_parser.write(fp) fp.close() def get_local_ips(): ips = ['::1','127.0.0.1'] for family in [socket.AF_INET, socket.AF_INET6]: try: for ip in socket.getaddrinfo(socket.getfqdn(), None, family): if ip[4][0] not in ips: ips.append(ip[4][0]) except: pass return ips diff --git a/pykolab/setup/setup_guam.py b/pykolab/setup/setup_guam.py index 6a23527..16b407f 100644 --- a/pykolab/setup/setup_guam.py +++ b/pykolab/setup/setup_guam.py @@ -1,64 +1,64 @@ # -*- coding: utf-8 -*- # Copyright 2010-2016 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import shutil import subprocess import sys import time -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('guam', execute, description=description()) def description(): return _("Setup Guam.") def execute(*args, **kw): if not os.path.isfile('/etc/guam/sys.config'): log.error(_("Guam is not installed on this system")) return if os.path.isfile('/etc/kolab/templates/guam.sys.config.tpl'): template = '/etc/kolab/templates/guam.sys.config.tpl' else: template = '/usr/share/kolab/templates/guam.sys.config.tpl' shutil.copyfile(template, '/etc/guam/sys.config') if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'restart', 'guam']) else: log.error(_("Could not start the guam service.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'enable', 'guam']) else: log.error(_("Could not configure the guam service to start on boot")) diff --git a/pykolab/setup/setup_imap.py b/pykolab/setup/setup_imap.py index c55582e..e145c0f 100644 --- a/pykolab/setup/setup_imap.py +++ b/pykolab/setup/setup_imap.py @@ -1,192 +1,192 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from augeas import Augeas from Cheetah.Template import Template import os import subprocess -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register( 'imap', execute, description=description(), after=['ldap'] ) def description(): return _("Setup IMAP.") def execute(*args, **kw): """ Apply the necessary settings to /etc/imapd.conf """ configdirectory = "/var/lib/imap/" partition_default = "/var/spool/imap/" sievedir = "/var/lib/imap/sieve/" if os.path.isdir("/var/lib/cyrus/"): configdirectory = "/var/lib/cyrus/" sievedir = "/var/lib/cyrus/sieve/" if os.path.isdir("/var/spool/cyrus/mail/"): partition_default = "/var/spool/cyrus/mail/" imapd_settings = { "ldap_servers": conf.get('ldap', 'ldap_uri'), "ldap_base": conf.get('ldap', 'base_dn'), "ldap_bind_dn": conf.get('ldap', 'service_bind_dn'), "ldap_password": conf.get('ldap', 'service_bind_pw'), "ldap_filter": '(|(&(|(uid=%s)(uid=cyrus-murder))(uid=%%U))(&(|(uid=%%U)(mail=%%U@%%d)(mail=%%U@%%r))(objectclass=kolabinetorgperson)))' % (conf.get('cyrus-imap', 'admin_login')), "ldap_user_attribute": conf.get('cyrus-sasl', 'result_attribute'), "ldap_group_base": conf.get('ldap', 'base_dn'), "ldap_group_filter": "(&(cn=%u)(objectclass=ldapsubentry)(objectclass=nsroledefinition))", "ldap_group_scope": "one", "ldap_member_base": conf.get('ldap','user_base_dn'), "ldap_member_method": "attribute", "ldap_member_attribute": "nsrole", "admins": conf.get('cyrus-imap', 'admin_login'), "postuser": "shared", "configdirectory": configdirectory, "partition_default": partition_default, "sievedir": sievedir } template_file = None if os.path.isfile('/etc/kolab/templates/imapd.conf.tpl'): template_file = '/etc/kolab/templates/imapd.conf.tpl' elif os.path.isfile('/usr/share/kolab/templates/imapd.conf.tpl'): template_file = '/usr/share/kolab/templates/imapd.conf.tpl' elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'imapd.conf.tpl'))): template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'imapd.conf.tpl')) if not template_file == None: fp = open(template_file, 'r') template_definition = fp.read() fp.close() t = Template(template_definition, searchList=[imapd_settings]) fp = open('/etc/imapd.conf', 'w') fp.write(t.__str__()) fp.close() else: log.error(_("Could not write out Cyrus IMAP configuration file /etc/imapd.conf")) return cyrus_settings = {} template_file = None if os.path.isfile('/etc/kolab/templates/cyrus.conf.tpl'): template_file = '/etc/kolab/templates/cyrus.conf.tpl' elif os.path.isfile('/usr/share/kolab/templates/cyrus.conf.tpl'): template_file = '/usr/share/kolab/templates/cyrus.conf.tpl' elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'cyrus.conf.tpl'))): template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'cyrus.conf.tpl')) if not template_file == None: fp = open(template_file, 'r') template_definition = fp.read() fp.close() t = Template(template_definition, searchList=[cyrus_settings]) fp = open('/etc/cyrus.conf', 'w') fp.write(t.__str__()) fp.close() else: log.error(_("Could not write out Cyrus IMAP configuration file /etc/cyrus.conf")) return annotations = [ "/vendor/kolab/activesync,mailbox,string,backend,value.priv,r", "/vendor/kolab/color,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/displayname,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/folder-test,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/folder-type,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/incidences-for,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/pxfb-readable-for,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/uniqueid,mailbox,string,backend,value.shared value.priv,a", "/vendor/kolab/h-share-attr-desc,mailbox,string,backend,value.shared value.priv,a", "/vendor/horde/share-params,mailbox,string,backend,value.shared value.priv,a", "/vendor/x-toltec/test,mailbox,string,backend,value.shared value.priv,a", ] fp = open('/etc/imapd.annotations.conf', 'w') fp.write("\n".join(annotations)) fp.close() if os.path.isfile('/etc/default/kolab-saslauthd'): myaugeas = Augeas() setting = os.path.join('/files/etc/default/kolab-saslauthd','START') if not myaugeas.get(setting) == 'yes': myaugeas.set(setting,'yes') myaugeas.save() myaugeas.close() imapservice = 'cyrus-imapd.service' if os.path.isfile('/usr/lib/systemd/system/cyrus.service'): imapservice = 'cyrus.service' if os.path.isfile('/bin/systemctl'): subprocess.call(['systemctl', 'stop', 'saslauthd.service']) subprocess.call(['systemctl', 'restart', 'kolab-saslauthd.service']) subprocess.call(['systemctl', 'restart', imapservice]) elif os.path.isfile('/sbin/service'): subprocess.call(['service', 'saslauthd', 'stop']) subprocess.call(['service', 'kolab-saslauthd', 'restart']) subprocess.call(['service', 'cyrus-imapd', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service','saslauthd','stop']) subprocess.call(['/usr/sbin/service','kolab-saslauthd','restart']) subprocess.call(['/usr/sbin/service','cyrus-imapd','restart']) else: log.error(_("Could not start the cyrus-imapd and kolab-saslauthd services.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['systemctl', 'disable', 'saslauthd.service']) subprocess.call(['systemctl', 'enable', 'kolab-saslauthd.service']) subprocess.call(['systemctl', 'enable', imapservice]) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['chkconfig', 'saslauthd', 'off']) subprocess.call(['chkconfig', 'kolab-saslauthd', 'on']) subprocess.call(['chkconfig', 'cyrus-imapd', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'saslauthd', 'disable']) subprocess.call(['/usr/sbin/update-rc.d', 'kolab-saslauthd', 'defaults']) subprocess.call(['/usr/sbin/update-rc.d', 'cyrus-imapd', 'defaults']) else: log.error(_("Could not configure to start on boot, the " + \ "cyrus-imapd and kolab-saslauthd services.")) diff --git a/pykolab/setup/setup_kolabd.py b/pykolab/setup/setup_kolabd.py index 54892ce..2c4a78e 100644 --- a/pykolab/setup/setup_kolabd.py +++ b/pykolab/setup/setup_kolabd.py @@ -1,97 +1,97 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import subprocess -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ from augeas import Augeas log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register( 'kolabd', execute, description=description(), after=['ldap','imap'] ) def description(): return _("Setup the Kolab daemon.") def execute(*args, **kw): if conf.has_section('example.org'): primary_domain = conf.get('kolab', 'primary_domain') if not primary_domain == 'example.org': utils.multiline_message( _(""" Copying the configuration section for 'example.org' over to a section applicable to your domain '%s'. """) % (primary_domain) ) conf.cfg_parser._sections[primary_domain] = \ conf.cfg_parser._sections['example.org'] conf.cfg_parser._sections.pop('example.org') fp = open(conf.cli_keywords.config_file, "w+") conf.cfg_parser.write(fp) fp.close() if os.path.isfile('/etc/default/kolab-server'): myaugeas = Augeas() setting = os.path.join('/files/etc/default/kolab-server','START') if not myaugeas.get(setting) == 'yes': myaugeas.set(setting,'yes') myaugeas.save() myaugeas.close() if os.path.isfile('/bin/systemctl'): if os.path.isfile('/etc/debian_version'): subprocess.call(['/bin/systemctl', 'restart', 'kolab-server.service']) else: subprocess.call(['/bin/systemctl', 'restart', 'kolabd.service']) elif os.path.isfile('/sbin/service'): subprocess.call(['/sbin/service', 'kolabd', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service','kolab-server','restart']) else: log.error(_("Could not start the kolab server service.")) if os.path.isfile('/bin/systemctl'): if os.path.isfile('/etc/debian_version'): subprocess.call(['/bin/systemctl', 'enable', 'kolab-server.service']) else: subprocess.call(['/bin/systemctl', 'enable', 'kolabd.service']) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['/sbin/chkconfig', 'kolabd', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'kolab-server', 'defaults']) else: log.error(_("Could not configure to start on boot, the " + \ "kolab server service.")) diff --git a/pykolab/setup/setup_ldap.py b/pykolab/setup/setup_ldap.py index fb865e9..9765545 100644 --- a/pykolab/setup/setup_ldap.py +++ b/pykolab/setup/setup_ldap.py @@ -1,690 +1,690 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import ldap import ldap.modlist import os import pwd import shutil import subprocess import tempfile import time -import components +from . import components import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('ldap', execute, description=description()) def cli_options(): ldap_group = conf.add_cli_parser_option_group(_("LDAP Options")) ldap_group.add_option( "--fqdn", dest = "fqdn", action = "store", default = fqdn, help = _("Specify FQDN (overriding defaults).") ) ldap_group.add_option( "--allow-anonymous", dest = "anonymous", action = "store_true", default = False, help = _("Allow anonymous binds (default: no).") ) ldap_group.add_option( "--without-ldap", dest = "without_ldap", action = "store_true", default = False, help = _("Skip setting up the LDAP server.") ) ldap_group.add_option( "--with-openldap", dest = "with_openldap", action = "store_true", default = False, help = _("Setup configuration for OpenLDAP compatibility.") ) ldap_group.add_option( "--with-ad", dest = "with_ad", action = "store_true", default = False, help = _("Setup configuration for Active Directory compatibility.") ) ldap_group.add_option( "--directory-manager-pwd", dest = "directory_manager_pwd", action = "store", default = None, help = _("Specify password for the Domain Manager.") ) def description(): return _("Setup LDAP.") def execute(*args, **kw): ask_questions = True if not conf.config_file == conf.defaults.config_file: ask_questions = False if conf.without_ldap: print(_("Skipping setup of LDAP, as specified"), file=sys.stderr) return _input = {} if conf.with_openldap and not conf.with_ad: conf.command_set('ldap', 'unique_attribute', 'entryuuid') fp = open(conf.defaults.config_file, "w+") conf.cfg_parser.write(fp) fp.close() return elif conf.with_ad and not conf.with_openldap: conf.command_set('ldap', 'auth_attributes', 'samaccountname') conf.command_set('ldap', 'modifytimestamp_format', '%%Y%%m%%d%%H%%M%%S.0Z') conf.command_set('ldap', 'unique_attribute', 'userprincipalname') # TODO: These attributes need to be checked conf.command_set('ldap', 'mail_attributes', 'mail') conf.command_set('ldap', 'mailserver_attributes', 'mailhost') conf.command_set('ldap', 'quota_attribute', 'mailquota') return elif conf.with_ad and conf.with_openldap: print(utils.multiline_message( _(""" You can not configure Kolab to run against OpenLDAP and Active Directory simultaneously. """) ), file=sys.stderr) sys.exit(1) # Pre-execution checks for path, directories, files in os.walk('/etc/dirsrv/'): for direct in directories: if direct.startswith('slapd-'): print(utils.multiline_message( _(""" It seems 389 Directory Server has an existing instance configured. This setup script does not intend to destroy or overwrite your data. Please make sure /etc/dirsrv/ and /var/lib/dirsrv/ are clean so that this setup does not have to worry. """) ), file=sys.stderr) sys.exit(1) _input = {} if ask_questions: print(utils.multiline_message( _(""" Please supply a password for the LDAP administrator user 'admin', used to login to the graphical console of 389 Directory server. """) ), file=sys.stderr) _input['admin_pass'] = utils.ask_question( _("Administrator password"), default=utils.generate_password(), password=True, confirm=True ) if conf.directory_manager_pwd is not None: _input['dirmgr_pass'] = conf.directory_manager_pwd else: print(utils.multiline_message( _(""" Please supply a password for the LDAP Directory Manager user, which is the administrator user you will be using to at least initially log in to the Web Admin, and that Kolab uses to perform administrative tasks. """) ), file=sys.stderr) _input['dirmgr_pass'] = utils.ask_question( _("Directory Manager password"), default=utils.generate_password(), password=True, confirm=True ) print(utils.multiline_message( _(""" Please choose the system user and group the service should use to run under. These should be existing, unprivileged, local system POSIX accounts with no shell. """) ), file=sys.stderr) try: pw = pwd.getpwnam("dirsrv") except: _input['userid'] = utils.ask_question(_("User"), default="nobody") _input['group'] = utils.ask_question(_("Group"), default="nobody") else: _input['userid'] = utils.ask_question(_("User"), default="dirsrv") _input['group'] = utils.ask_question(_("Group"), default="dirsrv") else: _input['admin_pass'] = conf.get('ldap', 'bind_pw') _input['dirmgr_pass'] = conf.get('ldap', 'bind_pw') try: pw = pwd.getpwnam("dirsrv") except: _input['userid'] = "nobody" _input['group'] = "nobody" else: _input['userid'] = "dirsrv" _input['group'] = "dirsrv" # TODO: Verify the user and group exist. # TODO: This takes the system fqdn, domainname and hostname, rather then # the desired fqdn, domainname and hostname. # # TODO^2: This should be confirmed. if conf.fqdn: _input['fqdn'] = conf.fqdn _input['hostname'] = conf.fqdn.split('.')[0] _input['domain'] = '.'.join(conf.fqdn.split('.')[1:]) else: _input['fqdn'] = fqdn _input['hostname'] = hostname.split('.')[0] _input['domain'] = domainname _input['nodotdomain'] = _input['domain'].replace('.','_') _input['rootdn'] = utils.standard_root_dn(_input['domain']) if ask_questions: print(utils.multiline_message( _(""" This setup procedure plans to set up Kolab Groupware for the following domain name space. This domain name is obtained from the reverse DNS entry on your network interface. Please confirm this is the appropriate domain name space. """) ), file=sys.stderr) answer = utils.ask_confirmation("%s" % (_input['domain'])) if not answer: positive_answer = False while not positive_answer: _input['domain'] = utils.ask_question(_("Domain name to use")) if not _input['domain'] == None and not _input['domain'] == "": positive_answer = True else: print(utils.multiline_message( _(""" Invalid input. Please try again. """) ), file=sys.stderr) _input['nodotdomain'] = _input['domain'].replace('.','_') _input['rootdn'] = utils.standard_root_dn(_input['domain']) print(utils.multiline_message( _(""" The standard root dn we composed for you follows. Please confirm this is the root dn you wish to use. """) ), file=sys.stderr) answer = utils.ask_confirmation("%s" % (_input['rootdn'])) if not answer: positive_answer = False while not positive_answer: _input['rootdn'] = utils.ask_question(_("Root DN to use")) if not _input['rootdn'] == None and not _input['rootdn'] == "": positive_answer = True else: print(utils.multiline_message( _(""" Invalid input. Please try again. """) ), file=sys.stderr) # TODO: Loudly complain if the fqdn does not resolve back to this system. data = """ [General] FullMachineName = %(fqdn)s SuiteSpotUserID = %(userid)s SuiteSpotGroup = %(group)s AdminDomain = %(domain)s ConfigDirectoryLdapURL = ldap://%(fqdn)s:389/o=NetscapeRoot ConfigDirectoryAdminID = admin ConfigDirectoryAdminPwd = %(admin_pass)s [slapd] SlapdConfigForMC = Yes UseExistingMC = 0 ServerPort = 389 ServerIdentifier = %(hostname)s Suffix = %(rootdn)s RootDN = cn=Directory Manager RootDNPwd = %(dirmgr_pass)s ds_bename = %(nodotdomain)s AddSampleEntries = No [admin] Port = 9830 ServerAdminID = admin ServerAdminPwd = %(admin_pass)s """ % (_input) (fp, filename) = tempfile.mkstemp(dir="/tmp/") os.write(fp, data) os.close(fp) if os.path.isfile("/usr/sbin/setup-ds-admin.pl"): setup_ds_admin = "/usr/sbin/setup-ds-admin.pl" elif os.path.isfile("/usr/sbin/setup-ds-admin"): setup_ds_admin = "/usr/sbin/setup-ds-admin" elif os.path.isfile("/usr/sbin/setup-ds.pl"): setup_ds_admin = "/usr/sbin/setup-ds.pl" elif os.path.isfile("/usr/sbin/setup-ds"): setup_ds_admin = "/usr/sbin/setup-ds" else: log.error(_("No directory server setup tool available.")) sys.exit(1) command = [ setup_ds_admin, '--debug', '--silent', '--force', '--file=%s' % (filename) ] print(utils.multiline_message( _(""" Setup is now going to set up the 389 Directory Server. This may take a little while (during which period there is no output and no progress indication). """) ), file=sys.stderr) log.info(_("Setting up 389 Directory Server")) setup_389 = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) (stdoutdata, stderrdata) = setup_389.communicate() if not setup_389.returncode == 0: print(utils.multiline_message( _(""" An error was detected in the setup procedure for 389 Directory Server. This setup will write out stderr and stdout to /var/log/kolab/setup.error.log and /var/log/kolab/setup.out.log respectively, before it exits. """) ), file=sys.stderr) fp = open('/var/log/kolab/setup.error.log', 'w') fp.write(stderrdata) fp.close() fp = open('/var/log/kolab/setup.out.log', 'w') fp.write(stdoutdata) fp.close() log.debug(_("Setup DS stdout:"), level=8) log.debug(stdoutdata, level=8) log.debug(_("Setup DS stderr:"), level=8) log.debug(stderrdata, level=8) if not setup_389.returncode == 0: sys.exit(1) # Find the kolab schema. It's installed as %doc in the kolab-schema package. # TODO: Chown nobody, nobody, chmod 440 schema_file = None for root, directories, filenames in os.walk('/usr/share/doc/'): for filename in filenames: if filename.startswith('kolab') and filename.endswith('.ldif') and schema_file == None: schema_file = os.path.join(root,filename) if not schema_file == None: try: shutil.copy( schema_file, '/etc/dirsrv/slapd-%s/schema/99%s' % ( _input['hostname'], os.path.basename(schema_file) ) ) schema_error = False except: log.error(_("Could not copy the LDAP extensions for Kolab")) schema_error = True else: log.error(_("Could not find the ldap Kolab schema file")) schema_error = True if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'restart', 'dirsrv.target']) subprocess.call(['/bin/systemctl', 'restart', 'dirsrv@' + _input['hostname']]) time.sleep(20) elif os.path.isfile('/sbin/service'): subprocess.call(['/sbin/service', 'dirsrv', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service','dirsrv','stop']) time.sleep(20) subprocess.call(['/usr/sbin/service','dirsrv','start']) else: log.error(_("Could not start the directory server service.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'enable', 'dirsrv.target']) subprocess.call(['/bin/systemctl', 'enable', 'dirsrv@' + _input['hostname']]) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['/sbin/chkconfig', 'dirsrv', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'dirsrv', 'defaults']) else: log.error(_("Could not configure to start on boot, the " + \ "directory server service.")) if ask_questions: print(utils.multiline_message( _(""" Please supply a Cyrus Administrator password. This password is used by Kolab to execute administrative tasks in Cyrus IMAP. You may also need the password yourself to troubleshoot Cyrus IMAP and/or perform other administrative tasks against Cyrus IMAP directly. """) ), file=sys.stderr) _input['cyrus_admin_pass'] = utils.ask_question( _("Cyrus Administrator password"), default=utils.generate_password(), password=True, confirm=True ) print(utils.multiline_message( _(""" Please supply a Kolab Service account password. This account is used by various services such as Postfix, and Roundcube, as anonymous binds to the LDAP server will not be allowed. """) ), file=sys.stderr) _input['kolab_service_pass'] = utils.ask_question( _("Kolab Service password"), default=utils.generate_password(), password=True, confirm=True ) else: _input['cyrus_admin_pass'] = conf.get('cyrus-imap', 'admin_password') _input['kolab_service_pass'] = conf.get('ldap', 'service_bind_pw') log.info(_("Writing out configuration to kolab.conf")) # Write out kolab configuration conf.command_set('kolab', 'primary_domain', _input['domain']) conf.command_set('ldap', 'base_dn', _input['rootdn']) conf.command_set('ldap', 'bind_dn', 'cn=Directory Manager') conf.command_set('ldap', 'bind_pw', _input['dirmgr_pass']) conf.command_set('ldap', 'service_bind_dn', 'uid=kolab-service,ou=Special Users,%s' % (_input['rootdn'])) conf.command_set('ldap', 'service_bind_pw', _input['kolab_service_pass']) fp = open(conf.defaults.config_file, "w+") conf.cfg_parser.write(fp) fp.close() log.info(_("Inserting service users into LDAP.")) # Insert service users auth = Auth(_input['domain']) auth.connect() auth._auth.connect() auth._auth._bind(bind_dn='cn=Directory Manager', bind_pw=_input['dirmgr_pass']) dn = 'uid=%s,ou=Special Users,%s' % (conf.get('cyrus-imap', 'admin_login'), _input['rootdn']) # A dict to help build the "body" of the object attrs = {} attrs['objectclass'] = ['top','person','inetorgperson','organizationalperson'] attrs['uid'] = conf.get('cyrus-imap', 'admin_login') attrs['givenname'] = "Cyrus" attrs['surname'] = "Administrator" attrs['cn'] = "Cyrus Administrator" attrs['userPassword'] = _input['cyrus_admin_pass'] # Convert our dict to nice syntax for the add-function using modlist-module ldif = ldap.modlist.addModlist(attrs) # Do the actual synchronous add-operation to the ldapserver auth._auth.ldap.add_s(dn, ldif) conf.command_set('cyrus-imap', 'admin_password', _input['cyrus_admin_pass']) dn = 'uid=kolab-service,ou=Special Users,%s' % (_input['rootdn']) # A dict to help build the "body" of the object attrs = {} attrs['objectclass'] = ['top','person','inetorgperson','organizationalperson'] attrs['uid'] = "kolab-service" attrs['givenname'] = "Kolab" attrs['surname'] = "Service" attrs['cn'] = "Kolab Service" attrs['userPassword'] = _input['kolab_service_pass'] attrs['nslookthroughlimit'] = '-1' attrs['nssizelimit'] = '-1' attrs['nstimelimit'] = '-1' attrs['nsidletimeout'] = '-1' # Convert our dict to nice syntax for the add-function using modlist-module ldif = ldap.modlist.addModlist(attrs) # Do the actual synchronous add-operation to the ldapserver auth._auth.ldap.add_s(dn, ldif) dn = 'ou=Resources,%s' % (_input['rootdn']) # A dict to help build the "body" of the object attrs = {} attrs['objectclass'] = ['top','organizationalunit'] attrs['ou'] = "Resources" # Convert our dict to nice syntax for the add-function using modlist-module ldif = ldap.modlist.addModlist(attrs) # Do the actual synchronous add-operation to the ldapserver auth._auth.ldap.add_s(dn, ldif) dn = 'ou=Shared Folders,%s' % (_input['rootdn']) # A dict to help build the "body" of the object attrs = {} attrs['objectclass'] = ['top','organizationalunit'] attrs['ou'] = "Shared Folders" # Convert our dict to nice syntax for the add-function using modlist-module ldif = ldap.modlist.addModlist(attrs) # Do the actual synchronous add-operation to the ldapserver auth._auth.ldap.add_s(dn, ldif) log.info(_("Writing out cn=kolab,cn=config")) dn = 'cn=kolab,cn=config' # A dict to help build the "body" of the object attrs = {} attrs['objectclass'] = ['top','extensibleobject'] attrs['cn'] = "kolab" attrs['aci'] = '(targetattr = "*") (version 3.0;acl "Kolab Services";allow (read,compare,search)(userdn = "ldap:///uid=kolab-service,ou=Special Users,%s");)' % (_input['rootdn']) # Convert our dict to nice syntax for the add-function using modlist-module ldif = ldap.modlist.addModlist(attrs) # Do the actual synchronous add-operation to the ldapserver auth._auth.ldap.add_s(dn, ldif) log.info(_("Adding domain %s to list of domains for this deployment") % (_input['domain'])) dn = "associateddomain=%s,cn=kolab,cn=config" % (_input['domain']) attrs = {} attrs['objectclass'] = ['top','domainrelatedobject'] attrs['associateddomain'] = [ '%s' % (_input['domain']), '%s' % (_input['fqdn']), 'localhost.localdomain', 'localhost' ] # De-duplicate attribute values before attempting to insert the object (#2205) attrs['associateddomain'] = list(set(attrs['associateddomain'])) attrs['associateddomain'].pop(attrs['associateddomain'].index(_input['domain'])) attrs['associateddomain'] = [ _input['domain'] ] + attrs['associateddomain'] attrs['aci'] = '(targetattr = "*") (version 3.0;acl "Read Access for %(domain)s Users";allow (read,compare,search)(userdn = "ldap:///%(rootdn)s??sub?(objectclass=*)");)' % (_input) # Add inetdomainbasedn in case the configured root dn is not the same as the # standard root dn for the domain name configured if not _input['rootdn'] == utils.standard_root_dn(_input['domain']): attrs['objectclass'].append('inetdomain') attrs['inetdomainbasedn'] = _input['rootdn'] ldif = ldap.modlist.addModlist(attrs) auth._auth.ldap.add_s(dn, ldif) if not conf.anonymous: log.info(_("Disabling anonymous binds")) dn = "cn=config" modlist = [] modlist.append((ldap.MOD_REPLACE, "nsslapd-allow-anonymous-access", "off")) auth._auth.ldap.modify_s(dn, modlist) # TODO: Ensure the uid attribute is unique # TODO^2: Consider renaming the general "attribute uniqueness to "uid attribute uniqueness" log.info(_("Enabling attribute uniqueness plugin")) dn = "cn=attribute uniqueness,cn=plugins,cn=config" modlist = [] modlist.append((ldap.MOD_REPLACE, "nsslapd-pluginEnabled", "on")) auth._auth.ldap.modify_s(dn, modlist) log.info(_("Enabling referential integrity plugin")) dn = "cn=referential integrity postoperation,cn=plugins,cn=config" modlist = [] modlist.append((ldap.MOD_REPLACE, "nsslapd-pluginEnabled", "on")) auth._auth.ldap.modify_s(dn, modlist) log.info(_("Enabling and configuring account policy plugin")) dn = "cn=Account Policy Plugin,cn=plugins,cn=config" modlist = [] modlist.append((ldap.MOD_REPLACE, "nsslapd-pluginEnabled", "on")) modlist.append((ldap.MOD_ADD, "nsslapd-pluginarg0", "cn=config,cn=Account Policy Plugin,cn=plugins,cn=config")) auth._auth.ldap.modify_s(dn, modlist) dn = "cn=config,cn=Account Policy Plugin,cn=plugins,cn=config" modlist = [] modlist.append((ldap.MOD_REPLACE, "alwaysrecordlogin", "yes")) modlist.append((ldap.MOD_ADD, "stateattrname", "lastLoginTime")) modlist.append((ldap.MOD_ADD, "altstateattrname", "createTimestamp")) auth._auth.ldap.modify_s(dn, modlist) # Add kolab-admin role log.info(_("Adding the kolab-admin role")) dn = "cn=kolab-admin,%s" % (_input['rootdn']) attrs = {} attrs['description'] = "Kolab Administrator" attrs['objectClass'] = ['top','ldapsubentry','nsroledefinition','nssimpleroledefinition','nsmanagedroledefinition'] attrs['cn'] = "kolab-admin" ldif = ldap.modlist.addModlist(attrs) auth._auth.ldap.add_s(dn, ldif) # User writeable attributes on root_dn log.info(_("Setting access control to %s") % (_input['rootdn'])) dn = _input['rootdn'] aci = [] if schema_error: aci.append('(targetattr = "carLicense || description || displayName || facsimileTelephoneNumber || homePhone || homePostalAddress || initials || jpegPhoto || l || labeledURI || mobile || o || pager || photo || postOfficeBox || postalAddress || postalCode || preferredDeliveryMethod || preferredLanguage || registeredAddress || roomNumber || secretary || seeAlso || st || street || telephoneNumber || telexNumber || title || userCertificate || userPassword || userSMIMECertificate || x500UniqueIdentifier") (version 3.0; acl "Enable self write for common attributes"; allow (read,compare,search,write)(userdn = "ldap:///self");)') else: aci.append('(targetattr = "carLicense || description || displayName || facsimileTelephoneNumber || homePhone || homePostalAddress || initials || jpegPhoto || l || labeledURI || mobile || o || pager || photo || postOfficeBox || postalAddress || postalCode || preferredDeliveryMethod || preferredLanguage || registeredAddress || roomNumber || secretary || seeAlso || st || street || telephoneNumber || telexNumber || title || userCertificate || userPassword || userSMIMECertificate || x500UniqueIdentifier || kolabDelegate || kolabInvitationPolicy || kolabAllowSMTPSender") (version 3.0; acl "Enable self write for common attributes"; allow (read,compare,search,write)(userdn = "ldap:///self");)') aci.append('(targetattr = "*") (version 3.0;acl "Directory Administrators Group";allow (all)(groupdn = "ldap:///cn=Directory Administrators,%(rootdn)s" or roledn = "ldap:///cn=kolab-admin,%(rootdn)s");)' % (_input)) aci.append('(targetattr="*")(version 3.0; acl "Configuration Administrators Group"; allow (all) groupdn="ldap:///cn=Configuration Administrators,ou=Groups,ou=TopologyManagement,o=NetscapeRoot";)') aci.append('(targetattr="*")(version 3.0; acl "Configuration Administrator"; allow (all) userdn="ldap:///uid=admin,ou=Administrators,ou=TopologyManagement,o=NetscapeRoot";)') aci.append('(targetattr = "*")(version 3.0; acl "SIE Group"; allow (all) groupdn = "ldap:///cn=slapd-%(hostname)s,cn=389 Directory Server,cn=Server Group,cn=%(fqdn)s,ou=%(domain)s,o=NetscapeRoot";)' % (_input)) aci.append('(targetattr != "userPassword") (version 3.0;acl "Search Access";allow (read,compare,search)(userdn = "ldap:///all");)') modlist = [] modlist.append((ldap.MOD_REPLACE, "aci", aci)) auth._auth.ldap.modify_s(dn, modlist) if os.path.isfile('/bin/systemctl'): if not os.path.isfile('/usr/lib/systemd/system/dirsrv-admin.service'): log.info(_("directory server admin service not available")) else: subprocess.call(['/bin/systemctl', 'enable', 'dirsrv-admin.service']) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['/sbin/chkconfig', 'dirsrv-admin', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'dirsrv-admin', 'defaults']) else: log.error(_("Could not start and configure to start on boot, the " + \ "directory server admin service.")) diff --git a/pykolab/setup/setup_manticore.py b/pykolab/setup/setup_manticore.py index 2f17244..c199ec6 100644 --- a/pykolab/setup/setup_manticore.py +++ b/pykolab/setup/setup_manticore.py @@ -1,101 +1,101 @@ # -*- coding: utf-8 -*- # Copyright 2010-2016 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from Cheetah.Template import Template import hashlib import os import random import re import subprocess import sys import time -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('manticore', execute, description=description(), after=['ldap','roundcube']) def description(): return _("Setup Manticore.") def execute(*args, **kw): if not os.path.isfile('/etc/manticore/local.env.js'): log.error(_("Manticore is not installed on this system")) return manticore_settings = { 'fqdn': hostname + '.' + domainname, 'secret': re.sub( r'[^a-zA-Z0-9]', "", "%s%s" % ( hashlib.md5("%s" % random.random()).digest().encode("base64"), hashlib.md5("%s" % random.random()).digest().encode("base64") ) )[:24], 'server_host': utils.parse_ldap_uri(conf.get('ldap', 'ldap_uri'))[1], 'auth_key': re.sub( r'[^a-zA-Z0-9]', "", "%s%s" % ( hashlib.md5("%s" % random.random()).digest().encode("base64"), hashlib.md5("%s" % random.random()).digest().encode("base64") ) )[:24], 'service_bind_dn': conf.get('ldap', 'service_bind_dn'), 'service_bind_pw': conf.get('ldap', 'service_bind_pw'), 'user_base_dn': conf.get('ldap', 'user_base_dn') } if os.path.isfile('/etc/kolab/templates/manticore.js.tpl'): fp = open('/etc/kolab/templates/manticore.js.tpl','r') else: fp = open('/usr/share/kolab/templates/manticore.js.tpl', 'r') template_definition = fp.read() fp.close() t = Template(template_definition, searchList=[manticore_settings]) fp = open('/etc/manticore/local.env.js', 'w') fp.write(t.__str__()) fp.close() if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'restart', 'mongod']) time.sleep(5) subprocess.call(['/bin/systemctl', 'restart', 'manticore']) else: log.error(_("Could not start the manticore service.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'enable', 'mongod']) subprocess.call(['/bin/systemctl', 'enable', 'manticore']) else: log.error(_("Could not configure the manticore service to start on boot")) diff --git a/pykolab/setup/setup_mta.py b/pykolab/setup/setup_mta.py index a880a5a..db8766d 100644 --- a/pykolab/setup/setup_mta.py +++ b/pykolab/setup/setup_mta.py @@ -1,543 +1,543 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from augeas import Augeas from Cheetah.Template import Template import os import shutil import subprocess -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('mta', execute, description=description(), after=['ldap']) def description(): return _("Setup MTA.") def execute(*args, **kw): group_filter = conf.get('ldap','kolab_group_filter') if group_filter == None: group_filter = conf.get('ldap','group_filter') user_filter = conf.get('ldap','kolab_user_filter') if user_filter == None: user_filter = conf.get('ldap','user_filter') resource_filter = conf.get('ldap', 'resource_filter') sharedfolder_filter = conf.get('ldap', 'sharedfolder_filter') server_host = utils.parse_ldap_uri(conf.get('ldap', 'ldap_uri'))[1] files = { "/etc/postfix/ldap/local_recipient_maps.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s query_filter = (&(|(mail=%%s)(alias=%%s))(|%(kolab_user_filter)s%(kolab_group_filter)s%(resource_filter)s%(sharedfolder_filter)s)) result_attribute = mail """ % { "base_dn": conf.get('ldap', 'base_dn'), "server_host": server_host, "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), "kolab_user_filter": user_filter, "kolab_group_filter": group_filter, "resource_filter": resource_filter, "sharedfolder_filter": sharedfolder_filter, }, "/etc/postfix/ldap/mydestination.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(domain_base_dn)s scope = sub bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s query_filter = %(domain_filter)s result_attribute = %(domain_name_attribute)s """ % { "server_host": server_host, "domain_base_dn": conf.get('ldap', 'domain_base_dn'), "domain_filter": conf.get('ldap', 'domain_filter').replace('*', '%s'), "domain_name_attribute": conf.get('ldap', 'domain_name_attribute'), "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, "/etc/postfix/ldap/mailenabled_distgroups.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(group_base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s # This finds the mail enabled distribution group LDAP entry query_filter = (&(|(mail=%%s)(alias=%%s))(objectClass=kolabgroupofuniquenames)(objectclass=groupofuniquenames)(!(objectclass=groupofurls))) # From this type of group, get all uniqueMember DNs special_result_attribute = uniqueMember # Only from those DNs, get the mail result_attribute = leaf_result_attribute = mail """ % { "server_host": server_host, "group_base_dn": conf.get('ldap', 'group_base_dn'), "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, "/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(group_base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s # This finds the mail enabled dynamic distribution group LDAP entry query_filter = (&(|(mail=%%s)(alias=%%s))(objectClass=kolabgroupofuniquenames)(objectClass=groupOfURLs)) # From this type of group, get all memberURL searches/references special_result_attribute = memberURL # Only from those DNs, get the mail result_attribute = leaf_result_attribute = mail """ % { "server_host": server_host, "group_base_dn": conf.get('ldap', 'group_base_dn'), "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, "/etc/postfix/ldap/transport_maps.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s query_filter = (&(|(mailAlternateAddress=%%s)(alias=%%s)(mail=%%s))(objectclass=kolabinetorgperson)) result_attribute = mail result_format = lmtp:unix:/var/lib/imap/socket/lmtp """ % { "base_dn": conf.get('ldap', 'base_dn'), "server_host": server_host, "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, "/etc/postfix/ldap/virtual_alias_maps.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabinetorgperson)) result_attribute = mail """ % { "base_dn": conf.get('ldap', 'base_dn'), "server_host": server_host, "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, "/etc/postfix/ldap/virtual_alias_maps_mailforwarding.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=mailrecipient)(objectclass=inetorgperson)(mailforwardingaddress=*)) result_attribute = mailForwardingAddress """ % { "base_dn": conf.get('ldap', 'base_dn'), "server_host": server_host, "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, "/etc/postfix/ldap/virtual_alias_maps_sharedfolders.cf": """ server_host = %(server_host)s server_port = 389 version = 3 search_base = %(base_dn)s scope = sub domain = ldap:/etc/postfix/ldap/mydestination.cf bind_dn = %(service_bind_dn)s bind_pw = %(service_bind_pw)s query_filter = (&(|(mail=%%s)(alias=%%s))(objectclass=kolabsharedfolder)(kolabFolderType=mail)) result_attribute = kolabtargetfolder result_format = "shared+%%s" """ % { "base_dn": conf.get('ldap', 'base_dn'), "server_host": server_host, "service_bind_dn": conf.get('ldap', 'service_bind_dn'), "service_bind_pw": conf.get('ldap', 'service_bind_pw'), }, } if not os.path.isfile('/etc/postfix/main.cf'): if os.path.isfile('/usr/share/postfix/main.cf.debian'): shutil.copy( '/usr/share/postfix/main.cf.debian', '/etc/postfix/main.cf' ) if not os.path.isdir('/etc/postfix/ldap'): os.mkdir('/etc/postfix/ldap/', 0o770) for filename in files: fp = open(filename, 'w') fp.write(files[filename]) fp.close() fp = open('/etc/postfix/transport', 'a') fp.write("\n# Shared Folder Delivery for %(domain)s:\nshared@%(domain)s\t\tlmtp:unix:/var/lib/imap/socket/lmtp\n" % {'domain': conf.get('kolab', 'primary_domain')}) fp.close() subprocess.call(["postmap", "/etc/postfix/transport"]) postfix_main_settings = { "inet_interfaces": "all", "recipient_delimiter": "+", "local_recipient_maps": "ldap:/etc/postfix/ldap/local_recipient_maps.cf", "mydestination": "ldap:/etc/postfix/ldap/mydestination.cf", "transport_maps": "ldap:/etc/postfix/ldap/transport_maps.cf, hash:/etc/postfix/transport", "virtual_alias_maps": "$alias_maps, ldap:/etc/postfix/ldap/virtual_alias_maps.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_mailforwarding.cf, ldap:/etc/postfix/ldap/virtual_alias_maps_sharedfolders.cf, ldap:/etc/postfix/ldap/mailenabled_distgroups.cf, ldap:/etc/postfix/ldap/mailenabled_dynamic_distgroups.cf", "smtpd_tls_auth_only": "yes", "smtpd_tls_security_level": "may", "smtp_tls_security_level": "may", "smtpd_sasl_auth_enable": "yes", "smtpd_sender_login_maps": "$local_recipient_maps", "smtpd_data_restrictions": "permit_mynetworks, check_policy_service unix:private/recipient_policy_incoming", "smtpd_recipient_restrictions": "permit_mynetworks, reject_unauth_pipelining, reject_rbl_client zen.spamhaus.org, reject_non_fqdn_recipient, reject_invalid_helo_hostname, reject_unknown_recipient_domain, reject_unauth_destination, check_policy_service unix:private/recipient_policy_incoming, permit", "smtpd_sender_restrictions": "permit_mynetworks, reject_sender_login_mismatch, check_policy_service unix:private/sender_policy_incoming", "submission_recipient_restrictions": "check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject", "submission_sender_restrictions": "reject_non_fqdn_sender, check_policy_service unix:private/submission_policy, permit_sasl_authenticated, reject", "submission_data_restrictions": "check_policy_service unix:private/submission_policy", "content_filter": "smtp-amavis:[127.0.0.1]:10024" } if os.path.isfile('/etc/pki/tls/certs/make-dummy-cert') and not os.path.isfile('/etc/pki/tls/private/localhost.pem'): subprocess.call(['/etc/pki/tls/certs/make-dummy-cert', '/etc/pki/tls/private/localhost.pem']) if os.path.isfile('/etc/pki/tls/private/localhost.pem'): postfix_main_settings['smtpd_tls_cert_file'] = "/etc/pki/tls/private/localhost.pem" postfix_main_settings['smtpd_tls_key_file'] = "/etc/pki/tls/private/localhost.pem" elif os.path.isfile('/etc/ssl/private/cyrus-imapd.pem'): # Debian 9 postfix_main_settings['smtpd_tls_cert_file'] = "/etc/ssl/private/cyrus-imapd.pem" postfix_main_settings['smtpd_tls_key_file'] = "/etc/ssl/private/cyrus-imapd.pem" # Copy header checks files for hc_file in [ 'inbound', 'internal', 'submission' ]: if not os.path.isfile("/etc/postfix/header_checks.%s" % (hc_file)): if os.path.isfile('/etc/kolab/templates/header_checks.%s' % (hc_file)): input_file = '/etc/kolab/templates/header_checks.%s' % (hc_file) elif os.path.isfile('/usr/share/kolab/templates/header_checks.%s' % (hc_file)): input_file = '/usr/share/kolab/templates/header_checks.%s' % (hc_file) elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'header_checks.%s' % (hc_file)))): input_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'header_checks.%s' % (hc_file))) shutil.copy(input_file, "/etc/postfix/header_checks.%s" % (hc_file)) subprocess.call(["postmap", "/etc/postfix/header_checks.%s" % (hc_file)]) myaugeas = Augeas() setting_base = '/files/etc/postfix/main.cf/' for setting_key in postfix_main_settings: setting = os.path.join(setting_base,setting_key) current_value = myaugeas.get(setting) if current_value == None: try: myaugeas.set(setting, postfix_main_settings[setting_key]) except: insert_paths = myaugeas.match('/files/etc/postfix/main.cf/*') insert_path = insert_paths[(len(insert_paths)-1)] myaugeas.insert(insert_path, setting_key, False) log.debug(_("Setting key %r to %r") % (setting_key, postfix_main_settings[setting_key]), level=8) myaugeas.set(setting, postfix_main_settings[setting_key]) myaugeas.save() postfix_master_settings = { } if os.path.exists('/usr/lib/postfix/kolab_smtp_access_policy'): postfix_master_settings['kolab_sap_executable_path'] = '/usr/lib/postfix/kolab_smtp_access_policy' else: postfix_master_settings['kolab_sap_executable_path'] = '/usr/libexec/postfix/kolab_smtp_access_policy' template_file = None if os.path.isfile('/etc/kolab/templates/master.cf.tpl'): template_file = '/etc/kolab/templates/master.cf.tpl' elif os.path.isfile('/usr/share/kolab/templates/master.cf.tpl'): template_file = '/usr/share/kolab/templates/master.cf.tpl' elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'master.cf.tpl'))): template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'master.cf.tpl')) if not template_file == None: fp = open(template_file, 'r') template_definition = fp.read() fp.close() t = Template(template_definition, searchList=[postfix_master_settings]) fp = open('/etc/postfix/master.cf', 'w') fp.write(t.__str__()) fp.close() else: log.error(_("Could not write out Postfix configuration file /etc/postfix/master.cf")) return if os.path.isdir('/etc/postfix/sasl/'): fp = open('/etc/postfix/sasl/smtpd.conf', 'w') fp.write("pwcheck_method: saslauthd\n") fp.write("mech_list: plain login\n") fp.close() amavisd_settings = { 'ldap_server': '%(server_host)s', 'ldap_bind_dn': conf.get('ldap', 'service_bind_dn'), 'ldap_bind_pw': conf.get('ldap', 'service_bind_pw'), 'primary_domain': conf.get('kolab', 'primary_domain'), 'ldap_filter': "(|(mail=%m)(alias=%m))", 'ldap_base_dn': conf.get('ldap', 'base_dn'), 'clamdsock': '/var/spool/amavisd/clamd.sock', } template_file = None # On RPM installations, Amavis configuration is contained within a single file. amavisconf_paths = [ "/etc/amavisd.conf", "/etc/amavis/amavisd.conf", "/etc/amavisd/amavisd.conf" ] amavis_conf = '' for amavisconf_path in amavisconf_paths: if os.path.isfile(amavisconf_path): amavis_conf = amavisconf_path break if os.path.isfile(amavis_conf): if os.path.isfile('/etc/kolab/templates/amavisd.conf.tpl'): template_file = '/etc/kolab/templates/amavisd.conf.tpl' elif os.path.isfile('/usr/share/kolab/templates/amavisd.conf.tpl'): template_file = '/usr/share/kolab/templates/amavisd.conf.tpl' elif os.path.isfile(os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'amavisd.conf.tpl'))): template_file = os.path.abspath(os.path.join(__file__, '..', '..', '..', 'share', 'templates', 'amavisd.conf.tpl')) if not template_file == None: fp = open(template_file, 'r') template_definition = fp.read() fp.close() if os.path.isfile('/etc/clamd.d/amavisd.conf'): amavisdconf_content = file('/etc/clamd.d/amavisd.conf') for line in amavisdconf_content: if line.startswith('LocalSocket'): amavisd_settings['clamdsock'] = line[len('LocalSocket '):].strip() t = Template(template_definition, searchList=[amavisd_settings]) fp = None fp = open(amavis_conf, 'w') if not fp == None: fp.write(t.__str__()) fp.close() else: log.error(_("Could not write out Amavis configuration file amavisd.conf")) return # On APT installations, /etc/amavis/conf.d/ is a directory with many more files. # # Somebody could work on enhancement request #1080 to configure LDAP lookups, # while really it isn't required. else: log.info(_("Not writing out any configuration for Amavis.")) # On debian wheezy amavisd-new expects '/etc/mailname' - possibly remediable through # the #1080 enhancement mentioned above, but here's a quick fix. f = open('/etc/mailname','w') f.writelines(conf.get('kolab', 'primary_domain')) f.close() if os.path.isfile('/etc/default/spamassassin'): myaugeas = Augeas() setting = os.path.join('/files/etc/default/spamassassin','ENABLED') if not myaugeas.get(setting) == '1': myaugeas.set(setting,'1') myaugeas.save() myaugeas.close() if os.path.isfile('/etc/default/wallace'): myaugeas = Augeas() setting = os.path.join('/files/etc/default/wallace','START') if not myaugeas.get(setting) == 'yes': myaugeas.set(setting,'yes') myaugeas.save() myaugeas.close() if os.path.isfile('/usr/lib/systemd/system/clamd@.service'): try: from ConfigParser import SafeConfigParser except ImportError: from configparser import SafeConfigParser unitfile = SafeConfigParser() unitfile.optionxform = str unitfile.read('/usr/lib/systemd/system/clamd@.service') if not unitfile.has_section('Install'): unitfile.add_section('Install') if not unitfile.has_option('Install', 'WantedBy'): unitfile.set('Install', 'WantedBy', 'multi-user.target') with open('/etc/systemd/system/clamd@.service', 'wb') as f: unitfile.write(f) log.info(_("Configuring and refreshing Anti-Virus...")) if os.path.isfile('/etc/kolab/templates/freshclam.conf.tpl'): shutil.copy( '/etc/kolab/templates/freshclam.conf.tpl', '/etc/freshclam.conf' ) elif os.path.isfile('/usr/share/kolab/templates/freshclam.conf.tpl'): shutil.copy( '/usr/share/kolab/templates/freshclam.conf.tpl', '/etc/freshclam.conf' ) else: log.error(_("Could not find a ClamAV update configuration file")) if os.path.isfile('/etc/freshclam.conf'): subprocess.call([ '/usr/bin/freshclam', '--quiet', '--datadir="/var/lib/clamav"' ]) amavisservice = 'amavisd.service' clamavservice = 'clamd@amavisd.service' if os.path.isfile('/usr/lib/systemd/system/amavis.service'): amavisservice = 'amavis.service' if os.path.isfile('/lib/systemd/system/amavis.service'): amavisservice = 'amavis.service' if os.path.isfile('/etc/init.d/amavis'): amavisservice = 'amavis.service' if os.path.isfile('/usr/lib/systemd/system/clamd.service'): clamavservice = 'clamd.service' if os.path.isfile('/lib/systemd/system/clamd.service'): clamavservice = 'clamd.service' if os.path.isfile('/lib/systemd/system/clamav-daemon.service'): clamavservice = 'clamav-daemon.service' if os.path.isfile('/bin/systemctl'): subprocess.call(['systemctl', 'restart', 'postfix.service']) subprocess.call(['systemctl', 'restart', amavisservice]) subprocess.call(['systemctl', 'restart', clamavservice]) subprocess.call(['systemctl', 'restart', 'wallace.service']) elif os.path.isfile('/sbin/service'): subprocess.call(['service', 'postfix', 'restart']) subprocess.call(['service', 'amavisd', 'restart']) subprocess.call(['service', 'clamd.amavisd', 'restart']) subprocess.call(['service', 'wallace', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service','postfix','restart']) subprocess.call(['/usr/sbin/service','amavis','restart']) subprocess.call(['/usr/sbin/service','clamav-daemon','restart']) subprocess.call(['/usr/sbin/service','wallace','restart']) else: log.error(_("Could not start the postfix, clamav and amavisd services services.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['systemctl', 'enable', 'postfix.service']) subprocess.call(['systemctl', 'enable', amavisservice]) subprocess.call(['systemctl', 'enable', clamavservice]) subprocess.call(['systemctl', 'enable', 'wallace.service']) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['chkconfig', 'postfix', 'on']) subprocess.call(['chkconfig', 'amavisd', 'on']) subprocess.call(['chkconfig', 'clamd.amavisd', 'on']) subprocess.call(['chkconfig', 'wallace', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'postfix', 'defaults']) subprocess.call(['/usr/sbin/update-rc.d', 'amavis', 'defaults']) subprocess.call(['/usr/sbin/update-rc.d', 'clamav-daemon', 'defaults']) subprocess.call(['/usr/sbin/update-rc.d', 'wallace', 'defaults']) else: log.error(_("Could not configure to start on boot, the " + \ "postfix, clamav and amavisd services.")) diff --git a/pykolab/setup/setup_mysql.py b/pykolab/setup/setup_mysql.py index 335c353..96ad202 100644 --- a/pykolab/setup/setup_mysql.py +++ b/pykolab/setup/setup_mysql.py @@ -1,343 +1,343 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import os import subprocess import tempfile import time -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('mysql', execute, description=description()) def cli_options(): mysql_group = conf.add_cli_parser_option_group(_("MySQL Options")) mysql_group.add_option( "--mysqlserver", dest="mysqlserver", action="store", help=_("Specify whether to use an (existing), (unix_socket) or (new) MySQL server.") ) mysql_group.add_option( "--mysqlhost", dest="mysqlhost", action="store", default='127.0.0.1', help=_("The MySQL host address.") ) mysql_group.add_option( "--mysqlrootpw", dest="mysqlrootpw", action="store", help=_("The MySQL root user password.") ) def description(): return _("Setup MySQL.") def execute(*args, **kw): # noqa: C901 socket_paths = [ "/var/lib/mysql/mysql.sock", "/var/run/mysqld/mysqld.sock", "/var/run/mysql/mysql.sock", "/var/run/mysqld/mysqld.pid" ] # on CentOS7, there is MariaDB instead of MySQL if conf.mysqlserver != 'existing': mysqlservice = 'mysqld.service' if os.path.isfile('/usr/lib/systemd/system/mariadb.service'): mysqlservice = 'mariadb.service' elif os.path.isfile('/usr/lib/systemd/system/mysql.service'): mysqlservice = 'mysql.service' if not os.path.isfile('/usr/lib/systemd/system/' + mysqlservice): # on Debian Jessie, systemctl restart mysql mysqlservice = 'mysql' if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'restart', mysqlservice]) elif os.path.isfile('/sbin/service'): subprocess.call(['/sbin/service', 'mysqld', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service', 'mysql', 'restart']) else: log.error(_("Could not start the MySQL database service.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'enable', mysqlservice]) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['/sbin/chkconfig', 'mysqld', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'mysql', 'defaults']) else: log.error( _("Could not configure to start on boot, the MySQL database service.") ) log.info(_("Waiting for at most 30 seconds for MySQL/MariaDB to settle...")) max_wait = 30 while max_wait > 0: for socket_path in socket_paths: if os.path.exists(socket_path): max_wait = 0 if max_wait > 0: max_wait = max_wait - 1 time.sleep(1) options = { 1: "Existing MySQL server (with root password already set).", 2: "Existing MySQL server (with unix_socket authentication plugin).", 3: "New MySQL server (needs to be initialized)." } answer = 0 if conf.mysqlserver != 'existing': if len([x for x in socket_paths if os.path.exists(x)]) > 0: if conf.mysqlserver: if conf.mysqlserver == 'existing': answer = 1 elif conf.mysqlserver == 'unix_socket': answer = 2 elif conf.mysqlserver == 'new': answer = 3 if answer == 0: answer = utils.ask_menu(_("What MySQL server are we setting up?"), options) else: answer = 1 if answer == "1" or answer == 1: if not conf.mysqlrootpw: print(utils.multiline_message( _(""" Please supply the root password for MySQL, so we can set up user accounts for other components that use MySQL. """) ), file=sys.stderr) mysql_root_password = utils.ask_question( _("MySQL root password"), password=True ) else: mysql_root_password = conf.mysqlrootpw elif answer == "2" or answer == 2: mysql_root_password = 'unix_socket' else: print(utils.multiline_message( _(""" Please supply a root password for MySQL. This password will be the administrative user for this MySQL server, and it should be kept a secret. After this setup process has completed, Kolab is going to discard and forget about this password, but you will need it for administrative tasks in MySQL. """) ), file=sys.stderr) mysql_root_password = utils.ask_question( _("MySQL root password"), default=utils.generate_password(), password=True, confirm=True ) p1 = subprocess.Popen( [ 'echo', 'UPDATE mysql.user SET Password=PASSWORD(\'%s\') WHERE User=\'root\';' % ( mysql_root_password ) ], stdout=subprocess.PIPE ) p2 = subprocess.Popen(['mysql'], stdin=p1.stdout) p1.stdout.close() p2.communicate() p1 = subprocess.Popen( [ 'echo', "UPDATE mysql.user SET authentication_string=PASSWORD('%s') WHERE User='root';" % ( mysql_root_password ) ], stdout=subprocess.PIPE ) p2 = subprocess.Popen(['mysql'], stdin=p1.stdout) p1.stdout.close() p2.communicate() p1 = subprocess.Popen( [ 'echo', """ UPDATE mysql.user SET plugin='mysql_native_password' WHERE User='root' AND plugin='auth_socket'; """ ], stdout=subprocess.PIPE ) p2 = subprocess.Popen(['mysql'], stdin=p1.stdout) p1.stdout.close() p2.communicate() p1 = subprocess.Popen(['echo', 'FLUSH PRIVILEGES;'], stdout=subprocess.PIPE) p2 = subprocess.Popen(['mysql'], stdin=p1.stdout) p1.stdout.close() p2.communicate() socket_path = None socket_paths = [ "/var/lib/mysql/mysql.sock", "/var/run/mysqld/mysqld.sock", "/var/run/mysql/mysql.sock" ] for sp in socket_paths: if os.path.exists(sp): socket_path = sp if mysql_root_password == "unix_socket" and socket_path is not None: data = """ [mysql] user=root password= host=localhost socket=%s """ % (socket_path) else: data = """ [mysql] user=root password='%s' host=%s """ % (mysql_root_password, conf.mysqlhost) fp = open('/tmp/kolab-setup-my.cnf', 'w') os.chmod('/tmp/kolab-setup-my.cnf', 0o600) fp.write(data) fp.close() schema_file = None for root, directories, filenames in os.walk('/usr/share/doc/'): for filename in filenames: if filename.startswith('kolab_wap') and filename.endswith('.sql'): # Skip the Oracle file if filename.endswith('oracle.sql'): continue schema_file = os.path.join(root, filename) if schema_file is not None: p1 = subprocess.Popen(['echo', 'create database kolab;'], stdout=subprocess.PIPE) p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf'], stdin=p1.stdout) p1.stdout.close() p2.communicate() print(utils.multiline_message( _(""" Please supply a password for the MySQL user 'kolab'. This password will be used by Kolab services, such as the Web Administration Panel. """) ), file=sys.stderr) mysql_kolab_password = utils.ask_question( _("MySQL kolab password"), default=utils.generate_password(), password=True, confirm=True ) p1 = subprocess.Popen( [ 'echo', "GRANT ALL PRIVILEGES ON kolab.* TO 'kolab'@'localhost' IDENTIFIED BY '%s';" % ( mysql_kolab_password ) ], stdout=subprocess.PIPE ) p2 = subprocess.Popen( [ 'mysql', '--defaults-file=/tmp/kolab-setup-my.cnf' ], stdin=p1.stdout ) p1.stdout.close() p2.communicate() p1 = subprocess.Popen(['cat', schema_file], stdout=subprocess.PIPE) p2 = subprocess.Popen( [ 'mysql', '--defaults-file=/tmp/kolab-setup-my.cnf', 'kolab' ], stdin=p1.stdout ) p1.stdout.close() p2.communicate() conf.command_set( 'kolab_wap', 'sql_uri', 'mysql://kolab:%s@localhost/kolab' % (mysql_kolab_password) ) conf.command_set( 'kolab_smtp_access_policy', 'cache_uri', 'mysql://kolab:%s@localhost/kolab' % (mysql_kolab_password) ) else: log.warning(_("Could not find the MySQL Kolab schema file")) diff --git a/pykolab/setup/setup_php.py b/pykolab/setup/setup_php.py index cbb93c0..a8fa654 100644 --- a/pykolab/setup/setup_php.py +++ b/pykolab/setup/setup_php.py @@ -1,139 +1,139 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function from augeas import Augeas import os import shutil import subprocess import tempfile -import components +from . import components import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('php', execute, description=description()) def cli_options(): php_group = conf.add_cli_parser_option_group(_("PHP Options")) php_group.add_option( "--timezone", dest="timezone", action="store", default=None, help=_("Specify the timezone for PHP.") ) php_group.add_option( "--with-php-ini", dest="php_ini_path", action="store", default=None, help=_("Specify the path to the php.ini file used with the webserver.") ) def description(): return _("Setup PHP.") def execute(*args, **kw): if conf.timezone is None: print(utils.multiline_message( _(""" Please supply the timezone PHP should be using. You have to use a Continent or Country / City locality name like 'Europe/Berlin', but not just 'CEST'. """) ), file=sys.stderr) conf.timezone = utils.ask_question( _("Timezone ID"), default="UTC" ) if conf.php_ini_path is not None: if not os.path.isfile(conf.php_ini_path): log.error( _("Cannot configure PHP through %r (No such file or directory)") % ( conf.php_ini_path ) ) return php_ini = conf.php_ini_path else: # Search and destroy php_ini = "/etc/php.ini" if not os.path.isfile(php_ini): php_ini = "/etc/php/7.2/apache2/php.ini" if not os.path.isfile(php_ini): php_ini = "/etc/php/7.0/apache2/php.ini" if not os.path.isfile(php_ini): php_ini = "/etc/php5/apache2/php.ini" if not os.path.isfile(php_ini): log.error(_("Could not find PHP configuration file php.ini")) return try: myaugeas = Augeas() setting_base = '/files%s/' % (php_ini) setting = os.path.join(setting_base, 'Date', 'date.timezone') current_value = myaugeas.get(setting) if current_value is None: insert_paths = myaugeas.match('/files%s/Date/*' % (php_ini)) insert_path = insert_paths[(len(insert_paths) - 1)] myaugeas.insert(insert_path, 'date.timezone', False) log.debug(_("Setting key %r to %r") % ('Date/date.timezone', conf.timezone), level=8) myaugeas.set(setting, conf.timezone) myaugeas.save() except IndexError: subprocess.Popen( [ 'sed', '-i', '-r', '-e', r's|^(;*)date\.timezone.*$|date.timezone = %s|g' % (conf.timezone), php_ini ] ) diff --git a/pykolab/setup/setup_roundcube.py b/pykolab/setup/setup_roundcube.py index b5567be..462c23a 100644 --- a/pykolab/setup/setup_roundcube.py +++ b/pykolab/setup/setup_roundcube.py @@ -1,375 +1,375 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import codecs import grp import hashlib import os import random import re import subprocess import sys import time from Cheetah.Template import Template -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register('roundcube', execute, description=description(), after=['mysql', 'ldap']) def description(): return _("Setup Roundcube.") def execute(*args, **kw): print(utils.multiline_message( """ Please supply a password for the MySQL user 'roundcube'. This password will be used by the Roundcube webmail interface. """ ), file=sys.stderr) mysql_roundcube_password = utils.ask_question( "MySQL roundcube password", default=utils.generate_password(), password=True, confirm=True ) conf.mysql_roundcube_password = mysql_roundcube_password rc_settings = { 'des_key': re.sub( r'[^a-zA-Z0-9]', "", "%s%s" % ( hashlib.md5("%s" % random.random()).digest().encode("base64"), hashlib.md5("%s" % random.random()).digest().encode("base64") ) )[:24], 'imap_admin_login': conf.get('cyrus-imap', 'admin_login'), 'imap_admin_password': conf.get('cyrus-imap', 'admin_password'), 'ldap_base_dn': conf.get('ldap', 'base_dn'), 'ldap_group_base_dn': conf.get('ldap', 'group_base_dn'), 'ldap_group_filter': conf.get('ldap', 'group_filter'), 'ldap_ldap_uri': conf.get('ldap', 'ldap_uri'), 'ldap_resource_base_dn': conf.get('ldap', 'resource_base_dn'), 'ldap_resource_filter': conf.get('ldap', 'resource_filter'), 'ldap_service_bind_dn': conf.get('ldap', 'service_bind_dn'), 'ldap_service_bind_pw': conf.get('ldap', 'service_bind_pw'), 'ldap_user_base_dn': conf.get('ldap', 'user_base_dn'), 'ldap_user_filter': conf.get('ldap', 'user_filter'), 'primary_domain': conf.get('kolab', 'primary_domain'), 'mysql_uri': 'mysqli://roundcube:%s@localhost/roundcube' % (mysql_roundcube_password), 'conf': conf } rc_paths = [ "/usr/share/roundcubemail/", "/usr/share/roundcube/", "/srv/www/roundcubemail/", "/var/www/roundcubemail/" ] rcpath = '' for rc_path in rc_paths: if os.path.isdir(rc_path): rcpath = rc_path break if not os.path.isdir(rcpath): log.error("Roundcube installation path not found.") return if os.access(rcpath + 'skins/kolab/', os.R_OK): rc_settings['skin'] = 'kolab' elif os.access(rcpath + 'skins/enterprise/', os.R_OK): rc_settings['skin'] = 'enterprise' elif os.access(rcpath + 'skins/chameleon/', os.R_OK): rc_settings['skin'] = 'chameleon' else: rc_settings['skin'] = 'larry' want_files = [ 'acl.inc.php', 'calendar.inc.php', 'config.inc.php', 'kolab_addressbook.inc.php', 'kolab_auth.inc.php', 'kolab_delegation.inc.php', 'kolab_files.inc.php', 'kolab_folders.inc.php', 'libkolab.inc.php', 'managesieve.inc.php', 'password.inc.php', 'recipient_to_contact.inc.php', 'terms.html', 'terms.inc.php' ] for want_file in want_files: template_file = None if os.path.isfile('/etc/kolab/templates/roundcubemail/%s.tpl' % (want_file)): template_file = '/etc/kolab/templates/roundcubemail/%s.tpl' % (want_file) elif os.path.isfile('/usr/share/kolab/templates/roundcubemail/%s.tpl' % (want_file)): template_file = '/usr/share/kolab/templates/roundcubemail/%s.tpl' % (want_file) if template_file is not None: # pylint: disable=logging-not-lazy log.debug("Using template file %r" % (template_file), level=8) filep = codecs.open(template_file, 'r', encoding='utf-8') template_definition = filep.read() filep.close() t = Template(template_definition, searchList=[rc_settings]) # pylint: disable=logging-not-lazy log.debug( "Successfully compiled template %r, writing out to %r" % ( template_file, want_file ), level=8 ) filep = None if os.path.isdir('/etc/roundcubemail'): filep = codecs.open('/etc/roundcubemail/%s' % (want_file), 'w', encoding='utf-8') elif os.path.isdir('/etc/roundcube'): filep = codecs.open('/etc/roundcube/%s' % (want_file), 'w', encoding='utf-8') if filep is not None: filep.write(t.respond()) filep.close() schema_files = [] # pylint: disable=too-many-nested-blocks for root, directories, filenames in os.walk('/usr/share/doc/'): directories.sort() for directory in directories: if directory.startswith("roundcubemail"): for _root, _directories, _filenames in os.walk(os.path.join(root, directory)): for filename in _filenames: if filename.startswith('mysql.initial') and filename.endswith('.sql'): schema_filepath = os.path.join(_root, filename) if schema_filepath not in schema_files: schema_files.append(schema_filepath) if schema_files: break if schema_files: break for root, directories, filenames in os.walk(rcpath + 'plugins/calendar/drivers/kolab/'): for filename in filenames: if filename.startswith('mysql') and filename.endswith('.sql'): schema_filepath = os.path.join(root, filename) if schema_filepath not in schema_files: schema_files.append(schema_filepath) for root, directories, filenames in os.walk(rcpath + 'plugins/libkolab/'): for filename in filenames: if filename.startswith('mysql') and filename.endswith('.sql'): schema_filepath = os.path.join(root, filename) if schema_filepath not in schema_files: schema_files.append(schema_filepath) for root, directories, filenames in os.walk('/usr/share/doc/'): directories.sort() for directory in directories: if directory.startswith("chwala"): for _root, _directories, _filenames in os.walk(os.path.join(root, directory)): for filename in _filenames: if filename.startswith('mysql.initial') and filename.endswith('.sql'): schema_filepath = os.path.join(_root, filename) if schema_filepath not in schema_files: schema_files.append(schema_filepath) if len(schema_files) > 0: break if len(schema_files) > 0: break if not os.path.isfile('/tmp/kolab-setup-my.cnf'): print(utils.multiline_message( """Please supply the MySQL root password (use 'unix_socket' for socket based authentication)""" ), file=sys.stderr) mysql_root_password = utils.ask_question( _("MySQL root password"), password=True ) socket_path = None socket_paths = [ "/var/lib/mysql/mysql.sock", "/var/run/mysqld/mysqld.sock", "/var/run/mysql/mysql.sock" ] for sp in socket_paths: if os.path.exists(sp): socket_path = sp if mysql_root_password == "unix_socket" and socket_path is not None: data = """ [mysql] user=root password= host=localhost socket=%s """ % (socket_path) else: data = """ [mysql] user=root password='%s' host=%s """ % (mysql_root_password, conf.mysqlhost) fp = open('/tmp/kolab-setup-my.cnf', 'w') os.chmod('/tmp/kolab-setup-my.cnf', 0o600) fp.write(data) fp.close() p1 = subprocess.Popen(['echo', 'create database roundcube;'], stdout=subprocess.PIPE) p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf'], stdin=p1.stdout) p1.stdout.close() p2.communicate() p1 = subprocess.Popen( [ 'echo', 'GRANT ALL PRIVILEGES ON roundcube.* TO \'roundcube\'@\'localhost\' IDENTIFIED BY \'%s\';' % ( mysql_roundcube_password ) ], stdout=subprocess.PIPE ) p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf'], stdin=p1.stdout) p1.stdout.close() p2.communicate() for schema_file in schema_files: p1 = subprocess.Popen(['cat', schema_file], stdout=subprocess.PIPE) p2 = subprocess.Popen( [ 'mysql', '--defaults-file=/tmp/kolab-setup-my.cnf', 'roundcube' ], stdin=p1.stdout ) p1.stdout.close() p2.communicate() p1 = subprocess.Popen(['echo', 'FLUSH PRIVILEGES;'], stdout=subprocess.PIPE) p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf'], stdin=p1.stdout) p1.stdout.close() p2.communicate() time.sleep(2) # Find Roundcube configuration that is not readable by the # webserver user/group. if os.path.isdir('/etc/roundcubemail/'): rccpath = "/etc/roundcubemail/" elif os.path.isdir('/etc/roundcube/'): rccpath = "/etc/roundcube" else: log.warning("Cannot find the configuration directory for roundcube.") rccpath = None root_uid = 0 webserver_gid = None for webserver_group in ['apache', 'www-data', 'www']: try: # pylint: disable=unused-variable (a, b, webserver_gid, d) = grp.getgrnam(webserver_group) break # pylint: disable=broad-except except Exception: pass if webserver_gid is not None: if rccpath is not None: for root, directories, filenames in os.walk(rccpath): for filename in filenames: try: os.chown( os.path.join(root, filename), root_uid, webserver_gid ) # pylint: disable=broad-except except Exception: pass httpservice = 'httpd.service' if os.path.isfile('/usr/lib/systemd/system/apache2.service'): httpservice = 'apache2.service' if os.path.isfile('/lib/systemd/system/apache2.service'): # Debian 9 httpservice = 'apache2.service' if os.path.isdir('/lib/systemd/system/apache2.service.d'): httpservice = 'apache2.service' if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'restart', httpservice]) elif os.path.isfile('/sbin/service'): subprocess.call(['/sbin/service', 'httpd', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service', 'apache2', 'restart']) else: log.error("Could not start the webserver server service.") if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'enable', httpservice]) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['/sbin/chkconfig', 'httpd', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'apache2', 'defaults']) else: log.error( "Could not configure to start on boot, the webserver server service." ) diff --git a/pykolab/setup/setup_syncroton.py b/pykolab/setup/setup_syncroton.py index 8ad2d8c..507ebf8 100644 --- a/pykolab/setup/setup_syncroton.py +++ b/pykolab/setup/setup_syncroton.py @@ -1,155 +1,155 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import os import subprocess import sys import time -import components +from . import components import pykolab from pykolab import utils from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() def __init__(): components.register( 'syncroton', execute, description=description(), after=['mysql','ldap','roundcube'] ) def description(): return _("Setup Syncroton.") def execute(*args, **kw): schema_files = [] for root, directories, filenames in os.walk('/usr/share/doc/'): for directory in directories: if directory.startswith("kolab-syncroton"): for root, directories, filenames in os.walk(os.path.join(root, directory)): for filename in filenames: if filename.startswith('mysql.initial') and filename.endswith('.sql'): schema_filepath = os.path.join(root,filename) if not schema_filepath in schema_files: schema_files.append(schema_filepath) break if len(schema_files) > 0: break if len(schema_files) > 0: break if not os.path.isfile('/tmp/kolab-setup-my.cnf'): print(utils.multiline_message( """Please supply the MySQL root password (use 'unix_socket' for socket based authentication)""" ), file=sys.stderr) mysql_root_password = utils.ask_question( _("MySQL root password"), password=True ) socket_path = None socket_paths = [ "/var/lib/mysql/mysql.sock", "/var/run/mysqld/mysqld.sock", "/var/run/mysql/mysql.sock" ] for sp in socket_paths: if os.path.exists(sp): socket_path = sp if mysql_root_password == "unix_socket" and socket_path is not None: data = """ [mysql] user=root password= host=localhost socket=%s """ % (socket_path) else: data = """ [mysql] user=root password='%s' host=%s """ % (mysql_root_password, conf.mysqlhost) data = """ [mysql] user=root password='%s' host=%s """ % (mysql_root_password, conf.mysqlhost) fp = open('/tmp/kolab-setup-my.cnf', 'w') os.chmod('/tmp/kolab-setup-my.cnf', 0o600) fp.write(data) fp.close() for schema_file in schema_files: p1 = subprocess.Popen(['cat', schema_file], stdout=subprocess.PIPE) p2 = subprocess.Popen(['mysql', '--defaults-file=/tmp/kolab-setup-my.cnf', 'roundcube'], stdin=p1.stdout) p1.stdout.close() p2.communicate() time.sleep(2) httpservice = 'httpd.service' if os.path.isfile('/usr/lib/systemd/system/apache2.service'): httpservice = 'apache2.service' if os.path.isfile('/lib/systemd/system/apache2.service'): # Debian 9 httpservice = 'apache2.service' if os.path.isdir('/lib/systemd/system/apache2.service.d'): httpservice = 'apache2.service' if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'restart', httpservice]) elif os.path.isfile('/sbin/service'): subprocess.call(['/sbin/service', 'httpd', 'restart']) elif os.path.isfile('/usr/sbin/service'): subprocess.call(['/usr/sbin/service','apache2','restart']) else: log.error(_("Could not start the webserver server service.")) if os.path.isfile('/bin/systemctl'): subprocess.call(['/bin/systemctl', 'enable', httpservice]) elif os.path.isfile('/sbin/chkconfig'): subprocess.call(['/sbin/chkconfig', 'httpd', 'on']) elif os.path.isfile('/usr/sbin/update-rc.d'): subprocess.call(['/usr/sbin/update-rc.d', 'apache2', 'defaults']) else: log.error(_("Could not configure to start on boot, the " + \ "webserver server service.")) diff --git a/pykolab/xml/__init__.py b/pykolab/xml/__init__.py index 99a269b..12ac4e6 100644 --- a/pykolab/xml/__init__.py +++ b/pykolab/xml/__init__.py @@ -1,69 +1,69 @@ -from attendee import Attendee -from attendee import InvalidAttendeeParticipantStatusError -from attendee import participant_status_label +from .attendee import Attendee +from .attendee import InvalidAttendeeParticipantStatusError +from .attendee import participant_status_label -from contact import Contact -from contact import ContactIntegrityError -from contact import contact_from_string -from contact import contact_from_message -from contact_reference import ContactReference -from recurrence_rule import RecurrenceRule +from .contact import Contact +from .contact import ContactIntegrityError +from .contact import contact_from_string +from .contact import contact_from_message +from .contact_reference import ContactReference +from .recurrence_rule import RecurrenceRule -from event import Event -from event import EventIntegrityError -from event import InvalidEventDateError -from event import InvalidEventStatusError -from event import event_from_ical -from event import event_from_string -from event import event_from_message +from .event import Event +from .event import EventIntegrityError +from .event import InvalidEventDateError +from .event import InvalidEventStatusError +from .event import event_from_ical +from .event import event_from_string +from .event import event_from_message -from todo import Todo -from todo import TodoIntegrityError -from todo import todo_from_ical -from todo import todo_from_string -from todo import todo_from_message +from .todo import Todo +from .todo import TodoIntegrityError +from .todo import todo_from_ical +from .todo import todo_from_string +from .todo import todo_from_message -from note import Note -from note import NoteIntegrityError -from note import note_from_string -from note import note_from_message +from .note import Note +from .note import NoteIntegrityError +from .note import note_from_string +from .note import note_from_message -from utils import property_label -from utils import property_to_string -from utils import compute_diff -from utils import to_dt +from .utils import property_label +from .utils import property_to_string +from .utils import compute_diff +from .utils import to_dt __all__ = [ "Attendee", "Contact", "ContactReference", "Event", "Todo", "Note", "RecurrenceRule", "event_from_ical", "event_from_string", "event_from_message", "todo_from_ical", "todo_from_string", "todo_from_message", "note_from_string", "note_from_message", "contact_from_string", "contact_from_message", "property_label", "property_to_string", "compute_diff", "to_dt", ] errors = [ "EventIntegrityError", "InvalidEventDateError", "InvalidAttendeeParticipantStatusError", "TodoIntegrityError", "NoteIntegrityError", "ContactIntegrityError", ] __all__.extend(errors) diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py index 050a1d5..cd8458f 100644 --- a/pykolab/xml/attendee.py +++ b/pykolab/xml/attendee.py @@ -1,280 +1,280 @@ import kolabformat from pykolab.translate import _ from pykolab.translate import N_ -from contact_reference import ContactReference +from .contact_reference import ContactReference participant_status_labels = { "NEEDS-ACTION": N_("Needs Action"), "ACCEPTED": N_("Accepted"), "DECLINED": N_("Declined"), "TENTATIVE": N_("Tentatively Accepted"), "DELEGATED": N_("Delegated"), "IN-PROCESS": N_("Started"), "COMPLETED": N_("Completed"), "PENDING": N_("Pending"), # support integer values, too kolabformat.PartNeedsAction: N_("Needs Action"), kolabformat.PartAccepted: N_("Accepted"), kolabformat.PartDeclined: N_("Declined"), kolabformat.PartTentative: N_("Tentatively Accepted"), kolabformat.PartDelegated: N_("Delegated"), kolabformat.PartInProcess: N_("Started"), kolabformat.PartCompleted: N_("Completed"), } def participant_status_label(status): return _(participant_status_labels[status]) if status in participant_status_labels else _(status) class Attendee(kolabformat.Attendee): cutype_map = { "INDIVIDUAL": kolabformat.CutypeIndividual, "RESOURCE": kolabformat.CutypeResource, "GROUP": kolabformat.CutypeGroup, "ROOM": kolabformat.CutypeRoom, "UNKNOWN": kolabformat.CutypeUnknown, } participant_status_map = { "NEEDS-ACTION": kolabformat.PartNeedsAction, "ACCEPTED": kolabformat.PartAccepted, "DECLINED": kolabformat.PartDeclined, "TENTATIVE": kolabformat.PartTentative, "DELEGATED": kolabformat.PartDelegated, "IN-PROCESS": kolabformat.PartInProcess, "COMPLETED": kolabformat.PartCompleted, } # See RFC 2445, 5445 role_map = { "CHAIR": kolabformat.Chair, "REQ-PARTICIPANT": kolabformat.Required, "OPT-PARTICIPANT": kolabformat.Optional, "NON-PARTICIPANT": kolabformat.NonParticipant, } rsvp_map = { "TRUE": True, "FALSE": False, } properties_map = { 'role': 'get_role', 'rsvp': 'rsvp', 'partstat': 'get_participant_status', 'cutype': 'get_cutype', 'delegated-to': 'get_delegated_to', 'delegated-from': 'get_delegated_from', } def __init__( self, email, name=None, rsvp=False, role=None, participant_status=None, cutype=None, ical_params=None ): self.email = email self.contactreference = ContactReference(email) if not name == None: self.contactreference.set_name(name) kolabformat.Attendee.__init__(self, self.contactreference) if isinstance(rsvp, bool): self.setRSVP(rsvp) else: if rsvp in self.rsvp_map: self.setRSVP(self.rsvp_map[rsvp]) if not role == None: self.set_role(role) if not cutype == None: self.set_cutype(cutype) if ical_params and 'DELEGATED-FROM' in ical_params: self.delegate_from(Attendee(str(ical_params['DELEGATED-FROM']), role=self.get_role(), cutype=self.get_cutype())) if ical_params and 'DELEGATED-TO' in ical_params: self.delegate_to(Attendee(str(ical_params['DELEGATED-TO']))) if not participant_status == None: self.set_participant_status(participant_status) def copy_from(self, obj): if isinstance(obj, kolabformat.Attendee): self.contactreference = ContactReference(obj.contact()) self.email = self.contactreference.get_email() self.setContact(self.contactreference) # manually copy all properities, copy constructor doesn't work :-( self.setRSVP(obj.rsvp()) self.setRole(obj.role()) self.setCutype(obj.cutype()) self.setPartStat(obj.partStat()) self.setDelegatedTo(obj.delegatedTo()) self.setDelegatedFrom(obj.delegatedFrom()) def delegate_from(self, delegators): crefs = [] if not isinstance(delegators, list): delegators = [delegators] for delegator in delegators: if not isinstance(delegator, Attendee): raise ValueError(_("Not a valid attendee")) else: self.set_role(delegator.get_role()) self.set_cutype(delegator.get_cutype()) crefs.append(delegator.contactreference) if len(crefs) == 0: raise ValueError(_("No valid delegator references found")) else: crefs += self.get_delegated_from() self.setDelegatedFrom(list(set(crefs))) def delegate_to(self, delegatees): self.set_participant_status("DELEGATED") crefs = [] if not isinstance(delegatees, list): delegatees = [delegatees] for delegatee in delegatees: if not isinstance(delegatee, Attendee): raise ValueError(_("Not a valid attendee")) else: crefs.append(delegatee.contactreference) if len(crefs) == 0: raise ValueError(_("No valid delegatee references found")) else: crefs += self.get_delegated_to() self.setDelegatedTo(list(set(crefs))) def get_cutype(self, translated=False): cutype = self.cutype() if translated: return self._translate_value(cutype, self.cutype_map) return cutype def get_delegated_from(self, translated=False): delegators = [] for cr in self.delegatedFrom(): delegators.append(cr.email() if translated else ContactReference(cr)) return delegators def get_delegated_to(self, translated=False): delegatees = [] for cr in self.delegatedTo(): delegatees.append(cr.email() if translated else ContactReference(cr)) return delegatees def get_email(self): return self.contactreference.get_email() def get_name(self): return self.contactreference.get_name() def get_displayname(self): name = self.contactreference.get_name() email = self.contactreference.get_email() return "%s <%s>" % (name, email) if not name == "" else email def get_participant_status(self, translated=False): partstat = self.partStat() if translated: return self._translate_value(partstat, self.participant_status_map) return partstat def get_role(self, translated=False): role = self.role() if translated: return self._translate_value(role, self.role_map) return role def get_rsvp(self): return self.rsvp() def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.items()]) return name_map[val] if val in name_map else 'UNKNOWN' def set_cutype(self, cutype): if cutype in self.cutype_map: self.setCutype(self.cutype_map[cutype]) elif cutype in self.cutype_map.values(): self.setCutype(cutype) else: raise InvalidAttendeeCutypeError(_("Invalid cutype %r") % (cutype)) def set_name(self, name): self.contactreference.set_name(name) self.setContact(self.contactreference) def set_participant_status(self, participant_status): if participant_status in self.participant_status_map: self.setPartStat(self.participant_status_map[participant_status]) elif participant_status in self.participant_status_map.values(): self.setPartStat(participant_status) else: raise InvalidAttendeeParticipantStatusError(_("Invalid participant status %r") % (participant_status)) def set_role(self, role): if role in self.role_map: self.setRole(self.role_map[role]) elif role in self.role_map.values(): self.setRole(role) else: raise InvalidAttendeeRoleError(_("Invalid role %r") % (role)) def set_rsvp(self, rsvp): self.setRSVP(rsvp) def to_dict(self): data = self.contactreference.to_dict() data.pop('type', None) for p, getter in self.properties_map.items(): val = None args = {} if hasattr(self, getter): if getter.startswith('get_'): args = dict(translated=True) val = getattr(self, getter)(**args) if val is not None: data[p] = val return data def __str__(self): return self.email class AttendeeIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeCutypeError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeParticipantStatusError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeRoleError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index 3b4fb29..cfd9289 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -1,1513 +1,1513 @@ from six import string_types import datetime import icalendar import kolabformat import pytz import time import uuid import base64 import re import pykolab from pykolab import constants from pykolab import utils from pykolab.xml import utils as xmlutils from pykolab.xml import participant_status_label from pykolab.xml.utils import ustr from pykolab.translate import _ from os import path -from attendee import Attendee -from contact_reference import ContactReference -from recurrence_rule import RecurrenceRule +from .attendee import Attendee +from .contact_reference import ContactReference +from .recurrence_rule import RecurrenceRule from collections import OrderedDict log = pykolab.getLogger('pykolab.xml_event') def event_from_ical(ical, string=None): return Event(from_ical=ical, from_string=string) def event_from_string(string): return Event(from_string=string) def event_from_message(message): event = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/calendar+xml": payload = part.get_payload(decode=True) event = event_from_string(payload) # append attachment parts to Event object elif event and 'Content-ID' in part: event._attachment_parts.append(part) return event class Event(object): type = 'event' thisandfuture = False status_map = { None: kolabformat.StatusUndefined, "TENTATIVE": kolabformat.StatusTentative, "CONFIRMED": kolabformat.StatusConfirmed, "CANCELLED": kolabformat.StatusCancelled, "COMPLETED": kolabformat.StatusCompleted, "IN-PROCESS": kolabformat.StatusInProcess, "NEEDS-ACTION": kolabformat.StatusNeedsAction, } classification_map = { "PUBLIC": kolabformat.ClassPublic, "PRIVATE": kolabformat.ClassPrivate, "CONFIDENTIAL": kolabformat.ClassConfidential, } alarm_type_map = { 'EMAIL': kolabformat.Alarm.EMailAlarm, 'DISPLAY': kolabformat.Alarm.DisplayAlarm, 'AUDIO': kolabformat.Alarm.AudioAlarm } related_map = { 'START': kolabformat.Start, 'END': kolabformat.End } properties_map = { # property: getter "uid": "get_uid", "created": "get_created", "lastmodified-date": "get_lastmodified", "sequence": "sequence", "classification": "get_classification", "categories": "categories", "start": "get_start", "end": "get_end", "duration": "get_duration", "transparency": "transparency", "rrule": "recurrenceRule", "rdate": "recurrenceDates", "exdate": "exceptionDates", "recurrence-id": "recurrenceID", "summary": "summary", "description": "description", "priority": "priority", "status": "get_ical_status", "location": "location", "organizer": "organizer", "attendee": "get_attendees", "attach": "attachments", "url": "url", "alarm": "alarms", "x-custom": "customProperties", # TODO: add to_dict() support for these # "exception": "exceptions", } def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] self._exceptions = [] self._attachment_parts = [] if isinstance(from_ical, str) and from_ical == "": if from_string == "": self.event = kolabformat.Event() else: self.event = kolabformat.readEvent(from_string, False) self._load_attendees() self._load_exceptions() else: self.from_ical(from_ical, from_string) self.set_created(self.get_created()) self.uid = self.get_uid() def _load_attendees(self): for a in self.event.attendees(): att = Attendee(a.contact().email()) att.copy_from(a) self._attendees.append(att) def _load_exceptions(self): for ex in self.event.exceptions(): exception = Event() exception.uid = ex.uid() exception.event = ex exception._load_attendees() self._exceptions.append(exception) def add_attendee(self, email_or_attendee, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL", params=None): if isinstance(email_or_attendee, Attendee): attendee = email_or_attendee else: attendee = Attendee(email_or_attendee, name, rsvp, role, participant_status, cutype, params) # apply update to self and all exceptions self.update_attendees([attendee], True) def add_category(self, category): self._categories.append(ustr(category)) self.event.setCategories(self._categories) def add_recurrence_date(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo is None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Rdate needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.addRecurrenceDate(xmlutils.to_cdatetime(_datetime, True)) def add_exception_date(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Exdate needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.addExceptionDate(xmlutils.to_cdatetime(_datetime, True)) def add_exception(self, exception): recurrence_id = exception.get_recurrence_id() if recurrence_id is None: raise EventIntegrityError("Recurrence exceptions require a Recurrence-ID property") # check if an exception with the given recurrence-id already exists append = True vexceptions = self.event.exceptions() for i, ex in enumerate(self._exceptions): if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture: # update the existing exception vexceptions[i] = exception.event self._exceptions[i] = exception append = False # check if main event matches the given recurrence-id if append and self.get_recurrence_id() == recurrence_id: self.event = exception.event self._load_attendees() self._load_exceptions() append = False if append: vexceptions.append(exception.event) self._exceptions.append(exception) self.event.setExceptions(vexceptions) def del_exception(self, exception): recurrence_id = exception.get_recurrence_id() if recurrence_id is None: raise EventIntegrityError("Recurrence exceptions require a Recurrence-ID property") updated = False vexceptions = self.event.exceptions() for i, ex in enumerate(self._exceptions): if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture: del vexceptions[i] del self._exceptions[i] updated = True if updated: self.event.setExceptions(vexceptions) def as_string_itip(self, method="REQUEST"): cal = icalendar.Calendar() cal.add( 'prodid', '-//pykolab-%s-%s//kolab.org//' % ( constants.__version__, constants.__release__ ) ) cal.add('version', '2.0') # TODO: Really? cal.add('calscale', 'GREGORIAN') # TODO: Not always a request... cal.add('method', method) # TODO: Add timezone information using icalendar.?() # Not sure if there is a class for it. cal.add_component(self.to_ical()) # add recurrence exceptions if len(self._exceptions) > 0 and not method == 'REPLY': for exception in self._exceptions: cal.add_component(exception.to_ical()) if hasattr(cal, 'to_ical'): return cal.to_ical() elif hasattr(cal, 'as_string'): return cal.as_string() def to_ical(self): event = icalendar.Event() # Required event['uid'] = self.get_uid() # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(event.singletons)): _attr = attr.lower().replace('-', '') ical_getter = 'get_ical_%s' % (_attr) default_getter = 'get_%s' % (_attr) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() if not retval == None and not retval == "": event.add(attr.lower(), retval) elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if not retval == None and not retval == "": event.add(attr.lower(), retval, encode=0) # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(event.multiple)): _attr = attr.lower().replace('-', '') ical_getter = 'get_ical_%s' % (_attr) default_getter = 'get_%s' % (_attr) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if isinstance(retval, list) and not len(retval) == 0: for _retval in retval: event.add(attr.lower(), _retval, encode=0) # copy custom properties to iCal for cs in self.event.customProperties(): event.add(cs.identifier, cs.value) return event def delegate(self, delegators, delegatees, names=None): if not isinstance(delegators, list): delegators = [delegators] if not isinstance(delegatees, list): delegatees = [delegatees] if not isinstance(names, list): names = [names] _delegators = [] for delegator in delegators: _delegators.append(self.get_attendee(delegator)) _delegatees = [] for i,delegatee in enumerate(delegatees): try: _delegatees.append(self.get_attendee(delegatee)) except: # TODO: An iTip needs to be sent out to the new attendee self.add_attendee(delegatee, names[i] if i < len(names) else None) _delegatees.append(self.get_attendee(delegatee)) for delegator in _delegators: delegator.delegate_to(_delegatees) for delegatee in _delegatees: delegatee.delegate_from(_delegators) self.event.setAttendees(self._attendees) def from_ical(self, ical, raw=None): if isinstance(ical, icalendar.Event) or isinstance(ical, icalendar.Calendar): ical_event = ical elif hasattr(icalendar.Event, 'from_ical'): ical_event = icalendar.Event.from_ical(ical) elif hasattr(icalendar.Event, 'from_string'): ical_event = icalendar.Event.from_string(ical) # VCALENDAR block was given, find the first VEVENT item if isinstance(ical_event, icalendar.Calendar): for c in ical_event.walk(): if c.name == 'VEVENT': ical_event = c break # use the libkolab calendaring bindings to load the full iCal data if 'RRULE' in ical_event or 'ATTACH' in ical_event \ or [part for part in ical_event.walk() if part.name == 'VALARM']: if raw is None or raw == "": raw = ical if isinstance(ical, str) else ical.to_ical() self._xml_from_ical(raw) else: self.event = kolabformat.Event() # TODO: Clause the timestamps for zulu suffix causing datetime.datetime # to fail substitution. for attr in list(set(ical_event.required)): if attr in ical_event: self.set_from_ical(attr.lower(), ical_event[attr]) # NOTE: Make sure to list(set()) or duplicates may arise # NOTE: Keep the original order e.g. to read DTSTART before RECURRENCE-ID for attr in list(OrderedDict.fromkeys(ical_event.singletons)): if attr in ical_event: if isinstance(ical_event[attr], list): ical_event[attr] = ical_event[attr][0]; self.set_from_ical(attr.lower(), ical_event[attr]) # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(ical_event.multiple)): if attr in ical_event: self.set_from_ical(attr.lower(), ical_event[attr]) def _xml_from_ical(self, ical): if not "BEGIN:VCALENDAR" in ical: ical = "BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR" from kolab.calendaring import EventCal self.event = EventCal() success = self.event.fromICal(ical) if success: self._load_exceptions() return success def get_attendee_participant_status(self, attendee): return attendee.get_participant_status() def get_attendee(self, attendee): if isinstance(attendee, string_types): if attendee in [x.get_email() for x in self.get_attendees()]: attendee = self.get_attendee_by_email(attendee) elif attendee in [x.get_name() for x in self.get_attendees()]: attendee = self.get_attendee_by_name(attendee) else: raise ValueError(_("No attendee with email or name %r") %(attendee)) return attendee elif isinstance(attendee, Attendee): return attendee else: raise ValueError(_("Invalid argument value attendee %r, must be basestring or Attendee") % (attendee)) def find_attendee(self, attendee): try: return self.get_attendee(attendee) except: return None def get_attendee_by_email(self, email): if email in [x.get_email() for x in self.get_attendees()]: return [x for x in self.get_attendees() if x.get_email() == email][0] raise ValueError(_("No attendee with email %r") %(email)) def get_attendee_by_name(self, name): if name in [x.get_name() for x in self.get_attendees()]: return [x for x in self.get_attendees() if x.get_name() == name][0] raise ValueError(_("No attendee with name %r") %(name)) def get_attendees(self): return self._attendees def get_categories(self): return [str(c) for c in self.event.categories()] def get_classification(self): return self.event.classification() def get_created(self): try: return xmlutils.from_cdatetime(self.event.created(), True) except ValueError: return datetime.datetime.now() def get_description(self): return self.event.description() def get_comment(self): if hasattr(self.event, 'comment'): return self.event.comment() else: return None def get_duration(self): duration = self.event.duration() if duration and duration.isValid(): dtd = datetime.timedelta( days=duration.days(), seconds=duration.seconds(), minutes=duration.minutes(), hours=duration.hours(), weeks=duration.weeks() ) return dtd return None def get_end(self): dt = xmlutils.from_cdatetime(self.event.end(), True) if not dt: duration = self.get_duration() if duration is not None: dt = self.get_start() + duration return dt def get_date_text(self, date_format=None, time_format=None): if date_format is None: date_format = _("%Y-%m-%d") if time_format is None: time_format = _("%H:%M (%Z)") start = self.get_start() end = self.get_end() all_day = not hasattr(start, 'date') start_date = start.date() if not all_day else start end_date = end.date() if not all_day else end if start_date == end_date: end_format = time_format else: end_format = date_format + " " + time_format if all_day: time_format = '' if start_date == end_date: return start.strftime(date_format) return "%s - %s" % (start.strftime(date_format + " " + time_format), end.strftime(end_format)) def get_recurrence_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.recurrenceDates()) def get_exception_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates()) def get_exceptions(self): return self._exceptions def has_exceptions(self): return len(self._exceptions) > 0 def get_attachments(self): return self.event.attachments() def get_attachment_data(self, i): vattach = self.event.attachments() if i < len(vattach): attachment = vattach[i] uri = attachment.uri() if uri and uri[0:4] == 'cid:': # get data from MIME part with matching content-id cid = '<' + uri[4:] + '>' for p in self._attachment_parts: if p['Content-ID'] == cid: return p.get_payload(decode=True) else: return attachment.data() return None def get_alarms(self): return self.event.alarms() def get_ical_attendee(self): # TODO: Formatting, aye? See also the example snippet: # # ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP: # MAILTO:employee-A@host.com attendees = [] for attendee in self.get_attendees(): email = attendee.get_email() name = attendee.get_name() rsvp = attendee.get_rsvp() role = attendee.get_role() partstat = attendee.get_participant_status() cutype = attendee.get_cutype() delegators = attendee.get_delegated_from() delegatees = attendee.get_delegated_to() if rsvp in attendee.rsvp_map: _rsvp = rsvp elif rsvp in attendee.rsvp_map.values(): _rsvp = [k for k, v in attendee.rsvp_map.items() if v == rsvp][0] else: _rsvp = None if role in attendee.role_map: _role = role elif role in attendee.role_map.values(): _role = [k for k, v in attendee.role_map.items() if v == role][0] else: _role = None if partstat in attendee.participant_status_map: _partstat = partstat elif partstat in attendee.participant_status_map.values(): _partstat = [k for k, v in attendee.participant_status_map.items() if v == partstat][0] else: _partstat = None if cutype in attendee.cutype_map: _cutype = cutype elif cutype in attendee.cutype_map.values(): _cutype = [k for k, v in attendee.cutype_map.items() if v == cutype][0] else: _cutype = None _attendee = icalendar.vCalAddress("MAILTO:%s" % email) if not name == None and not name == "": _attendee.params['CN'] = icalendar.vText(name) if not _rsvp == None: _attendee.params['RSVP'] = icalendar.vText(_rsvp) if not _role == None: _attendee.params['ROLE'] = icalendar.vText(_role) if not _partstat == None: _attendee.params['PARTSTAT'] = icalendar.vText(_partstat) if not _cutype == None: _attendee.params['CUTYPE'] = icalendar.vText(_cutype) if not delegators == None and len(delegators) > 0: _attendee.params['DELEGATED-FROM'] = icalendar.vText(delegators[0].email()) if not delegatees == None and len(delegatees) > 0: _attendee.params['DELEGATED-TO'] = icalendar.vText(delegatees[0].email()) attendees.append(_attendee) return attendees def get_ical_attendee_participant_status(self, attendee): attendee = self.get_attendee(attendee) if attendee.get_participant_status() in attendee.participant_status_map: return attendee.get_participant_status() elif attendee.get_participant_status() in attendee.participant_status_map.values(): return [k for k, v in attendee.participant_status_map.items() if v == attendee.get_participant_status()][0] else: raise ValueError(_("Invalid participant status")) def get_ical_created(self): return self.get_created() def get_ical_dtend(self): dtend = self.get_end() # shift end by one day on all-day events if not hasattr(dtend, 'hour'): dtend = dtend + datetime.timedelta(days=1) return dtend def get_ical_dtstamp(self): try: retval = self.get_lastmodified() if retval == None or retval == "": return datetime.datetime.now() except: return datetime.datetime.now() def get_ical_lastmodified(self): return self.get_ical_dtstamp() def get_ical_dtstart(self): return self.get_start() def get_ical_organizer(self): contact = self.get_organizer() organizer = icalendar.vCalAddress("MAILTO:%s" % contact.email()) name = contact.name() if not name == None and not name == "": organizer.params["CN"] = icalendar.vText(name) return organizer def get_ical_status(self): status = self.event.status() if status in self.status_map: return status return self._translate_value(status, self.status_map) if status else None def get_ical_class(self): class_ = self.event.classification() return self._translate_value(class_, self.classification_map) if class_ else None def get_ical_sequence(self): return str(self.event.sequence()) if self.event.sequence() else None def get_ical_comment(self): comment = self.get_comment() if comment is not None: return [ comment ] return None def get_ical_recurrenceid(self): rid = self.get_recurrence_id() if isinstance(rid, datetime.datetime) or isinstance(rid, datetime.date): prop = icalendar.vDatetime(rid) if isinstance(rid, datetime.datetime) else icalendar.vDate(rid) if self.thisandfuture: prop.params.update({'RANGE':'THISANDFUTURE'}) return prop return None def get_ical_rrule(self): result = [] rrule = self.get_recurrence() if rrule.isValid(): result.append(rrule.to_ical()) return result def get_ical_rdate(self): rdates = self.get_recurrence_dates() for i in range(len(rdates)): rdates[i] = icalendar.prop.vDDDLists(rdates[i]) return rdates def get_location(self): return self.event.location() def get_lastmodified(self): try: _datetime = self.event.lastModified() if _datetime == None or not _datetime.isValid(): self.__str__() except: self.__str__() return xmlutils.from_cdatetime(self.event.lastModified(), True) def get_organizer(self): organizer = self.event.organizer() return organizer def get_priority(self): return str(self.event.priority()) def get_start(self): return xmlutils.from_cdatetime(self.event.start(), True) def get_status(self, translated=False): status = self.event.status() if translated: return self._translate_value(status, self.status_map) if status else None return status def get_summary(self): return self.event.summary() def get_uid(self): uid = self.event.uid() if not uid == '': return uid else: self.set_uid(uuid.uuid4()) return self.get_uid() def get_recurrence_id(self): self.thisandfuture = self.event.thisAndFuture(); recurrence_id = xmlutils.from_cdatetime(self.event.recurrenceID(), True) # fix recurrence-id type if stored as date instead of datetime if recurrence_id is not None and isinstance(recurrence_id, datetime.date): dtstart = self.get_start() if not type(recurrence_id) == type(dtstart): recurrence_id = datetime.datetime.combine(recurrence_id, dtstart.time()).replace(tzinfo=dtstart.tzinfo) return recurrence_id def get_thisandfuture(self): self.thisandfuture = self.event.thisAndFuture(); return self.thisandfuture def get_sequence(self): return self.event.sequence() def get_url(self): return self.event.url() def get_transparency(self): return self.event.transparency() def get_recurrence(self): return RecurrenceRule(self.event.recurrenceRule()) def set_attendees(self, _attendees, recursive=False): if recursive: self._attendees = [] self.update_attendees(_attendees, True) else: self._attendees = _attendees self.event.setAttendees(self._attendees) def set_attendee_participant_status(self, attendee, status, rsvp=None): """ Set the participant status of an attendee to status. As the attendee arg, pass an email address or name, for this function to obtain the attendee object by searching the list of attendees for this event. """ attendee = self.get_attendee(attendee) attendee.set_participant_status(status) if rsvp is not None: attendee.set_rsvp(rsvp) # apply update to self and all exceptions self.update_attendees([attendee], False) def update_attendees(self, _attendees, append=True): self.merge_attendee_data(_attendees, append) if len(self._exceptions): vexceptions = self.event.exceptions() for i, exception in enumerate(self._exceptions): exception.merge_attendee_data(_attendees, append) vexceptions[i] = exception.event self.event.setExceptions(vexceptions) def merge_attendee_data(self, _attendees, append=True): for attendee in _attendees: found = False for candidate in self._attendees: if candidate.get_email() == attendee.get_email(): candidate.copy_from(attendee) found = True break if not found and append: self._attendees.append(attendee) self.event.setAttendees(self._attendees) def set_classification(self, classification): if classification in self.classification_map: self.event.setClassification(self.classification_map[classification]) elif classification in self.classification_map.values(): self.event.setClassification(classification) else: raise ValueError(_("Invalid classification %r") % (classification)) def set_created(self, _datetime=None): if _datetime is None or isinstance(_datetime, datetime.time): _datetime = datetime.datetime.utcnow() self.event.setCreated(xmlutils.to_cdatetime(_datetime, False, True)) def set_description(self, description): self.event.setDescription(ustr(description)) def set_comment(self, comment): if hasattr(self.event, 'setComment'): self.event.setComment(ustr(comment)) def set_dtstamp(self, _datetime): self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True)) def set_end(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event end needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setEnd(xmlutils.to_cdatetime(_datetime, True)) def set_exception_dates(self, _datetimes): for _datetime in _datetimes: self.add_exception_date(_datetime) def set_recurrence_dates(self, _datetimes): for _datetime in _datetimes: self.add_recurrence_date(_datetime) def add_custom_property(self, name, value): if not name.upper().startswith('X-'): raise ValueError(_("Invalid custom property name %r") % (name)) props = self.event.customProperties() props.append(kolabformat.CustomProperty(name.upper(), value)) self.event.setCustomProperties(props) def set_from_ical(self, attr, value): attr = attr.replace('-', '') ical_setter = 'set_ical_' + attr default_setter = 'set_' + attr params = value.params if hasattr(value, 'params') else {} if isinstance(value, icalendar.vDDDTypes) and hasattr(value, 'dt'): value = value.dt if attr == "categories": self.add_category(value) elif attr == "class": if (value and value[:2] not in ['X-', 'x-']): self.set_classification(value) elif attr == "recurrenceid": self.set_ical_recurrenceid(value, params) elif hasattr(self, ical_setter): getattr(self, ical_setter)(value) elif hasattr(self, default_setter): getattr(self, default_setter)(value) def set_ical_attendee(self, _attendee): if isinstance(_attendee, string_types): _attendee = [_attendee] if isinstance(_attendee, list): for attendee in _attendee: address = str(attendee).split(':')[-1] if hasattr(attendee, 'params'): params = attendee.params else: params = {} if 'CN' in params: name = ustr(params['CN']) else: name = None if 'ROLE' in params: role = params['ROLE'] else: role = None if 'PARTSTAT' in params: partstat = params['PARTSTAT'] else: partstat = None if 'RSVP' in params: rsvp = params['RSVP'] else: rsvp = None if 'CUTYPE' in params: cutype = params['CUTYPE'] else: cutype = kolabformat.CutypeIndividual att = self.add_attendee(address, name=name, rsvp=rsvp, role=role, participant_status=partstat, cutype=cutype, params=params) def set_ical_dtend(self, dtend): # shift end by one day on all-day events if not hasattr(dtend, 'hour'): dtend = dtend - datetime.timedelta(days=1) self.set_end(dtend) def set_ical_dtstamp(self, dtstamp): self.set_dtstamp(dtstamp) def set_ical_dtstart(self, dtstart): self.set_start(dtstart) def set_ical_lastmodified(self, lastmod): self.set_lastmodified(lastmod) def set_ical_duration(self, value): if hasattr(value, 'dt'): value = value.dt duration = kolabformat.Duration(value.days, 0, 0, value.seconds, False) self.event.setDuration(duration) def set_ical_organizer(self, organizer): address = str(organizer).split(':')[-1] cn = None if hasattr(organizer, 'params'): params = organizer.params else: params = {} if 'CN' in params: cn = ustr(params['CN']) self.set_organizer(str(address), name=cn) def set_ical_priority(self, priority): self.set_priority(priority) def set_ical_sequence(self, sequence): self.set_sequence(sequence) def set_ical_summary(self, summary): self.set_summary(ustr(summary)) def set_ical_uid(self, uid): self.set_uid(str(uid)) def set_ical_rdate(self, rdate): rdates = [] # rdate here can be vDDDLists or a list of vDDDLists, why?! if isinstance(rdate, icalendar.prop.vDDDLists): rdate = [rdate] for _rdate in rdate: if isinstance(_rdate, icalendar.prop.vDDDLists): tzid = None if hasattr(_rdate, 'params') and 'TZID' in _rdate.params: tzid = _rdate.params['TZID'] dts = icalendar.prop.vDDDLists.from_ical(_rdate.to_ical(), tzid) for datetime in dts: rdates.append(datetime) self.set_recurrence_dates(rdates) def set_ical_recurrenceid(self, value, params): try: self.thisandfuture = params.get('RANGE', '') == 'THISANDFUTURE' self.set_recurrence_id(value, self.thisandfuture) except InvalidEventDateError: pass def set_lastmodified(self, _datetime=None): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): valid_datetime = True if _datetime is None or isinstance(_datetime, datetime.time): valid_datetime = True _datetime = datetime.datetime.utcnow() if not valid_datetime: raise InvalidEventDateError(_("Event last-modified needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True)) def set_location(self, location): self.event.setLocation(ustr(location)) def set_organizer(self, email, name=None): contactreference = ContactReference(email) if not name == None: contactreference.setName(name) self.event.setOrganizer(contactreference) def set_priority(self, priority): self.event.setPriority(priority) def set_sequence(self, sequence): self.event.setSequence(int(sequence)) def set_url(self, url): self.event.setUrl(ustr(url)) def set_recurrence(self, recurrence): self.event.setRecurrenceRule(recurrence) # reset eventcal instance if hasattr(self, 'eventcal'): del self.eventcal def set_start(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event start needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setStart(xmlutils.to_cdatetime(_datetime, True)) def set_status(self, status): if status in self.status_map: self.event.setStatus(self.status_map[status]) elif status in self.status_map.values(): self.event.setStatus(status) elif not status == kolabformat.StatusUndefined: raise InvalidEventStatusError(_("Invalid status set: %r") % (status)) def set_summary(self, summary): self.event.setSummary(summary) def set_uid(self, uid): self.uid = uid self.event.setUid(str(uid)) def set_recurrence_id(self, _datetime, _thisandfuture=None): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, use the one from event start if _datetime.tzinfo == None: _start = self.get_start() _datetime = _datetime.replace(tzinfo=_start.tzinfo) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event recurrence-id needs datetime.datetime instance")) if _thisandfuture is None: _thisandfuture = self.thisandfuture self.event.setRecurrenceID(xmlutils.to_cdatetime(_datetime), _thisandfuture) def set_transparency(self, transp): return self.event.setTransparency(transp) def __str__(self): event_xml = kolabformat.writeEvent(self.event) error = kolabformat.error() if error == None or not error: return event_xml else: raise EventIntegrityError(kolabformat.errorMessage()) def to_dict(self): data = dict() for p, getter in self.properties_map.items(): val = None if hasattr(self, getter): val = getattr(self, getter)() elif hasattr(self.event, getter): val = getattr(self.event, getter)() if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectordatetime): val = [xmlutils.from_cdatetime(x, True) for x in val] elif isinstance(val, kolabformat.vectors): val = [str(x) for x in val] elif isinstance(val, kolabformat.vectorcs): for x in val: data[x.identifier] = x.value val = None elif isinstance(val, kolabformat.ContactReference): val = ContactReference(val).to_dict() elif isinstance(val, kolabformat.RecurrenceRule): val = RecurrenceRule(val).to_dict() elif isinstance(val, kolabformat.vectorattachment): val = [dict(fmttype=x.mimetype(), label=x.label(), uri=x.uri()) for x in val] elif isinstance(val, kolabformat.vectoralarm): val = [self._alarm_to_dict(x) for x in val] elif isinstance(val, list): val = [x.to_dict() for x in val if hasattr(x, 'to_dict')] if val is not None: data[p] = val return data def _alarm_to_dict(self, alarm): ret = dict( action=self._translate_value(alarm.type(), self.alarm_type_map), summary=alarm.summary(), description=alarm.description(), trigger=None ) start = alarm.start() if start and start.isValid(): ret['trigger'] = xmlutils.from_cdatetime(start, True) else: ret['trigger'] = dict(related=self._translate_value(alarm.relativeTo(), self.related_map)) duration = alarm.relativeStart() if duration and duration.isValid(): prefix = '-' if duration.isNegative() else '+' value = prefix + "P%dW%dDT%dH%dM%dS" % ( duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds() ) ret['trigger']['value'] = re.sub(r"T$", '', re.sub(r"0[WDHMS]", '', value)) if alarm.type() == kolabformat.Alarm.EMailAlarm: ret['attendee'] = [ContactReference(a).to_dict() for a in alarm.attendees()] return ret def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.items()]) return name_map[val] if val in name_map else 'UNKNOWN' def to_message(self, creator=None): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate msg = MIMEMultipart() organizer = self.get_organizer() email = organizer.email() name = organizer.name() if creator: msg['From'] = creator elif not name: msg['From'] = email else: msg['From'] = '"%s" <%s>' % (name, email) msg['To'] = ', '.join([x.__str__() for x in self.get_attendees()]) msg['Date'] = formatdate(localtime=True) msg.add_header('X-Kolab-MIME-Version', '3.0') msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.' + self.type) text = utils.multiline_message(""" This is a Kolab Groupware object. To view this object you will need an email client that understands the Kolab Groupware format. For a list of such email clients please visit http://www.kolab.org/ """) msg.attach( MIMEText(text) ) part = MIMEBase('application', "calendar+xml") part.set_charset('UTF-8') msg["Subject"] = self.get_uid() # extract attachment data into separate MIME parts vattach = self.event.attachments() i = 0 for attach in vattach: if attach.uri(): continue mimetype = attach.mimetype() (primary, seconday) = mimetype.split('/') name = attach.label() if not name: name = 'unknown.x' (basename, suffix) = path.splitext(name) t = datetime.datetime.now() cid = "%s.%s.%s%s" % (basename, time.mktime(t.timetuple()), t.microsecond + len(self._attachment_parts), suffix) p = MIMEBase(primary, seconday) p.add_header('Content-Disposition', 'attachment', filename=name) p.add_header('Content-Transfer-Encoding', 'base64') p.add_header('Content-ID', '<' + cid + '>') p.set_payload(base64.b64encode(attach.data())) self._attachment_parts.append(p) # modify attachment object attach.setData('', mimetype) attach.setUri('cid:' + cid, mimetype) vattach[i] = attach i += 1 self.event.setAttachments(vattach) part.set_payload(str(self)) part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"') part.replace_header('Content-Transfer-Encoding', '8bit') msg.attach(part) # append attachment parts for p in self._attachment_parts: msg.attach(p) return msg def to_message_itip(self, from_address, method="REQUEST", participant_status="ACCEPTED", subject=None, message_text=None): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate # encode unicode strings with quoted-printable from email import charset charset.add_charset('utf-8', charset.SHORTEST, charset.QP) msg = MIMEMultipart("alternative") msg_from = None attendees = None if method == "REPLY": # TODO: Make user friendly name msg['To'] = self.get_organizer().email() attendees = self.get_attendees() reply_attendees = [] # There's an exception here for delegation (partstat DELEGATED) for attendee in attendees: if attendee.get_email() == from_address: # Only the attendee is supposed to be listed in a reply attendee.set_participant_status(participant_status) attendee.set_rsvp(False) reply_attendees.append(attendee) name = attendee.get_name() email = attendee.get_email() if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) elif from_address in attendee.get_delegated_from(True): reply_attendees.append(attendee) # keep only replying (and delegated) attendee(s) self._attendees = reply_attendees self.event.setAttendees(self._attendees) if msg_from == None: organizer = self.get_organizer() email = organizer.email() name = organizer.name() if email == from_address: if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) elif method == "REQUEST": organizer = self.get_organizer() email = organizer.email() name = organizer.name() if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) if msg_from == None: if from_address == None: log.error(_("No sender specified")) else: msg_from = from_address msg['From'] = utils.str2unicode(msg_from) msg['Date'] = formatdate(localtime=True) if subject is None: subject = _("Invitation for %s was %s") % (self.get_summary(), participant_status_label(participant_status)) msg['Subject'] = utils.str2unicode(subject) if message_text is None: message_text = _("""This is an automated response to one of your event requests.""") msg.attach(MIMEText(utils.stripped_message(message_text), _charset='utf-8')) part = MIMEBase('text', 'calendar', charset='UTF-8', method=method) del part['MIME-Version'] # mime parts don't need this part.set_payload(self.as_string_itip(method=method)) part.add_header('Content-Transfer-Encoding', '8bit') msg.attach(part) # restore the original list of attendees # attendees being reduced to the replying attendee above if attendees is not None: self._attendees = attendees self.event.setAttendees(self._attendees) return msg def is_recurring(self): return self.event.recurrenceRule().isValid() or len(self.get_recurrence_dates()) > 0 def to_event_cal(self): from kolab.calendaring import EventCal return EventCal(self.event) def get_next_occurence(self, _datetime): if not hasattr(self, 'eventcal'): self.eventcal = self.to_event_cal() next_cdatetime = self.eventcal.getNextOccurence(xmlutils.to_cdatetime(_datetime, True)) next_datetime = xmlutils.from_cdatetime(next_cdatetime, True) if next_cdatetime is not None else None # cut infinite recurrence at a reasonable point if next_datetime and not self.get_last_occurrence() and next_datetime > xmlutils.to_dt(self._recurrence_end()): next_datetime = None # next_datetime is always a cdatetime, convert to date if necessary if next_datetime and not isinstance(self.get_start(), datetime.datetime): next_datetime = datetime.date(next_datetime.year, next_datetime.month, next_datetime.day) return next_datetime def get_occurence_end_date(self, datetime): if not datetime: return None if not hasattr(self, 'eventcal'): return None end_cdatetime = self.eventcal.getOccurenceEndDate(xmlutils.to_cdatetime(datetime, True)) return xmlutils.from_cdatetime(end_cdatetime, True) if end_cdatetime is not None else None def get_last_occurrence(self, force=False): if not hasattr(self, 'eventcal'): self.eventcal = self.to_event_cal() last = self.eventcal.getLastOccurrence() last_datetime = xmlutils.from_cdatetime(last, True) if last is not None else None # we're forced to return some date if last_datetime is None and force: last_datetime = self._recurrence_end() return last_datetime def get_next_instance(self, datetime): next_start = self.get_next_occurence(datetime) if next_start: instance = Event(from_string=str(self)) instance.set_start(next_start) instance.event.setRecurrenceID(xmlutils.to_cdatetime(next_start), False) next_end = self.get_occurence_end_date(next_start) if next_end: instance.set_end(next_end) # unset recurrence rule and exceptions instance.set_recurrence(kolabformat.RecurrenceRule()) instance.event.setExceptions(kolabformat.vectorevent()) instance.event.setExceptionDates(kolabformat.vectordatetime()) instance._exceptions = [] instance._isexception = False # unset attachments list (only stored in main event) instance.event.setAttachments(kolabformat.vectorattachment()) # copy data from matching exception # (give precedence to single occurrence exceptions over thisandfuture) for exception in self._exceptions: recurrence_id = exception.get_recurrence_id() if recurrence_id == next_start and (not exception.thisandfuture or not instance._isexception): instance = exception instance._isexception = True if not exception.thisandfuture: break elif exception.thisandfuture and next_start > recurrence_id: # TODO: merge exception properties over this instance + adjust start/end with the according offset pass return instance return None def get_instance(self, _datetime): # If no timezone information is given, use the one from event start if isinstance(_datetime, datetime.datetime) and _datetime.tzinfo == None: _start = self.get_start() if hasattr(_start, 'tzinfo'): _datetime = _datetime.replace(tzinfo=_start.tzinfo) if self.is_recurring(): instance = self.get_next_instance(_datetime - datetime.timedelta(days=1)) while instance: recurrence_id = instance.get_recurrence_id() if type(recurrence_id) == type(_datetime) and recurrence_id <= _datetime: if xmlutils.dates_equal(recurrence_id, _datetime): return instance instance = self.get_next_instance(instance.get_start()) else: break elif self.has_exceptions(): for exception in self._exceptions: recurrence_id = exception.get_recurrence_id() if type(recurrence_id) == type(_datetime) and xmlutils.dates_equal(recurrence_id, _datetime): return exception if self.get_recurrence_id(): recurrence_id = self.get_recurrence_id() if type(recurrence_id) == type(_datetime) and xmlutils.dates_equal(recurrence_id, _datetime): return self return None def _recurrence_end(self): """ Determine a reasonable end date for infinitely recurring events """ rrule = self.event.recurrenceRule() if rrule.isValid() and rrule.count() < 0 and not rrule.end().isValid(): now = datetime.datetime.now() switch = { kolabformat.RecurrenceRule.Yearly: 100, kolabformat.RecurrenceRule.Monthly: 20 } intvl = switch[rrule.frequency()] if rrule.frequency() in switch else 10 return self.get_start().replace(year=now.year + intvl) return xmlutils.from_cdatetime(rrule.end(), True) class EventIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidEventDateError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidEventStatusError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/wallace/__init__.py b/wallace/__init__.py index 4a22e3e..4f73ac6 100644 --- a/wallace/__init__.py +++ b/wallace/__init__.py @@ -1,680 +1,680 @@ # -*- coding: utf-8 -*- # Copyright 2010-2019 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import asyncore from distutils import version import grp import multiprocessing import os import pwd import traceback import smtpd import socket import struct import sys import tempfile from threading import _Timer import time import pykolab from pykolab import utils from pykolab.logger import StderrToLogger from pykolab.translate import _ as _l -import modules -from modules import cb_action_ACCEPT +from . import modules +from .modules import cb_action_ACCEPT # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.wallace') sys.stderr = StderrToLogger(log) conf = pykolab.getConf() def pickup_message(filepath, *args, **kwargs): wallace_modules = args[0] if 'module' in kwargs: # Cause the previous modules to be skipped wallace_modules = wallace_modules[(wallace_modules.index(kwargs['module']) + 1):] log.debug(_l("Wallace modules: %r") % (wallace_modules), level=8) # Execute the module if 'stage' in kwargs: modules.execute(kwargs['module'], filepath, stage=kwargs['stage']) else: modules.execute(kwargs['module'], filepath) # After all modules are executed, continue with a call to # accept the message and re-inject in to Postfix. continue_with_accept = True for module in wallace_modules: try: result_filepath = modules.execute(module, filepath) except Exception: log.error( "Module %s.execute() failed on message %r with error: %s" % ( module, filepath, traceback.format_exc() ) ) result_filepath = False if result_filepath is not None and result_filepath is not False: filepath = result_filepath else: # A module has returned False or None continue_with_accept = False # The message very likely has been consumed by the module that returned False if not os.path.isfile(filepath): break if continue_with_accept: cb_action_ACCEPT('wallace', filepath) def modules_heartbeat(wallace_modules): lastrun = 0 while not multiprocessing.current_process().finished.is_set(): try: for module in wallace_modules: try: modules.heartbeat(module, lastrun) except Exception: log.error( "Module %s.heartbeat() failed with error: %s" % ( module, traceback.format_exc() ) ) lastrun = int(time.time()) multiprocessing.current_process().finished.wait(60) except (SystemExit, KeyboardInterrupt) as errmsg: log.warning("Exiting %s, %s" % (multiprocessing.current_process().name, errmsg)) break def worker_process(*args, **kwargs): import signal signal.signal(signal.SIGINT, signal.SIG_IGN) log.debug("Worker process %s initializing" % (multiprocessing.current_process().name), level=1) # pylint: disable=too-few-public-methods class Timer(_Timer): def run(self): while True: while not self.finished.is_set(): self.finished.wait(self.interval) log.debug(_l("Timer looping function '%s' every %ss") % ( self.function.__name__, self.interval ), level=8) self.function(*self.args, **self.kwargs) self.finished.set() log.debug( _l("Timer loop %s") % ('still active', 'finished')[self.finished.is_set()], level=8 ) break class WallaceDaemon: heartbeat = None timer = None def __init__(self): self.current_connections = 0 self.max_connections = 24 self.parent_pid = None self.pool = None daemon_group = conf.add_cli_parser_option_group(_l("Daemon Options")) daemon_group.add_option( "--fork", dest="fork_mode", action="store_true", default=False, help=_l("Fork to the background.") ) daemon_group.add_option( "-b", "--bind", dest="wallace_bind_address", action="store", default="localhost", help=_l("Bind address for Wallace.") ) daemon_group.add_option( "-g", "--group", dest="process_groupname", action="store", default="kolab", help=_l("Run as group GROUPNAME"), metavar="GROUPNAME" ) daemon_group.add_option( "--threads", dest="max_threads", action="store", default=4, type=int, help=_l("Number of threads to use.") ) daemon_group.add_option( "--max-tasks", dest="max_tasks", action="store", default=None, type=int, help=_l("Number of tasks per process.") ) daemon_group.add_option( "-p", "--pid-file", dest="pidfile", action="store", default="/var/run/wallaced/wallaced.pid", help=_l("Path to the PID file to use.") ) daemon_group.add_option( "--port", dest="wallace_port", action="store", default=10026, type=int, help=_l("Port that Wallace is supposed to use.") ) daemon_group.add_option( "-u", "--user", dest="process_username", action="store", default="kolab", help=_l("Run as user USERNAME"), metavar="USERNAME" ) conf.finalize_conf() utils.ensure_directory( os.path.dirname(conf.pidfile), conf.process_username, conf.process_groupname ) if conf.debuglevel >= 9: mp_logger = multiprocessing.get_logger() mp_logger.setLevel(multiprocessing.SUBDEBUG) mp_logger.debug('Python multi-processing logger started') modules.initialize() self.modules = conf.get_list('wallace', 'modules') if not self.modules: self.modules = [] # pylint: disable=too-many-branches # pylint: disable=too-many-statements def do_wallace(self): self.parent_pid = os.getpid() if version.StrictVersion(sys.version[:3]) >= version.StrictVersion("2.7"): self.pool = multiprocessing.Pool(conf.max_threads, worker_process, (), conf.max_tasks) else: self.pool = multiprocessing.Pool(conf.max_threads, worker_process, ()) self.pickup_spool_messages(sync=True) s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) bound = False shutdown = False while not bound: try: if shutdown: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind((conf.wallace_bind_address, conf.wallace_port)) bound = True # pylint: disable=broad-except except Exception: log.warning( _l("Could not bind to socket on port %d on bind address %s") % ( conf.wallace_port, conf.wallace_bind_address ) ) while not shutdown: try: s.shutdown(socket.SHUT_RDWR) shutdown = True # pylint: disable=broad-except except Exception: log.warning(_l("Could not shut down socket")) time.sleep(1) s.close() time.sleep(1) s.listen(5) self.timer = Timer(180, self.pickup_spool_messages, args=[], kwargs={'sync': True}) # pylint: disable=attribute-defined-outside-init self.timer.daemon = True self.timer.start() # start background process to run periodic jobs in active modules try: self.heartbeat = multiprocessing.Process( target=modules_heartbeat, name='Wallace_Heartbeat', args=[self.modules] ) self.heartbeat.finished = multiprocessing.Event() self.heartbeat.daemon = True self.heartbeat.start() except Exception as errmsg: log.error("Failed to start heartbeat daemon: %s" % (errmsg)) finally: log.debug( "Wallace heartbeat is %s" % ('not alive', 'alive')[self.heartbeat.is_alive()], level=8 ) try: while 1: while self.current_connections >= self.max_connections: log.debug( _l("Reached limit of max connections of: %s. Sleeping for 0.5s") % ( self.max_connections ), level=6 ) time.sleep(0.5) pair = s.accept() log.debug( _l("Accepted connection %r with address %r") % ( pair if pair is not None else (None, None) ), level=8 ) if pair is not None: self.current_connections += 1 connection, address = pair _smtpd = smtpd # Set DEBUGSTREAM of smtpd to log to pykolab logger if conf.debuglevel > 8: _smtpd.DEBUGSTREAM = pykolab.logger.StderrToLogger(log) log.debug(_l("Creating SMTPChannel for accepted message"), level=8) _smtpd.SMTPChannel(self, connection, address) asyncore.loop() else: log.error(_l("Socket accepted, but (conn, address) tuple is None.")) # pylint: disable=broad-except except Exception: traceback.print_exc() s.shutdown(1) s.close() # shut down hearbeat process self.heartbeat.terminate() self.timer.cancel() self.timer.join() # pylint: disable=no-self-use def data_header(self, mailfrom, rcpttos): COMMASPACE = ', ' return "X-Kolab-From: " + mailfrom + "\r\n" + \ "X-Kolab-To: " + COMMASPACE.join(rcpttos) + "\r\n" def pickup_spool_messages(self, sync=False): # Mind you to include the trailing slash pickup_path = '/var/spool/pykolab/wallace/' messages = [] for root, _, files in os.walk(pickup_path): for filename in files: messages.append((root, filename)) for root, filename in messages: filepath = os.path.join(root, filename) try: # ignore calls on too young files if os.stat(filepath).st_mtime + 150 > time.time(): log.debug("File not more than 150s old. Skipping %s" % (filepath), level=8) continue # ignore calls on lock files if '/locks/' in filepath: log.debug("File is in locks directory. Skipping %s" % (filepath), level=8) continue # pylint: disable=broad-except except Exception as errmsg: log.error("Error: %s. Skipping %s" % (errmsg, filepath)) continue if not root == pickup_path: module = os.path.dirname(root).replace(pickup_path, '') # Compare uppercase status (specifically, DEFER) with # lowercase (plugin names). # # The messages in DEFER are supposed to be picked up by # another thread, whereas the messages in other directories # are pending being handled by their respective plugins. # # TODO: Handle messages in spool directories for which a # plugin had been enabled, but is not enabled any longer. # if module.lower() == "defer": # Wallace was unable to deliver to re-injection smtpd. # Skip it, another thread is picking up the deferred # messages. continue stage = root.replace(pickup_path, '').split('/') if len(stage) < 2: stage = None else: stage = stage[1] if stage.lower() == "hold": continue # Do not handle messages in a defer state. if stage.lower() == "defer": continue self.current_connections += 1 if sync: pickup_message(filepath, self.modules, module=module, stage=stage) else: self.pool.apply_async( pickup_message, ( filepath, (self.modules), {'module': module, 'stage': stage} ) ) self.current_connections -= 1 continue self.current_connections += 1 if sync: pickup_message(filepath, self.modules) else: self.pool.apply_async(pickup_message, (filepath, (self.modules))) self.current_connections -= 1 def process_message(self, peer, mailfrom, rcpttos, data): """ We have retrieved the message. This should be as fast as possible, and not ever block. """ header = self.data_header(mailfrom, rcpttos) (fp, filename) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/") # @TODO: and add line separator (\n or \r\n?) # we should make sure there's only one line separator between # kolab headers and the original message (data) os.write(fp, header) os.write(fp, data) os.close(fp) log.debug(_l("Started processing accepted message %s") % filename, level=8) self.pool.apply_async(pickup_message, (filename, (self.modules))) self.current_connections -= 1 return "250 OK Message %s queued" % (filename) def reload_config(self, *args, **kwargs): pass def remove_pid(self, *args, **kwargs): try: if os.getpid() == self.parent_pid: log.debug("Stopping process %s" % multiprocessing.current_process().name, level=8) log.debug(_l("Terminating processes pool"), level=8) self.pool.close() if hasattr(self, 'timer'): if not self.timer.finished.is_set(): log.debug("Canceling Wallace Timer", level=8) self.timer.finished.set() self.timer.cancel() log.debug(_l("Terminating heartbeat process"), level=8) self.heartbeat.finished.set() self.heartbeat.terminate() self.pool.close() self.pool.join(5) self.timer.join(5) self.heartbeat.join(5) if os.access(conf.pidfile, os.R_OK): log.warning(_l("Removing PID file %s") % conf.pidfile) os.remove(conf.pidfile) log.warning("Exiting!") sys.exit() else: sys.exit(0) except Exception as errmsg: log.debug( "Exception while trying to stop %s: %s" % ( multiprocessing.current_process().name, errmsg ), level=8 ) sys.exit(1) sys.exit(0) # pylint: disable=too-many-locals def run(self): # noqa: C901 """ Run the Wallace daemon. """ exitcode = 0 try: try: (ruid, _, _) = os.getresuid() (rgid, _, _) = os.getresgid() except AttributeError: ruid = os.getuid() rgid = os.getgid() if ruid == 0: # Means we can setreuid() / setregid() / setgroups() if rgid == 0: # Get group entry details try: (_, _, group_gid, _) = grp.getgrnam(conf.process_groupname) except KeyError: print(_l("Group %s does not exist") % (conf.process_groupname)) sys.exit(1) # Set real and effective group if not the same as current. if not group_gid == rgid: log.debug( _l("Switching real and effective group id to %d") % ( group_gid ), level=8 ) os.setregid(group_gid, group_gid) if ruid == 0: # Means we haven't switched yet. try: (_, _, user_uid, _, _, _, _) = pwd.getpwnam(conf.process_username) except KeyError: print(_l("User %s does not exist") % (conf.process_username)) sys.exit(1) # Set real and effective user if not the same as current. if not user_uid == ruid: log.debug( _l("Switching real and effective user id to %d") % ( user_uid ), level=8 ) os.setreuid(user_uid, user_uid) # pylint: disable=broad-except except Exception: log.error(_l("Could not change real and effective uid and/or gid")) try: pid = os.getpid() if conf.fork_mode: pid = os.fork() if pid > 0 and not conf.fork_mode: self.do_wallace() elif pid > 0: sys.exit(0) else: # Give up the session, all control, # all open file descriptors, see #5151 os.chdir("/") old_umask = os.umask(0) os.setsid() pid = os.fork() if pid > 0: sys.exit(0) sys.stderr.flush() sys.stdout.flush() os.close(0) os.close(1) os.close(2) os.open(os.devnull, os.O_RDONLY) os.open(os.devnull, os.O_WRONLY) os.open(os.devnull, os.O_WRONLY) os.umask(old_umask) log.remove_stdout_handler() self.set_signal_handlers() self.write_pid() self.do_wallace() except SystemExit as errmsg: exitcode = errmsg except KeyboardInterrupt: exitcode = 1 log.info(_l("Interrupted by user")) except AttributeError: exitcode = 1 traceback.print_exc() print(_l("Traceback occurred, please report a bug.")) except TypeError as errmsg: exitcode = 1 traceback.print_exc() log.error(_l("Type Error: %s") % errmsg) except Exception: exitcode = 2 traceback.print_exc() print(_l("Traceback occurred, please report a bug.")) sys.exit(exitcode) def set_signal_handlers(self): import signal signal.signal(signal.SIGHUP, self.reload_config) signal.signal(signal.SIGTERM, self.remove_pid) def write_pid(self): pid = os.getpid() if os.access(os.path.dirname(conf.pidfile), os.W_OK): fp = open(conf.pidfile, 'w') fp.write("%d\n" % (pid)) fp.close() else: print(_l("Could not write pid file %s") % (conf.pidfile)) diff --git a/wallace/module_footer.py b/wallace/module_footer.py index 2833a59..96e8cf6 100644 --- a/wallace/module_footer.py +++ b/wallace/module_footer.py @@ -1,185 +1,185 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import re import tempfile import time from email import message_from_file from email.encoders import encode_quopri -import modules +from . import modules import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/footer') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/footer/' def __init__(): modules.register('footer', execute, description=description()) def description(): return """Append a footer to messages.""" def append_footer(content, footer, position=None, isHtml=False): if (isHtml): append = "\n\n" + footer + "\n\n" if position == 'top': match = re.search('(]*>)', content, re.IGNORECASE | re.DOTALL) if match: content = content.replace(match.group(0), match.group(0) + append) else: content = "" + append + content + "" else: match = re.search('()', content, re.IGNORECASE | re.DOTALL) if match: content = content.replace(match.group(0), append + match.group(0)) else: content = "" + content + append + "" else: if position == 'top': content = footer + "\n\n" + content else: content += "\n\n-- \n" + footer return content def set_part_content(part, content): # Reset old encoding and use quoted-printable (#5414) del part['Content-Transfer-Encoding'] part.set_payload(content) encode_quopri(part) return True def execute(*args, **kw): global extra_log_params # TODO: Test for correct call. filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT']: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'],'optout',filepath)) return log.debug(_("Executing module footer for %r, %r") % (args, kw), level=8) new_filepath = os.path.join('/var/spool/pykolab/wallace/footer/incoming', os.path.basename(filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse message message = message_from_file(open(filepath, 'r')) # Possible footer answers are limited to ACCEPT only answers = [ 'ACCEPT' ] footer = {} footer_position = conf.get('wallace', 'footer_position') footer_html_file = conf.get('wallace', 'footer_html') footer_text_file = conf.get('wallace', 'footer_text') if not os.path.isfile(footer_text_file) and not os.path.isfile(footer_html_file): log.warning(_("No contents configured for footer module")) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', filepath)) return if os.path.isfile(footer_text_file): footer['plain'] = open(footer_text_file, 'r').read() if not os.path.isfile(footer_html_file): footer['html'] = '

' + footer['plain'] + '

' else: footer['html'] = open(footer_html_file, 'r').read() if footer['html'] == "": footer['html'] = '

' + footer['plain'] + '

' if footer['plain'] == "" and footer['html'] == "

": exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', filepath)) return footer_added = False try: _footer_added = message.get("X-Wallace-Footer") except: pass if _footer_added == "YES": exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', filepath)) return for part in message.walk(): disposition = None try: content_type = part.get_content_type() except: continue try: disposition = part.get("Content-Disposition") except: pass log.debug("Walking message part: %s; disposition = %r" % (content_type, disposition), level=8) if disposition is not None: continue if content_type == "text/plain": content = part.get_payload(decode=True) content = append_footer(content, footer['plain'], footer_position, False) footer_added = set_part_content(part, content) elif content_type == "text/html": content = part.get_payload(decode=True) content = append_footer(content, footer['html'], footer_position, True) footer_added = set_part_content(part, content) if footer_added: log.debug("Footer attached.") message.add_header("X-Wallace-Footer", "YES") (fp, new_filepath) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/footer/ACCEPT") os.write(fp, message.as_string()) os.close(fp) os.unlink(filepath) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','footer', new_filepath)) diff --git a/wallace/module_gpgencrypt.py b/wallace/module_gpgencrypt.py index 9083055..e98bf0d 100644 --- a/wallace/module_gpgencrypt.py +++ b/wallace/module_gpgencrypt.py @@ -1,293 +1,293 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; version 3 or, at your option, any later version # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Library General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # import os import tempfile import time from email import message_from_string from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.parser import Parser from email.utils import formataddr from email.utils import getaddresses import email.mime.application import email.mime.multipart import email.mime.text import email.encoders import gnupg -import modules +from . import modules import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/gpgencrypt') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/gpgencrypt/' def __init__(): modules.register('gpgencrypt', execute, description=description()) def description(): return """Encrypt messages to the recipient(s).""" def pgp_mime(msg, recepients): gpg = gnupg.GPG(gnupghome='/var/lib/kolab/.gnupg', verbose=conf.debuglevel > 8) gpg.encoding = 'utf-8' msg = msg msg_boundary = str(msg.get_boundary()) msg_content_type = str(msg.get_content_type()) payload = msg.get_payload() content = "Content-Type: " + msg_content_type + ";" + "\n boundary=\"" + msg_boundary + "\"\n\n" + payload encrypted_content = gpg.encrypt(content, recepients, always_trust=True) msg.set_type("multipart/encrypted") msg.set_param("protocol","application/pgp-encrypted") msg_boundary_gpg = "--boundary-gpg-encryption-42" msg_preamble = "This is an OpenPGP/MIME encrypted message (RFC 4880 and 3156)\n\ " + msg_boundary_gpg + "\n\ Content-Type: application/pgp-encrypted\n\ Content-Description: PGP/MIME version identification\n\ \n\ Version: 1\n\ \n\ " + msg_boundary_gpg + "\n\ Content-Type: application/octet-stream; name=\"encrypted.asc\"\n\ Content-Description: OpenPGP encrypted message\n\ Content-Disposition: inline; filename=\"encrypted.asc\"\n\n" msg.set_boundary(msg_boundary_gpg) msg.set_payload(msg_preamble + str(encrypted_content) + "\n" + msg_boundary_gpg) return msg def execute(*args, **kw): global extra_log_params # TODO: Test for correct call. filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'],'gpgencrypt',filepath)) log.debug(_("Executing module gpgencrypt for %r, %r") % (args, kw), level=8) new_filepath = os.path.join('/var/spool/pykolab/wallace/gpgencrypt/incoming', os.path.basename(filepath)) if not filepath == new_filepath: log.debug("Renaming %r to %r" % (filepath, new_filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse message headers # @TODO: make sure we can use True as the 2nd argument here message = Parser().parse(open(filepath, 'r'), True) # Possible gpgencrypt answers are limited to ACCEPT only answers = [ 'ACCEPT' ] # from Mail::GnuPG.is_encrypted # #sub is_encrypted { # my ($self,$entity) = @_; # return 1 # if (($entity->effective_type =~ m!multipart/encrypted!) # || # ($entity->as_string =~ m!^-----BEGIN PGP MESSAGE-----!m)); # return 0; #} message_already_encrypted = False for part in message.walk(): if part.get_content_type() in [ "application/pgp-encrypted" ]: message_already_encrypted = True log.debug(_("Message is already encrypted (app/pgp-enc content-type)"), level=8) if message.get_content_type() in [ "multipart/encrypted" ]: message_already_encrypted = True log.debug(_("Message already encrypted by main content-type header"), level=8) if message_already_encrypted: return filepath try: # What are recipient addresses to encrypt to (bitmask)? # 1 - organization key # 2 - envelope to # 4 - to # 8 - cc # 16 - resent-to # 32 - resent-cc encrypt_to_rcpts = conf.get('wallace', 'gpgencrypt_to_rcpts') if encrypt_to_rcpts == None: encrypt_to_rcpts = 14 else: encrypt_to_rcpts = (int)(encrypt_to_rcpts) # Only encrypt to keys that are trusted strict_crypt = conf.get('wallace', 'gpgencrypt_strict') if strict_crypt == None: strict_crypt = False # Organization key ID if encrypt_to_rcpts & 1: encrypt_to_org = conf.get('wallace', 'gpgencrypt_to_org_key') if encrypt_to_org == None and encrypt_to_rcpts & 1: if strict_crypt: log.error(_("Configured to encrypt to a key not configured, and strict policy enabled. Bailing out.")) modules.cb_action_REJECT('gpgencrypt',filepath) else: log.error(_("Configured to encrypt to a key not configured, but continuing anyway (see 'gpgencrypt_strict').")) else: encrypt_to_org = [] # Bounce the message if encryption fails? force_crypt = conf.get('wallace', 'gpgencrypt_force') if force_crypt == None: force_crypt = False # Retrieve keys from remote server(s) automatically? retrieve_keys = conf.get('wallace', 'gpgencrypt_retrieve_keys') if retrieve_keys == None: retrieve_keys = False if retrieve_keys: gpgserver = conf.get('wallace', 'gpgencrypt_server') if gpgserver == None: gpgserver = 'pgp.mit.edu' encrypt_to = [] if encrypt_to_rcpts & 2: encrypt_to.extend(message.get_all('X-Kolab-To', [])) if encrypt_to_rcpts & 4: encrypt_to.extend(message.get_all('to', [])) if encrypt_to_rcpts & 8: encrypt_to.extend(message.get_all('cc', [])) if encrypt_to_rcpts & 16: encrypt_to.extend(message.get_all('resent-to', [])) if encrypt_to_rcpts & 32: encrypt_to.extend(message.get_all('resent-cc', [])) recipients = [address for displayname,address in getaddresses(encrypt_to)] log.debug(_("Recipients: %r") % (recipients)) # Split between recipients we can encrypt for/to, and ones we can not encrypt_rcpts = [] nocrypt_rcpts = [] gpg = gnupg.GPG(gnupghome='/var/lib/kolab/.gnupg', verbose=conf.debuglevel > 8) gpg.encoding = 'utf-8' local_keys = gpg.list_keys() log.debug(_("Current keys: %r") % (local_keys), level=8) for recipient in recipients: key_local = False log.debug(_("Retrieving key for recipient: %r") % (recipient)) for key in local_keys: for address in [x for x in [address for displayname,address in getaddresses(key['uids'])] if x == recipient]: log.debug(_("Found matching address %r") % (address)) key_local = key['keyid'] if key_local == False: if retrieve_keys: remote_keys = gpg.search_keys(recipient, gpgserver) if len(remote_keys) == 1: for address in [x for x in [address for displayname,address in getaddresses(remote_keys[0]['uids'])] if x == recipient]: log.debug(_("Found matching address %r in remote keys") % (address)) gpg.recv_keys(gpgserver, remote_keys[0]['keyid']) local_keys = gpg.list_keys() else: nocrypt_rcpts.append(recipient) for key in local_keys: for address in [x for x in [address for displayname,address in getaddresses(key['uids'])] if x == recipient]: log.debug(_("Found matching address %r") % (address)) key_local = key['keyid'] if not key_local == False: encrypt_rcpts.append(key_local) payload = message.get_payload() #print "payload:", payload if len(encrypt_rcpts) < 1: return filepath if "multipart" in message.get_content_type(): log.debug(_("Mime Message - we need to build multipart/encrypted structure"), level=8) msg = message enc_mime_message = pgp_mime(msg, encrypt_rcpts) message = enc_mime_message else: log.debug(_("No Mime Message - encypt plain"), level=8) encrypted_data = gpg.encrypt(payload, encrypt_rcpts, always_trust=True) encrypted_string = str(encrypted_data) message.set_payload(encrypted_string) message.add_header('X-wallace-gpg-encrypted', 'true') (fp, new_filepath) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/gpgencrypt/ACCEPT") os.write(fp, message.as_string()) os.close(fp) os.unlink(filepath) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','gpgencrypt', new_filepath)) except Exception as errmsg: log.error(_("An error occurred: %r") % (errmsg)) if conf.debuglevel > 8: import traceback traceback.print_exc() diff --git a/wallace/module_invitationpolicy.py b/wallace/module_invitationpolicy.py index 38dfc70..da6eec9 100644 --- a/wallace/module_invitationpolicy.py +++ b/wallace/module_invitationpolicy.py @@ -1,1484 +1,1484 @@ # -*- coding: utf-8 -*- # Copyright 2014 Kolab Systems AG (http://www.kolabsys.com) # # Thomas Bruederli (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from six import string_types import datetime import os import random import signal import tempfile import time try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse import urllib import hashlib import traceback import re from email import message_from_string from email.parser import Parser from email.utils import formataddr from email.utils import getaddresses -import modules +from . import modules import pykolab import kolabformat from pykolab import utils from pykolab.auth import Auth from pykolab.conf import Conf from pykolab.imap import IMAP from pykolab.xml import to_dt from pykolab.xml import utils as xmlutils from pykolab.xml import todo_from_message from pykolab.xml import event_from_message from pykolab.xml import participant_status_label from pykolab.itip import objects_from_message from pykolab.itip import check_event_conflict from pykolab.itip import send_reply from pykolab.translate import _ # define some contstants used in the code below ACT_MANUAL = 1 ACT_ACCEPT = 2 ACT_DELEGATE = 4 ACT_REJECT = 8 ACT_UPDATE = 16 ACT_CANCEL_DELETE = 32 ACT_SAVE_TO_FOLDER = 64 COND_IF_AVAILABLE = 128 COND_IF_CONFLICT = 256 COND_TENTATIVE = 512 COND_NOTIFY = 1024 COND_FORWARD = 2048 COND_TYPE_EVENT = 4096 COND_TYPE_TASK = 8192 COND_TYPE_ALL = COND_TYPE_EVENT + COND_TYPE_TASK ACT_TENTATIVE = ACT_ACCEPT + COND_TENTATIVE ACT_UPDATE_AND_NOTIFY = ACT_UPDATE + COND_NOTIFY ACT_SAVE_AND_FORWARD = ACT_SAVE_TO_FOLDER + COND_FORWARD ACT_CANCEL_DELETE_AND_NOTIFY = ACT_CANCEL_DELETE + COND_NOTIFY FOLDER_TYPE_ANNOTATION = '/vendor/kolab/folder-type' MESSAGE_PROCESSED = 1 MESSAGE_FORWARD = 2 policy_name_map = { # policy values applying to all object types 'ALL_MANUAL': ACT_MANUAL + COND_TYPE_ALL, 'ALL_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL, 'ALL_REJECT': ACT_REJECT + COND_TYPE_ALL, 'ALL_DELEGATE': ACT_DELEGATE + COND_TYPE_ALL, # not implemented 'ALL_UPDATE': ACT_UPDATE + COND_TYPE_ALL, 'ALL_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL, 'ALL_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_ALL, 'ALL_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_ALL, 'ALL_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_ALL, 'ALL_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL, # event related policy values 'EVENT_MANUAL': ACT_MANUAL + COND_TYPE_EVENT, 'EVENT_ACCEPT': ACT_ACCEPT + COND_TYPE_EVENT, 'EVENT_TENTATIVE': ACT_TENTATIVE + COND_TYPE_EVENT, 'EVENT_REJECT': ACT_REJECT + COND_TYPE_EVENT, 'EVENT_DELEGATE': ACT_DELEGATE + COND_TYPE_EVENT, # not implemented 'EVENT_UPDATE': ACT_UPDATE + COND_TYPE_EVENT, 'EVENT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_EVENT, 'EVENT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'EVENT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'EVENT_DELEGATE_IF_CONFLICT': ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT, 'EVENT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT, 'EVENT_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT, 'EVENT_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT, 'EVENT_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_EVENT, 'EVENT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_EVENT, # task related policy values 'TASK_MANUAL': ACT_MANUAL + COND_TYPE_TASK, 'TASK_ACCEPT': ACT_ACCEPT + COND_TYPE_TASK, 'TASK_REJECT': ACT_REJECT + COND_TYPE_TASK, 'TASK_DELEGATE': ACT_DELEGATE + COND_TYPE_TASK, # not implemented 'TASK_UPDATE': ACT_UPDATE + COND_TYPE_TASK, 'TASK_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_TASK, 'TASK_SAVE_TO_FOLDER': ACT_SAVE_TO_FOLDER + COND_TYPE_TASK, 'TASK_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_TASK, 'TASK_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_TASK, 'TASK_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_TASK, # legacy values 'ACT_MANUAL': ACT_MANUAL + COND_TYPE_ALL, 'ACT_ACCEPT': ACT_ACCEPT + COND_TYPE_ALL, 'ACT_ACCEPT_IF_NO_CONFLICT': ACT_ACCEPT + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'ACT_TENTATIVE': ACT_TENTATIVE + COND_TYPE_EVENT, 'ACT_TENTATIVE_IF_NO_CONFLICT': ACT_ACCEPT + COND_TENTATIVE + COND_IF_AVAILABLE + COND_TYPE_EVENT, 'ACT_DELEGATE': ACT_DELEGATE + COND_TYPE_ALL, 'ACT_DELEGATE_IF_CONFLICT': ACT_DELEGATE + COND_IF_CONFLICT + COND_TYPE_EVENT, 'ACT_REJECT': ACT_REJECT + COND_TYPE_ALL, 'ACT_REJECT_IF_CONFLICT': ACT_REJECT + COND_IF_CONFLICT + COND_TYPE_EVENT, 'ACT_UPDATE': ACT_UPDATE + COND_TYPE_ALL, 'ACT_UPDATE_AND_NOTIFY': ACT_UPDATE_AND_NOTIFY + COND_TYPE_ALL, 'ACT_CANCEL_DELETE': ACT_CANCEL_DELETE + COND_TYPE_ALL, 'ACT_CANCEL_DELETE_AND_NOTIFY': ACT_CANCEL_DELETE_AND_NOTIFY + COND_TYPE_ALL, 'ACT_SAVE_TO_CALENDAR': ACT_SAVE_TO_FOLDER + COND_TYPE_EVENT, 'ACT_SAVE_AND_FORWARD': ACT_SAVE_AND_FORWARD + COND_TYPE_EVENT, } policy_value_map = dict([(v &~ COND_TYPE_ALL, k) for (k, v) in policy_name_map.items()]) object_type_conditons = { 'event': COND_TYPE_EVENT, 'task': COND_TYPE_TASK } log = pykolab.getLogger('pykolab.wallace/invitationpolicy') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/invitationpolicy/' auth = None imap = None write_locks = [] def __init__(): modules.register('invitationpolicy', execute, description=description()) def accept(filepath): new_filepath = os.path.join( mybasepath, 'ACCEPT', os.path.basename(filepath) ) cleanup() os.rename(filepath, new_filepath) filepath = new_filepath exec('modules.cb_action_ACCEPT(%r, %r)' % ('invitationpolicy',filepath)) def reject(filepath): new_filepath = os.path.join( mybasepath, 'REJECT', os.path.basename(filepath) ) os.rename(filepath, new_filepath) filepath = new_filepath exec('modules.cb_action_REJECT(%r, %r)' % ('invitationpolicy',filepath)) def description(): return """Invitation policy execution module.""" def cleanup(): global auth, imap, write_locks, extra_log_params log.debug("cleanup(): %r, %r" % (auth, imap), level=8) extra_log_params['qid'] = '-' auth.disconnect() del auth # Disconnect IMAP or we lock the mailbox almost constantly imap.disconnect() del imap # remove remaining write locks for key in write_locks: remove_write_lock(key, False) def execute(*args, **kw): global auth, imap, extra_log_params filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) # (re)set language to default pykolab.translate.setUserLanguage(conf.get('kolab','default_locale')) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER', 'locks']: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) log.debug(_("Invitation policy called for %r, %r") % (args, kw), level=8) auth = Auth() imap = IMAP() # ignore calls on lock files if '/locks/' in filepath or 'stage' in kw and kw['stage'] == 'locks': return False log.debug("Invitation policy executing for %r, %r" % (filepath, '/locks/' in filepath), level=8) if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec( 'modules.cb_action_%s(%r, %r)' % ( kw['stage'], 'invitationpolicy', filepath ) ) return filepath else: # Move to incoming new_filepath = os.path.join( mybasepath, 'incoming', os.path.basename(filepath) ) if not filepath == new_filepath: log.debug("Renaming %r to %r" % (filepath, new_filepath)) os.rename(filepath, new_filepath) filepath = new_filepath # parse full message message = Parser().parse(open(filepath, 'r')) # invalid message, skip if not message.get('X-Kolab-To'): return filepath recipients = [address for displayname,address in getaddresses(message.get_all('X-Kolab-To'))] sender_email = [address for displayname,address in getaddresses(message.get_all('X-Kolab-From'))][0] any_itips = False recipient_email = None recipient_emails = [] recipient_user_dn = None # An iTip message may contain multiple events. Later on, test if the message # is an iTip message by checking the length of this list. try: itip_events = objects_from_message(message, ['VEVENT','VTODO'], ['REQUEST', 'REPLY', 'CANCEL']) except Exception as errmsg: log.error(_("Failed to parse iTip objects from message: %r" % (errmsg))) itip_events = [] if not len(itip_events) > 0: log.info(_("Message is not an iTip message or does not contain any (valid) iTip objects.")) else: any_itips = True log.debug(_("iTip objects attached to this message contain the following information: %r") % (itip_events), level=8) # See if any iTip actually allocates a user. if any_itips and len([x['uid'] for x in itip_events if 'attendees' in x or 'organizer' in x]) > 0: auth.connect() # we're looking at the first itip object itip_event = itip_events[0] for recipient in recipients: recipient_user_dn = user_dn_from_email_address(recipient) if recipient_user_dn: receiving_user = auth.get_entry_attributes(None, recipient_user_dn, ['*']) recipient_emails = auth.extract_recipient_addresses(receiving_user) recipient_email = recipient # extend with addresses from delegators # (only do this lookup for REPLY messages) receiving_user['_delegated_mailboxes'] = [] if itip_event['method'] == 'REPLY': for _delegator in auth.list_delegators(recipient_user_dn): if not _delegator['_mailbox_basename'] == None: receiving_user['_delegated_mailboxes'].append( _delegator['_mailbox_basename'].split('@')[0] ) log.debug(_("Recipient emails for %s: %r") % (recipient_user_dn, recipient_emails), level=8) break if not any_itips: log.debug(_("No itips, no users, pass along %r") % (filepath), level=5) return filepath elif recipient_email is None: log.debug(_("iTips, but no users, pass along %r") % (filepath), level=5) return filepath # for replies, the organizer is the recipient if itip_event['method'] == 'REPLY': # Outlook can send iTip replies without an organizer property if 'organizer' in itip_event: organizer_mailto = str(itip_event['organizer']).split(':')[-1] user_attendees = [organizer_mailto] if organizer_mailto in recipient_emails else [] else: user_attendees = [recipient_email] else: # Limit the attendees to the one that is actually invited with the current message. attendees = [str(a).split(':')[-1] for a in (itip_event['attendees'] if 'attendees' in itip_event else [])] user_attendees = [a for a in attendees if a in recipient_emails] if 'organizer' in itip_event: sender_email = itip_event['xml'].get_organizer().email() # abort if no attendee matches the envelope recipient if len(user_attendees) == 0: log.info(_("No user attendee matching envelope recipient %s, skip message") % (recipient_email)) return filepath log.debug(_("Receiving user: %r") % (receiving_user), level=8) # set recipient_email to the matching attendee mailto: address recipient_email = user_attendees[0] # change gettext language to the preferredlanguage setting of the receiving user if 'preferredlanguage' in receiving_user: pykolab.translate.setUserLanguage(receiving_user['preferredlanguage']) # find user's kolabInvitationPolicy settings and the matching policy values type_condition = object_type_conditons.get(itip_event['type'], COND_TYPE_ALL) policies = get_matching_invitation_policies(receiving_user, sender_email, type_condition) # select a processing function according to the iTip request method method_processing_map = { 'REQUEST': process_itip_request, 'REPLY': process_itip_reply, 'CANCEL': process_itip_cancel } done = None if itip_event['method'] in method_processing_map: processor_func = method_processing_map[itip_event['method']] # connect as cyrus-admin imap.connect() for policy in policies: log.debug(_("Apply invitation policy %r for sender %r") % (policy_value_map[policy], sender_email), level=8) done = processor_func(itip_event, policy, recipient_email, sender_email, receiving_user) # matching policy found if done is not None: break # remove possible write lock from this iteration remove_write_lock(get_lock_key(receiving_user, itip_event['uid'])) else: log.debug(_("Ignoring '%s' iTip method") % (itip_event['method']), level=8) # message has been processed by the module, remove it if done == MESSAGE_PROCESSED: log.debug(_("iTip message %r consumed by the invitationpolicy module") % (message.get('Message-ID')), level=5) os.unlink(filepath) cleanup() return None # accept message into the destination inbox accept(filepath) def process_itip_request(itip_event, policy, recipient_email, sender_email, receiving_user): """ Process an iTip REQUEST message according to the given policy """ # if invitation policy is set to MANUAL, pass message along if policy & ACT_MANUAL: log.info(_("Pass invitation for manual processing")) return MESSAGE_FORWARD try: receiving_attendee = itip_event['xml'].get_attendee_by_email(recipient_email) log.debug(_("Receiving attendee: %r") % (receiving_attendee.to_dict()), level=8) except Exception as errmsg: log.error("Could not find envelope attendee: %r" % (errmsg)) return MESSAGE_FORWARD # process request to participating attendees with RSVP=TRUE or PARTSTAT=NEEDS-ACTION is_task = itip_event['type'] == 'task' nonpart = receiving_attendee.get_role() == kolabformat.NonParticipant partstat = receiving_attendee.get_participant_status() save_object = not nonpart or not partstat == kolabformat.PartNeedsAction rsvp = receiving_attendee.get_rsvp() scheduling_required = rsvp or partstat == kolabformat.PartNeedsAction respond_with = receiving_attendee.get_participant_status(True) condition_fulfilled = True # find existing event in user's calendar (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True) # compare sequence number to determine a (re-)scheduling request if existing is not None: scheduling_required = itip_event['sequence'] > 0 and itip_event['sequence'] > existing.get_sequence() log.debug(_("Scheduling required: %r, for existing %s: %s") % (scheduling_required, existing.type, existing.get_uid()), level=8) save_object = True # if scheduling: check availability (skip that for tasks) if scheduling_required: if not is_task and policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): condition_fulfilled = check_availability(itip_event, receiving_user) if not is_task and policy & COND_IF_CONFLICT: condition_fulfilled = not condition_fulfilled log.debug(_("Precondition for object %r fulfilled: %r") % (itip_event['uid'], condition_fulfilled), level=5) if existing: respond_with = None if policy & ACT_ACCEPT and condition_fulfilled: respond_with = 'TENTATIVE' if policy & COND_TENTATIVE else 'ACCEPTED' elif policy & ACT_REJECT and condition_fulfilled: respond_with = 'DECLINED' # TODO: only save declined invitation when a certain config option is set? elif policy & ACT_DELEGATE and condition_fulfilled: # TODO: delegate (but to whom?) return None # auto-update changes if enabled for this user elif policy & ACT_UPDATE and existing: # compare sequence number to avoid outdated updates if not itip_event['sequence'] == existing.get_sequence(): log.info(_("The iTip request sequence (%r) doesn't match the referred object version (%r). Ignoring.") % ( itip_event['sequence'], existing.get_sequence() )) return None log.debug(_("Auto-updating %s %r on iTip REQUEST (no re-scheduling)") % (existing.type, existing.uid), level=8) save_object = True rsvp = False # retain task status and percent-complete properties from my old copy if is_task: itip_event['xml'].set_status(existing.get_status()) itip_event['xml'].set_percentcomplete(existing.get_percentcomplete()) if policy & COND_NOTIFY: sender = itip_event['xml'].get_organizer() comment = itip_event['xml'].get_comment() send_update_notification(itip_event['xml'], receiving_user, existing, False, sender, comment) # if RSVP, send an iTip REPLY if rsvp or scheduling_required: # set attendee's CN from LDAP record if yet missing if not receiving_attendee.get_name() and 'cn' in receiving_user: receiving_attendee.set_name(receiving_user['cn']) # send iTip reply if respond_with is not None and not respond_with == 'NEEDS-ACTION': receiving_attendee.set_participant_status(respond_with) send_reply(recipient_email, itip_event, invitation_response_text(itip_event['type']), subject=_('"%(summary)s" has been %(status)s')) elif policy & ACT_SAVE_TO_FOLDER: # copy the invitation into the user's default folder with PARTSTAT=NEEDS-ACTION itip_event['xml'].set_attendee_participant_status(receiving_attendee, respond_with or 'NEEDS-ACTION') save_object = True else: # policy doesn't match, pass on to next one return None if save_object: targetfolder = None # delete old version from IMAP if existing: targetfolder = existing._imap_folder delete_object(existing) elif master and hasattr(master, '_imap_folder'): targetfolder = master._imap_folder delete_object(master) if not nonpart or existing: # save new copy from iTip if store_object(itip_event['xml'], receiving_user, targetfolder, master): if policy & COND_FORWARD: log.debug(_("Forward invitation for notification"), level=5) return MESSAGE_FORWARD else: return MESSAGE_PROCESSED return None def process_itip_reply(itip_event, policy, recipient_email, sender_email, receiving_user): """ Process an iTip REPLY message according to the given policy """ # if invitation policy is set to MANUAL, pass message along if policy & ACT_MANUAL: log.info(_("Pass reply for manual processing")) return MESSAGE_FORWARD # auto-update is enabled for this user if policy & ACT_UPDATE: try: sender_attendee = itip_event['xml'].get_attendee_by_email(sender_email) log.debug(_("Sender Attendee: %r") % (sender_attendee), level=8) except Exception as errmsg: log.error("Could not find envelope sender attendee: %r" % (errmsg)) return MESSAGE_FORWARD # find existing event in user's calendar # sets/checks lock to avoid concurrent wallace processes trying to update the same event simultaneously (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True) if existing: # compare sequence number to avoid outdated replies? if not itip_event['sequence'] == existing.get_sequence(): log.info(_("The iTip reply sequence (%r) doesn't match the referred object version (%r). Forwarding to Inbox.") % ( itip_event['sequence'], existing.get_sequence() )) remove_write_lock(existing._lock_key) return MESSAGE_FORWARD log.debug(_("Auto-updating %s %r on iTip REPLY") % (existing.type, existing.uid), level=8) updated_attendees = [] try: existing.set_attendee_participant_status(sender_email, sender_attendee.get_participant_status(), rsvp=False) existing_attendee = existing.get_attendee(sender_email) updated_attendees.append(existing_attendee) except Exception as errmsg: log.error("Could not find corresponding attende in organizer's copy: %r" % (errmsg)) # append delegated-from attendee ? if len(sender_attendee.get_delegated_from()) > 0: existing.add_attendee(sender_attendee) updated_attendees.append(sender_attendee) else: # TODO: accept new participant if ACT_ACCEPT ? remove_write_lock(existing._lock_key) return MESSAGE_FORWARD # append delegated-to attendee if len(sender_attendee.get_delegated_to()) > 0: try: delegatee_email = sender_attendee.get_delegated_to(True)[0] sender_delegatee = itip_event['xml'].get_attendee_by_email(delegatee_email) existing_delegatee = existing.find_attendee(delegatee_email) if not existing_delegatee: existing.add_attendee(sender_delegatee) log.debug(_("Add delegatee: %r") % (sender_delegatee.to_dict()), level=8) else: existing_delegatee.copy_from(sender_delegatee) log.debug(_("Update existing delegatee: %r") % (existing_delegatee.to_dict()), level=8) updated_attendees.append(sender_delegatee) # copy all parameters from replying attendee (e.g. delegated-to, role, etc.) existing_attendee.copy_from(sender_attendee) existing.update_attendees([existing_attendee]) log.debug(_("Update delegator: %r") % (existing_attendee.to_dict()), level=8) except Exception as errmsg: log.error("Could not find delegated-to attendee: %r" % (errmsg)) # update the organizer's copy of the object if update_object(existing, receiving_user, master): if policy & COND_NOTIFY: send_update_notification(existing, receiving_user, existing, True, sender_attendee, itip_event['xml'].get_comment()) # update all other attendee's copies if conf.get('wallace','invitationpolicy_autoupdate_other_attendees_on_reply'): propagate_changes_to_attendees_accounts(existing, updated_attendees) return MESSAGE_PROCESSED else: log.error(_("The object referred by this reply was not found in the user's folders. Forwarding to Inbox.")) return MESSAGE_FORWARD return None def process_itip_cancel(itip_event, policy, recipient_email, sender_email, receiving_user): """ Process an iTip CANCEL message according to the given policy """ # if invitation policy is set to MANUAL, pass message along if policy & ACT_MANUAL: log.info(_("Pass cancellation for manual processing")) return MESSAGE_FORWARD # auto-update the local copy if policy & ACT_UPDATE or policy & ACT_CANCEL_DELETE: # find existing object in user's folders (existing, master) = find_existing_object(itip_event['uid'], itip_event['type'], itip_event['recurrence-id'], receiving_user, True) remove_object = policy & ACT_CANCEL_DELETE if existing: # on this-and-future cancel requests, set the recurrence until date on the master event if itip_event['recurrence-id'] and master and itip_event['xml'].get_thisandfuture(): rrule = master.get_recurrence() rrule.set_count(0) rrule.set_until(existing.get_start() + datetime.timedelta(days=-1)) master.set_recurrence(rrule) existing.set_recurrence_id(existing.get_recurrence_id(), True) remove_object = False # delete the local copy if remove_object: # remove exception and register an exdate to the main event if master: log.debug(_("Remove cancelled %s instance %s from %r") % (existing.type, itip_event['recurrence-id'], existing.uid), level=8) master.add_exception_date(existing.get_start()) master.del_exception(existing) success = update_object(master, receiving_user) # delete main event else: success = delete_object(existing) # update the local copy with STATUS=CANCELLED else: log.debug(_("Update cancelled %s %r with STATUS=CANCELLED") % (existing.type, existing.uid), level=8) existing.set_status('CANCELLED') existing.set_transparency(True) success = update_object(existing, receiving_user, master) if success: # send cancellation notification if policy & COND_NOTIFY: sender = itip_event['xml'].get_organizer() comment = itip_event['xml'].get_comment() send_cancel_notification(existing, receiving_user, remove_object, sender, comment) return MESSAGE_PROCESSED else: log.error(_("The object referred by this cancel request was not found in the user's folders. Forwarding to Inbox.")) return MESSAGE_FORWARD return None def user_dn_from_email_address(email_address): """ Resolves the given email address to a Kolab user entity """ global auth if not auth: auth = Auth() auth.connect() # return cached value if email_address in user_dn_from_email_address.cache: return user_dn_from_email_address.cache[email_address] local_domains = auth.list_domains() if local_domains is not None: local_domains = list(set(local_domains.keys())) if not email_address.split('@')[1] in local_domains: user_dn_from_email_address.cache[email_address] = None return None log.debug(_("Checking if email address %r belongs to a local user") % (email_address), level=8) user_dn = auth.find_user_dn(email_address, True) if isinstance(user_dn, string_types): log.debug(_("User DN: %r") % (user_dn), level=8) else: log.debug(_("No user record(s) found for %r") % (email_address), level=8) # remember this lookup user_dn_from_email_address.cache[email_address] = user_dn return user_dn user_dn_from_email_address.cache = {} def get_matching_invitation_policies(receiving_user, sender_email, type_condition=COND_TYPE_ALL): # get user's kolabInvitationPolicy settings policies = receiving_user['kolabinvitationpolicy'] if 'kolabinvitationpolicy' in receiving_user else [] if policies and not isinstance(policies, list): policies = [policies] if len(policies) == 0: policies = conf.get_list('wallace', 'kolab_invitation_policy') # match policies agains the given sender_email matches = [] for p in policies: if ':' in p: (value, domain) = p.split(':', 1) else: value = p domain = '' if domain == '' or domain == '*' or str(sender_email).endswith(domain): value = value.upper() if value in policy_name_map: val = policy_name_map[value] # append if type condition matches if val & type_condition: matches.append(val &~ COND_TYPE_ALL) # add manual as default action if len(matches) == 0: matches.append(ACT_MANUAL) return matches def imap_proxy_auth(user_rec): """ Perform IMAP login using proxy authentication with admin credentials """ global imap mail_attribute = conf.get('cyrus-sasl', 'result_attribute') if mail_attribute is None: mail_attribute = 'mail' mail_attribute = mail_attribute.lower() if mail_attribute not in user_rec: log.error(_("User record doesn't have the mailbox attribute %r set" % (mail_attribute))) return False # do IMAP prox auth with the given user backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') try: imap.disconnect() imap.connect(login=False) imap.login_plain(admin_login, admin_password, user_rec[mail_attribute]) except Exception as errmsg: log.error(_("IMAP proxy authentication failed: %r") % (errmsg)) return False return True def list_user_folders(user_rec, _type): """ Get a list of the given user's private calendar/tasks folders """ global imap # return cached list if '_imap_folders' in user_rec: return user_rec['_imap_folders'] result = [] if not imap_proxy_auth(user_rec): return result folders = imap.get_metadata('*') log.debug( _("List %r folders for user %r: %r") % ( _type, user_rec['mail'], folders ), level=8 ) (ns_personal, ns_other, ns_shared) = imap.namespaces() _folders = {} # Filter the folders by type relevance for folder, metadata in folders.items(): key = '/shared' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): _folders[folder] = metadata key = '/private' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): _folders[folder] = metadata for folder, metadata in _folders.items(): folder_delegated = False # Exclude shared and other user's namespace # # First, test if this is another users folder if ns_other is not None and folder.startswith(ns_other): # If we have no delegated mailboxes, we can skip this entirely if '_delegated_mailboxes' not in user_rec: continue for _m in user_rec['_delegated_mailboxes']: if folder.startswith(ns_other + _m + '/'): folder_delegated = True if not folder_delegated: continue # TODO: list shared folders the user has write privileges ? if ns_shared is not None: if len([_ns for _ns in ns_shared if folder.startswith(_ns)]) > 0: continue key = '/shared' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): result.append(folder) key = '/private' + FOLDER_TYPE_ANNOTATION if key in metadata: if metadata[key].startswith(_type): result.append(folder) # store default folder in user record if metadata[key].endswith('.default'): user_rec['_default_folder'] = folder continue # store private and confidential folders in user record if metadata[key].endswith('.confidential'): if '_confidential_folder' not in user_rec: user_rec['_confidential_folder'] = folder continue if metadata[key].endswith('.private'): if '_private_folder' not in user_rec: user_rec['_private_folder'] = folder continue # cache with user record user_rec['_imap_folders'] = result return result def find_existing_object(uid, type, recurrence_id, user_rec, lock=False): """ Search user's private folders for the given object (by UID+type) """ global imap lock_key = None if lock: lock_key = get_lock_key(user_rec, uid) set_write_lock(lock_key) event = None master = None for folder in list_user_folders(user_rec, type): log.debug(_("Searching folder %r for %s %r") % (folder, type, uid), level=8) imap.imap.m.select(imap.folder_utf7(folder)) res, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) for num in reversed(data[0].split()): res, data = imap.imap.m.fetch(num, '(UID RFC822)') try: msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1) except Exception: log.error(_("No UID found in IMAP response: %r") % (data[0][0])) continue try: if type == 'task': event = todo_from_message(message_from_string(data[0][1])) else: event = event_from_message(message_from_string(data[0][1])) # find instance in a recurring series if recurrence_id and (event.is_recurring() or event.has_exceptions() or event.get_recurrence_id()): master = event event = master.get_instance(recurrence_id) setattr(master, '_imap_folder', folder) setattr(master, '_msguid', msguid) # return master, even if instance is not found if not event and master.uid == uid: return (event, master) if event is not None: setattr(event, '_imap_folder', folder) setattr(event, '_lock_key', lock_key) setattr(event, '_msguid', msguid) except Exception: log.error(_("Failed to parse %s from message %s/%s: %s") % (type, folder, num, traceback.format_exc())) event = None master = None continue if event and event.uid == uid: return (event, master) if lock_key is not None: remove_write_lock(lock_key) return (event, master) def check_availability(itip_event, receiving_user): """ For the receiving user, determine if the event in question is in conflict. """ start = time.time() num_messages = 0 conflict = False # return previously detected conflict if '_conflicts' in itip_event: return not itip_event['_conflicts'] for folder in list_user_folders(receiving_user, 'event'): log.debug(_("Listing events from folder %r") % (folder), level=8) imap.imap.m.select(imap.folder_utf7(folder)) res, data = imap.imap.m.search(None, '(UNDELETED HEADER X-Kolab-Type "application/x-vnd.kolab.event")') num_messages += len(data[0].split()) for num in reversed(data[0].split()): event = None res, data = imap.imap.m.fetch(num, '(RFC822)') try: event = event_from_message(message_from_string(data[0][1])) except Exception as errmsg: log.error(_("Failed to parse event from message %s/%s: %r") % (folder, num, errmsg)) continue if event and event.uid: conflict = check_event_conflict(event, itip_event) if conflict: log.info(_("Existing event %r conflicts with invitation %r") % (event.uid, itip_event['uid'])) break if conflict: break end = time.time() log.debug(_("start: %r, end: %r, total: %r, messages: %d") % (start, end, (end-start), num_messages), level=8) # remember the result of this check for further iterations itip_event['_conflicts'] = conflict return not conflict def set_write_lock(key, wait=True): """ Set a write-lock for the given key and wait if such a lock already exists """ if not os.path.isdir(mybasepath): os.makedirs(mybasepath) if not os.path.isdir(os.path.join(mybasepath, 'locks')): os.makedirs(os.path.join(mybasepath, 'locks')) filename = os.path.join(mybasepath, 'locks', key + '.lock') locktime = 0 if os.path.isfile(filename): locktime = os.path.getmtime(filename) # wait if file lock is in place while time.time() < locktime + 300: if not wait: return False log.debug(_("%r is locked, waiting...") % (key), level=8) time.sleep(0.5) locktime = os.path.getmtime(filename) if os.path.isfile(filename) else 0 # touch the file if os.path.isfile(filename): os.utime(filename, None) else: open(filename, 'w').close() # register active lock write_locks.append(key) return True def remove_write_lock(key, update=True): """ Remove the lock file for the given key """ global write_locks if key is not None: file = os.path.join(mybasepath, 'locks', key + '.lock') if os.path.isfile(file): os.remove(file) if update: write_locks = [k for k in write_locks if not k == key] def get_lock_key(user, uid): return hashlib.md5("%s/%s" % (user['mail'], uid)).hexdigest() def update_object(object, user_rec, master=None): """ Update the given object in IMAP (i.e. delete + append) """ success = False saveobj = object # updating a single instance only: use master event if object.get_recurrence_id() and master: saveobj = master if hasattr(saveobj, '_imap_folder'): if delete_object(saveobj): saveobj.set_lastmodified() # update last-modified timestamp success = store_object(object, user_rec, saveobj._imap_folder, master) # remove write lock for this event if hasattr(saveobj, '_lock_key') and saveobj._lock_key is not None: remove_write_lock(saveobj._lock_key) return success def store_object(object, user_rec, targetfolder=None, master=None): """ Append the given object to the user's default calendar/tasklist """ # find calendar folder to save object to if not specified if targetfolder is None: targetfolders = list_user_folders(user_rec, object.type) oc = object.get_classification() # use *.confidential/private folder for confidential/private invitations if oc == kolabformat.ClassConfidential and '_confidential_folder' in user_rec: targetfolder = user_rec['_confidential_folder'] elif oc == kolabformat.ClassPrivate and '_private_folder' in user_rec: targetfolder = user_rec['_private_folder'] # use *.default folder if exists elif '_default_folder' in user_rec: targetfolder = user_rec['_default_folder'] # fallback to any existing folder of specified type elif targetfolders is not None and len(targetfolders) > 0: targetfolder = targetfolders[0] if targetfolder is None: log.error(_("Failed to save %s: no target folder found for user %r") % (object.type, user_rec['mail'])) return False saveobj = object # updating a single instance only: add exception to master event if object.get_recurrence_id() and master: object.set_lastmodified() # update last-modified timestamp master.add_exception(object) saveobj = master log.debug(_("Save %s %r to user folder %r") % (saveobj.type, saveobj.uid, targetfolder), level=8) try: imap.imap.m.select(imap.folder_utf7(targetfolder)) result = imap.imap.m.append( imap.folder_utf7(targetfolder), None, None, saveobj.to_message(creator="Kolab Server ").as_string() ) return result except Exception as errmsg: log.error(_("Failed to save %s to user folder at %r: %r") % ( saveobj.type, targetfolder, errmsg )) return False def delete_object(existing): """ Removes the IMAP object with the given UID from a user's folder """ targetfolder = existing._imap_folder msguid = existing._msguid if hasattr(existing, '_msguid') else None try: imap.imap.m.select(imap.folder_utf7(targetfolder)) # delete by IMAP UID if msguid is not None: log.debug(_("Delete %s %r in %r by UID: %r") % ( existing.type, existing.uid, targetfolder, msguid ), level=8) imap.imap.m.uid('store', msguid, '+FLAGS', '(\\Deleted)') else: res, data = imap.imap.m.search(None, '(HEADER SUBJECT "%s")' % existing.uid) log.debug(_("Delete %s %r in %r: %r") % ( existing.type, existing.uid, targetfolder, data ), level=8) for num in data[0].split(): imap.imap.m.store(num, '+FLAGS', '(\\Deleted)') imap.imap.m.expunge() return True except Exception as errmsg: log.error(_("Failed to delete %s from folder %r: %r") % ( existing.type, targetfolder, errmsg )) return False def send_update_notification(object, receiving_user, old=None, reply=True, sender=None, comment=None): """ Send a (consolidated) notification about the current participant status to organizer """ global auth from email.MIMEText import MIMEText from email.Utils import formatdate from email.header import Header from email import charset # encode unicode strings with quoted-printable charset.add_charset('utf-8', charset.SHORTEST, charset.QP) organizer = object.get_organizer() orgemail = organizer.email() orgname = organizer.name() itip_comment = None if comment is not None: comment = comment.strip() if sender is not None and not comment == '': itip_comment = _("%s commented: %s") % (_attendee_name(sender), comment) if reply: log.debug(_("Compose participation status summary for %s %r to user %r") % ( object.type, object.uid, receiving_user['mail'] ), level=8) auto_replies_expected = 0 auto_replies_received = 0 is_manual_reply = True partstats = {'ACCEPTED': [], 'TENTATIVE': [], 'DECLINED': [], 'DELEGATED': [], 'IN-PROCESS': [], 'COMPLETED': [], 'PENDING': []} for attendee in object.get_attendees(): parstat = attendee.get_participant_status(True) if parstat in partstats: partstats[parstat].append(attendee.get_displayname()) else: partstats['PENDING'].append(attendee.get_displayname()) # look-up kolabinvitationpolicy for this attendee if attendee.get_cutype() == kolabformat.CutypeResource: resource_dns = auth.find_resource(attendee.get_email()) if isinstance(resource_dns, list): attendee_dn = resource_dns[0] if len(resource_dns) > 0 else None else: attendee_dn = resource_dns else: attendee_dn = user_dn_from_email_address(attendee.get_email()) if attendee_dn: attendee_rec = auth.get_entry_attributes(None, attendee_dn, ['kolabinvitationpolicy']) if is_auto_reply(attendee_rec, orgemail, object.type): auto_replies_expected += 1 if not parstat == 'NEEDS-ACTION': auto_replies_received += 1 if sender is not None and sender.get_email() == attendee.get_email(): is_manual_reply = False # skip notification until we got replies from all automatically responding attendees if not is_manual_reply and auto_replies_received < auto_replies_expected: log.debug(_("Waiting for more automated replies (got %d of %d); skipping notification") % ( auto_replies_received, auto_replies_expected ), level=8) return # build notification message body roundup = '' if itip_comment is not None: roundup += "\n" + itip_comment for status,attendees in partstats.items(): if len(attendees) > 0: roundup += "\n" + participant_status_label(status) + ":\n\t" + "\n\t".join(attendees) + "\n" else: # build notification message body roundup = '' if itip_comment is not None: roundup += "\n" + itip_comment roundup += "\n" + _("Changes submitted by %s have been automatically applied.") % (orgname if orgname else orgemail) # list properties changed from previous version if old: diff = xmlutils.compute_diff(old.to_dict(), object.to_dict()) if len(diff) > 1: roundup += "\n" for change in diff: if not change['property'] in ['created','lastmodified-date','sequence']: new_value = xmlutils.property_to_string(change['property'], change['new']) if change['new'] else _("(removed)") if new_value: roundup += "\n- %s: %s" % (xmlutils.property_label(change['property']), new_value) # compose different notification texts for events/tasks if object.type == 'task': message_text = _(""" The assignment for '%(summary)s' has been updated in your tasklist. %(roundup)s """) % { 'summary': object.get_summary(), 'roundup': roundup } else: message_text = _(""" The event '%(summary)s' at %(start)s has been updated in your calendar. %(roundup)s """) % { 'summary': object.get_summary(), 'start': xmlutils.property_to_string('start', object.get_start()), 'roundup': roundup } if object.get_recurrence_id(): message_text += _("NOTE: This update only refers to this single occurrence!") + "\n" message_text += "\n" + _("*** This is an automated message. Please do not reply. ***") # compose mime message msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') msg['To'] = receiving_user['mail'] msg['Date'] = formatdate(localtime=True) msg['Subject'] = utils.str2unicode(_('"%s" has been updated') % (object.get_summary())) msg['From'] = Header(utils.str2unicode('%s' % orgname) if orgname else '') msg['From'].append("<%s>" % orgemail) seed = random.randint(0, 6) alarm_after = (seed * 10) + 60 log.debug(_("Set alarm to %s seconds") % (alarm_after), level=8) signal.alarm(alarm_after) result = modules._sendmail(orgemail, receiving_user['mail'], msg.as_string()) log.debug(_("Sent update notification to %r: %r") % (receiving_user['mail'], result), level=8) signal.alarm(0) def send_cancel_notification(object, receiving_user, deleted=False, sender=None, comment=None): """ Send a notification about event/task cancellation """ from email.MIMEText import MIMEText from email.Utils import formatdate from email.header import Header from email import charset # encode unicode strings with quoted-printable charset.add_charset('utf-8', charset.SHORTEST, charset.QP) log.debug(_("Send cancellation notification for %s %r to user %r") % ( object.type, object.uid, receiving_user['mail'] ), level=8) organizer = object.get_organizer() orgemail = organizer.email() orgname = organizer.name() # compose different notification texts for events/tasks if object.type == 'task': message_text = _("The assignment for '%(summary)s' has been cancelled by %(organizer)s.") % { 'summary': object.get_summary(), 'organizer': orgname if orgname else orgemail } if deleted: message_text += " " + _("The copy in your tasklist has been removed accordingly.") else: message_text += " " + _("The copy in your tasklist has been marked as cancelled accordingly.") else: message_text = _("The event '%(summary)s' at %(start)s has been cancelled by %(organizer)s.") % { 'summary': object.get_summary(), 'start': xmlutils.property_to_string('start', object.get_start()), 'organizer': orgname if orgname else orgemail } if deleted: message_text += " " + _("The copy in your calendar has been removed accordingly.") else: message_text += " " + _("The copy in your calendar has been marked as cancelled accordingly.") if comment is not None: comment = comment.strip() if sender is not None and not comment == '': message_text += "\n" + _("%s commented: %s") % (_attendee_name(sender), comment) if object.get_recurrence_id(): message_text += "\n" + _("NOTE: This cancellation only refers to this single occurrence!") message_text += "\n\n" + _("*** This is an automated message. Please do not reply. ***") # compose mime message msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') msg['To'] = receiving_user['mail'] msg['Date'] = formatdate(localtime=True) msg['Subject'] = utils.str2unicode(_('"%s" has been cancelled') % (object.get_summary())) msg['From'] = Header(utils.str2unicode('%s' % orgname) if orgname else '') msg['From'].append("<%s>" % orgemail) seed = random.randint(0, 6) alarm_after = (seed * 10) + 60 log.debug(_("Set alarm to %s seconds") % (alarm_after), level=8) signal.alarm(alarm_after) result = modules._sendmail(orgemail, receiving_user['mail'], msg.as_string()) log.debug(_("Sent cancel notification to %r: %r") % (receiving_user['mail'], result), level=8) signal.alarm(0) def is_auto_reply(user, sender_email, type): accept_available = False accept_conflicts = False for policy in get_matching_invitation_policies(user, sender_email, object_type_conditons.get(type, COND_TYPE_EVENT)): if policy & (ACT_ACCEPT | ACT_REJECT | ACT_DELEGATE): if check_policy_condition(policy, True): accept_available = True if check_policy_condition(policy, False): accept_conflicts = True # we have both cases covered by a policy if accept_available and accept_conflicts: return True # manual action reached if policy & (ACT_MANUAL | ACT_SAVE_TO_FOLDER): return False return False def check_policy_condition(policy, available): condition_fulfilled = True if policy & (COND_IF_AVAILABLE | COND_IF_CONFLICT): condition_fulfilled = available if policy & COND_IF_CONFLICT: condition_fulfilled = not condition_fulfilled return condition_fulfilled def propagate_changes_to_attendees_accounts(object, updated_attendees=None): """ Find and update copies of this object in all attendee's personal folders """ recurrence_id = object.get_recurrence_id() for attendee in object.get_attendees(): attendee_user_dn = user_dn_from_email_address(attendee.get_email()) if attendee_user_dn: attendee_user = auth.get_entry_attributes(None, attendee_user_dn, ['*']) (attendee_object, master_object) = find_existing_object(object.uid, object.type, recurrence_id, attendee_user, True) # does IMAP authenticate if attendee_object: # find attendee's entry by one of its email addresses attendee_emails = auth.extract_recipient_addresses(attendee_user) for attendee_email in attendee_emails: try: attendee_entry = attendee_object.get_attendee_by_email(attendee_email) except: attendee_entry = None if attendee_entry: break # copy all attendees from master object (covers additions and removals) new_attendees = [] for a in object.get_attendees(): # keep my own entry intact if attendee_entry is not None and attendee_entry.get_email() == a.get_email(): new_attendees.append(attendee_entry) else: new_attendees.append(a) attendee_object.set_attendees(new_attendees) if updated_attendees and not recurrence_id: log.debug("Update Attendees %r for %s" % ([a.get_email()+':'+a.get_participant_status(True) for a in updated_attendees], attendee_user['mail']), level=8) attendee_object.update_attendees(updated_attendees, False) success = update_object(attendee_object, attendee_user, master_object) log.debug(_("Updated %s's copy of %r: %r") % (attendee_user['mail'], object.uid, success), level=8) else: log.debug(_("Attendee %s's copy of %r not found") % (attendee_user['mail'], object.uid), level=8) else: log.debug(_("Attendee %r not found in LDAP") % (attendee.get_email()), level=8) def invitation_response_text(type): footer = "\n\n" + _("*** This is an automated message. Please do not reply. ***") if type == 'task': return _("%(name)s has %(status)s your assignment for %(summary)s.") + footer else: return _("%(name)s has %(status)s your invitation for %(summary)s.") + footer def _attendee_name(attendee): # attendee here can be Attendee or ContactReference try: name = attendee.get_name() except Exception: name = attendee.name() if name == '': try: name = attendee.get_email() except Exception: name = attendee.email() return name diff --git a/wallace/module_optout.py b/wallace/module_optout.py index 6e44378..9220ebe 100644 --- a/wallace/module_optout.py +++ b/wallace/module_optout.py @@ -1,195 +1,195 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import json import os import random import tempfile import time try: from urlparse import urlparse except ImportError: from urllib.parse import urlparse import urllib from email import message_from_file from email.utils import formataddr from email.utils import getaddresses -import modules +from . import modules import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/optout') conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/optout/' def __init__(): modules.register('optout', execute, description=description()) def description(): return """Consult the opt-out service.""" def execute(*args, **kw): if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) # TODO: Test for correct call. filepath = args[0] if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'],'optout',filepath)) return #modules.next_module('optout') log.debug(_("Consulting opt-out service for %r, %r") % (args, kw), level=8) message = message_from_file(open(filepath, 'r')) envelope_sender = getaddresses(message.get_all('From', [])) recipients = { "To": getaddresses(message.get_all('To', [])), "Cc": getaddresses(message.get_all('Cc', [])) # TODO: Are those all recipient addresses? } # optout answers are ACCEPT, REJECT, HOLD or DEFER answers = [ 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ] # Initialize our results placeholders. _recipients = {} for answer in answers: _recipients[answer] = { "To": [], "Cc": [] } for recipient_type in recipients: for recipient in recipients[recipient_type]: log.debug( _("Running opt-out consult from envelope sender '%s " + \ "<%s>' to recipient %s <%s>") % ( envelope_sender[0][0], envelope_sender[0][1], recipient[0], recipient[1] ), level=8 ) optout_answer = request( { 'unique-message-id': 'bogus', 'envelope_sender': envelope_sender[0][1], 'recipient': recipient[1] } ) _recipients[optout_answer][recipient_type].append(recipient) #print _recipients ## ## TODO ## ## If one of them all is DEFER, DEFER the entire message and discard the ## other results. ## for answer in answers: # Create the directory for the answer if not os.path.isdir(os.path.join(mybasepath, answer)): os.makedirs(os.path.join(mybasepath, answer)) # Consider using a new mktemp()-like call new_filepath = os.path.join(mybasepath, answer, os.path.basename(filepath)) # Write out a message file representing the new contents for the message # use formataddr(recipient) _message = message_from_file(open(filepath, 'r')) use_this = False for recipient_type in _recipients[answer]: _message.__delitem__(recipient_type) if not len(_recipients[answer][recipient_type]) == 0: _message.__setitem__( recipient_type, ',\n '.join( [formataddr(x) for x in _recipients[answer][recipient_type]] ) ) use_this = True if use_this: # TODO: Do not set items with an empty list. (fp, filename) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/optout/%s" % (answer)) os.write(fp, _message.__str__()) os.close(fp) # Callback with new filename if hasattr(modules, 'cb_action_%s' % (answer)): log.debug(_("Attempting to execute cb_action_%s(%r, %r)") % (answer, 'optout', filename), level=8) exec('modules.cb_action_%s(%r, %r)' % (answer,'optout', filename)) os.unlink(filepath) #print "Moving filepath %s to new_filepath %s" % (filepath, new_filepath) #os.rename(filepath, new_filepath) #if hasattr(modules, 'cb_action_%s' % (optout_answer)): #log.debug(_("Attempting to execute cb_action_%s()") % (optout_answer), level=8) #exec('modules.cb_action_%s(%r, %r)' % (optout_answer,'optout', new_filepath)) #return def request(params=None): params = json.dumps(params) optout_url = conf.get('wallace_optout', 'optout_url') try: f = urllib.urlopen(optout_url, params) except Exception: log.error(_("Could not send request to optout_url %s") % (optout_url)) return "DEFER" response = f.read() try: response_data = json.loads(response) except ValueError: # Some data is not JSON print("Response data is not JSON") return response_data['result'] diff --git a/wallace/module_resources.py b/wallace/module_resources.py index e29f193..afaf384 100644 --- a/wallace/module_resources.py +++ b/wallace/module_resources.py @@ -1,1805 +1,1805 @@ # -*- coding: utf-8 -*- # pylint: disable=too-many-lines # 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 base64 import datetime from email import message_from_string from email.parser import Parser from email.utils import formataddr from email.utils import getaddresses import os import random import re import signal from six import string_types import time import uuid from dateutil.tz import tzlocal -import modules +from . import modules import kolabformat import pykolab from pykolab.auth import Auth from pykolab.conf import Conf from pykolab.imap import IMAP from pykolab.logger import LoggerAdapter from pykolab.itip import events_from_message from pykolab.itip import check_event_conflict from pykolab.translate import _ 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 # define some contstants used in the code below COND_NOTIFY = 256 ACT_MANUAL = 1 ACT_ACCEPT = 2 ACT_REJECT = 8 ACT_STORE = 16 ACT_ACCEPT_AND_NOTIFY = ACT_ACCEPT + COND_NOTIFY ACT_STORE_AND_NOTIFY = ACT_STORE + COND_NOTIFY # noqa: E241 policy_name_map = { 'ACT_MANUAL': ACT_MANUAL, # noqa: E241 'ACT_ACCEPT': ACT_ACCEPT, # noqa: E241 'ACT_REJECT': ACT_REJECT, # noqa: E241 'ACT_ACCEPT_AND_NOTIFY': ACT_ACCEPT_AND_NOTIFY, 'ACT_STORE_AND_NOTIFY': ACT_STORE_AND_NOTIFY } # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.wallace/resources') extra_log_params = {'qid': '-'} log = LoggerAdapter(log, extra_log_params) 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, extra_log_params log.debug("cleanup(): %r, %r" % (auth, imap), level=8) extra_log_params['qid'] = '-' auth.disconnect() del auth # Disconnect IMAP or we lock the mailbox almost constantly imap.disconnect() del imap # pylint: disable=inconsistent-return-statements # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-return-statements # pylint: disable=too-many-statements def execute(*args, **kw): global auth, imap, extra_log_params # TODO: Test for correct call. filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) # (re)set language to default pykolab.translate.setUserLanguage(conf.get('kolab', 'default_locale')) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER']: 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() if 'stage' in kw: log.debug( _("Issuing callback after processing to stage %s") % ( kw['stage'] ), level=8 ) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug( _("Attempting to execute cb_action_%s()") % (kw['stage']), level=8 ) exec( 'modules.cb_action_%s(%r, %r)' % ( kw['stage'], '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']) # pylint: disable=broad-except except Exception as errmsg: log.error(_("Failed to parse iTip events from message: %r" % (errmsg))) itip_events = [] if not len(itip_events) > 0: log.info("Message is not an iTip message or does not contain any (valid) iTip.") else: any_itips = True log.debug( "iTip events attached to this message contain the following information: %r" % ( itip_events ), level=8 ) if any_itips: # See if any iTip actually allocates a resource. if (len([x['resources'] for x in itip_events if 'resources' in x]) > 0 or len([x['attendees'] for x in itip_events if 'attendees' in x]) > 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(r'.+\+[A-Za-z0-9=/-]+@', recipient): try: (prefix, host) = recipient.split('@') (local, uid) = prefix.split('+') reference_uid = base64.b64decode(uid, '-/') recipient = local + '@' + host # pylint: disable=broad-except except Exception: 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=8 ) # pylint: disable=broad-except except Exception as errmsg: log.error(_("Could not find envelope sender attendee: %r") % (errmsg)) 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=8 ) # pylint: disable=broad-except except Exception as errmsg: log.error(_("Could not find envelope attendee: %r") % (errmsg)) continue # ignore updates and cancellations to resource collections who already delegated the event att_delegated = (len(receiving_attendee.get_delegated_to()) > 0) att_nonpart = (receiving_attendee.get_role() == kolabformat.NonParticipant) att_rsvp = receiving_attendee.get_rsvp() if (att_delegated or att_nonpart) and not att_rsvp: 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: r_emails = [a.get_email() for a in itip_event['xml'].get_attendees()] _resource = resources[resource] if _resource['mail'] in r_emails and 'kolabtargetfolder' in _resource: (event, master) = find_existing_event( itip_event['uid'], itip_event['recurrence-id'], _resource ) if not event: continue # remove entire event if 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 else: 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: atts = [a.get_email() for a in itip_event['xml'].get_attendees()] if available_resource['mail'] in atts: # check if reservation was delegated if available_resource['mail'] != receiving_resource['mail']: if 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 'memberof' in available_resource: original_resource = resources[available_resource['memberof']] atts = [a.get_email() for a in itip_event['xml'].get_attendees()] if original_resource['mail'] in atts: # # 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=8) 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 'kolabtargetfolder' in resource_attrs: try: expunge_resource_calendar(resource_attrs['kolabtargetfolder']) # pylint: disable=broad-except except Exception as errmsg: log.error( _("Expunge resource calendar for %s (%s) failed: %r") % ( resource_dn, resource_attrs['kolabtargetfolder'], errmsg ) ) 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=8 ) typ, data = imap.imap.m.fetch(num, '(RFC822)') try: event = event_from_message(message_from_string(data[0][1])) # pylint: disable=broad-except except Exception as errmsg: log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, errmsg)) 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: # skip this for resource collections if 'kolabtargetfolder' not in resources[resource]: continue # sets the 'conflicting' flag and adds a list of conflicting events found try: num_messages += read_resource_calendar(resources[resource], itip_events) # pylint: disable=broad-except except Exception as 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=8 ) # 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=8) if resource not in resources: log.debug(_("Resource %r has been popped from the list") % (resource), level=8) continue if 'conflicting_events' not in resources[resource]: log.debug(_("Resource is a collection"), level=8) # 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=8) 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=8 ) 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 _eas = [a.get_email() for a in itip_event['xml'].get_attendees()] # Now we have the event that was conflicting if resources[resource]['mail'] in _eas: # 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 _eas = [a.get_email() for a in itip_event['xml'].get_attendees()] if resources[resource]['mail'] in _eas: 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 'memberof' in resources[resource]: original_resource = resources[resources[resource]['memberof']] # Randomly select a target resource from the resource collection. _selected = random.randint(0, (len(original_resource['uniquemember']) - 1)) available_resource = resources[original_resource['uniquemember'][_selected]] 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=8 ) # 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=8 ) typ, data = imap.imap.m.fetch(num, '(UID RFC822)') try: msguid = re.search(r"\WUID (\d+)", data[0][0]).group(1) # pylint: disable=broad-except except Exception: log.error(_("No UID found in IMAP response: %r") % (data[0][0])) continue try: event = event_from_message(message_from_string(data[0][1])) # pylint: disable=broad-except except Exception as 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=8) try: imap.imap.m.select(imap.folder_quote(mailbox)) typ, data = imap.imap.m.search(None, '(UNDELETED HEADER SUBJECT "%s")' % (uid)) # pylint: disable=broad-except except Exception as errmsg: log.error(_("Failed to access resource calendar:: %r") % (errmsg)) 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) # pylint: disable=broad-except except Exception: 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: if 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) # pylint: disable=broad-except except Exception as errmsg: log.error(_("Failed to parse event from message %s/%s: %r") % (mailbox, num, errmsg)) 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, depending on the policy, set the attendee status of the given resource to ACCEPTED/TENTATIVE and send an iTip reply message to the organizer, or set the status to NEEDS-ACTION and don't send a reply to the organizer. """ owner = get_resource_owner(resource) confirmation_required = False do_send_response = True partstat = 'ACCEPTED' if not confirmed and owner: if invitationpolicy is None: invitationpolicy = get_resource_invitationpolicy(resource) log.debug(_("Apply invitation policies %r") % (invitationpolicy), level=8) if invitationpolicy is not None: for policy in invitationpolicy: if policy & ACT_MANUAL and owner['mail']: confirmation_required = True partstat = 'TENTATIVE' break if policy & ACT_STORE: partstat = 'NEEDS-ACTION' # Do not send an immediate response to the organizer do_send_response = False break 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 and do_send_response: 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'] # 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 'existing_events' in resource 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( resource['kolabtargetfolder'], conf.get(conf.get('kolab', 'imap_backend'), 'admin_login'), "lrswipkxtecda" ) # append new version result = imap.append( resource['kolabtargetfolder'], save_event.to_message(creator="Kolab Server ").as_string() ) return result # pylint: disable=broad-except except Exception as 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=8) for num in data[0].split(): imap.imap.m.store(num, '+FLAGS', '\\Deleted') imap.imap.m.expunge() return True # pylint: disable=broad-except except Exception as 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 local_domains is not 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=8) elif isinstance(resource_records, string_types): 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=8) attendees_raw = [] _lars = [ x for x in [ y['attendees'] for y in itip_events if 'attendees' in y and isinstance(y['attendees'], list) ] ] for list_attendees_raw in _lars: attendees_raw.extend(list_attendees_raw) _lars = [ y['attendees'] for y in itip_events if 'attendees' in y and isinstance(y['attendees'], string_types) ] for list_attendees_raw in _lars: attendees_raw.append(list_attendees_raw) log.debug(_("Raw set of attendees: %r") % (attendees_raw), level=8) # TODO: Resources are actually not implemented in the format. We reset this # list later. resources_raw = [] _lrrs = [x for x in [y['resources'] for y in itip_events if 'resource' in y]] for list_resources_raw in _lrrs: resources_raw.extend(list_resources_raw) log.debug(_("Raw set of resources: %r") % (resources_raw), level=8) # consider organizer (in REPLY messages), too organizers_raw = [ re.sub(r'\+[A-Za-z0-9=/-]+@', '@', str(y['organizer'])) for y in itip_events if 'organizer' in y ] 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=8) elif isinstance(_resource_records, string_types): 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, string_types): 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 'kolabsharedfolder' not in [x.lower() for x in resource_attrs['objectclass']]: if 'uniquemember' in resource_attrs: 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 'owner' not in member_attrs and 'owner' in resources[resource_dn]: 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 'kolabinvitationpolicy' in attrs: if not isinstance(attrs['kolabinvitationpolicy'], list): attrs['kolabinvitationpolicy'] = [attrs['kolabinvitationpolicy']] attrs['kolabinvitationpolicy'] = [ policy_name_map[p] for p in attrs['kolabinvitationpolicy'] if p in policy_name_map ] elif isinstance(parent, dict) and 'kolabinvitationpolicy' in parent: attrs['kolabinvitationpolicy'] = parent['kolabinvitationpolicy'] def get_resource_collection(email_address): """ Obtain a resource collection object from an 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 'kolabsharedfolder' not 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 'owner' in resource: 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 'owner' in collection and isinstance(collection['owner'], list): owners += collection['owner'] elif 'owner' in collection: 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 'kolabinvitationpolicy' not in resource 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=8 ) for dn, collection in collections: # ldap.search_entry_by_attribute() doesn't return the attributes lower-cased if 'kolabInvitationPolicy' in collection: collection['kolabinvitationpolicy'] = collection['kolabInvitationPolicy'] if 'kolabinvitationpolicy' in collection: parse_kolabinvitationpolicy(collection) resource['kolabinvitationpolicy'] = collection['kolabinvitationpolicy'] break return resource['kolabinvitationpolicy'] if 'kolabinvitationpolicy' in resource 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 'telephoneNumber' in owner else '' ) return message_text def send_owner_notification(resource, owner, itip_event, success=True): """ Send a reservation notification to the resource owner """ 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 'preferredlanguage' in owner: pykolab.translate.setUserLanguage(owner['preferredlanguage']) message_text = owner_notification_text(resource, owner, itip_event['xml'], success, status) msg = MIMEText(utils.stripped_message(message_text), _charset='utf-8') msg['To'] = owner['mail'] msg['From'] = resource['mail'] msg['Date'] = formatdate(localtime=True) if status == 'NEEDS-ACTION': msg['Subject'] = utils.str2unicode(_('New booking request for %s') % ( resource['cn'] )) else: msg['Subject'] = utils.str2unicode(_('Booking for %s has been %s') % ( resource['cn'], participant_status_label(status) if success else _('failed') )) seed = random.randint(0, 6) alarm_after = (seed * 10) + 60 log.debug(_("Set alarm to %s seconds") % (alarm_after), level=8) signal.alarm(alarm_after) result = modules._sendmail(resource['mail'], owner['mail'], msg.as_string()) log.debug(_("Owner notification was sent successfully: %r") % result, level=8) signal.alarm(0) def owner_notification_text(resource, owner, event, success, status): organizer = event.get_organizer() status = event.get_attendee_by_email(resource['mail']).get_participant_status(True) domain = resource['mail'].split('@')[1] url = conf.get('wallace', 'webmail_url') if success: if status == 'NEEDS-ACTION': message_text = _( """ The resource booking request is for %(resource)s by %(orgname)s <%(orgemail)s> for %(date)s. *** This is an automated message, sent to you as the resource owner. *** """ ) else: 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. *** """ ) if url: message_text += ( "\n " + _("You can change the status via %(url)s") % { 'url': url } + '?_task=calendar' ) 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 for the event: %(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(), 'domain': domain } 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 ) diff --git a/wallace/module_signature.py b/wallace/module_signature.py index c483d93..cfebcb5 100644 --- a/wallace/module_signature.py +++ b/wallace/module_signature.py @@ -1,254 +1,254 @@ # -*- coding: utf-8 -*- # Copyright 2010-2019 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import json import os import re import tempfile from email.encoders import encode_quopri from email.parser import Parser from email.utils import getaddresses -import modules +from . import modules import pykolab from pykolab.auth import Auth from pykolab.translate import _ # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.wallace/signature') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/signature/' def __init__(): modules.register('signature', execute, description=description()) def description(): return """Append a signature to messages.""" def set_part_content(part, content): # Reset old encoding and use quoted-printable (#5414) del part['Content-Transfer-Encoding'] part.set_payload(content) encode_quopri(part) return True def attr_resolve(sender_info, attr): try: attr, attr_val = attr.split(':') except ValueError: return None auth = Auth() auth.connect() values = [] if not isinstance(sender_info[attr], list): sender_info[attr] = [sender_info[attr]] for sender_attr_val in sender_info[attr]: values.append(auth.get_entry_attribute(None, sender_attr_val, attr_val)) return ", ".join(values) # pylint: disable=too-many-branches,too-many-locals,too-many-statements def execute(*args, **kw): # noqa: C901 global extra_log_params # TODO: Test for correct call. filepath = args[0] extra_log_params['qid'] = os.path.basename(filepath) if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT']: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) if 'stage' in kw: log.debug(_("Issuing callback after processing to stage %s") % (kw['stage']), level=8) log.debug(_("Testing cb_action_%s()") % (kw['stage']), level=8) if hasattr(modules, 'cb_action_%s' % (kw['stage'])): log.debug(_("Attempting to execute cb_action_%s()") % (kw['stage']), level=8) exec('modules.cb_action_%s(%r, %r)' % (kw['stage'], 'signature', filepath)) return log.debug(_("Executing module signature for %r, %r") % (args, kw), level=8) new_filepath = os.path.join( '/var/spool/pykolab/wallace/signature/incoming', os.path.basename(filepath) ) os.rename(filepath, new_filepath) filepath = new_filepath # parse message message = Parser().parse(open(filepath, 'r')) sender_address = [ address for displayname, address in getaddresses(message.get_all('X-Kolab-From')) ][0] auth = Auth() auth.connect() sender_dn = auth.find_recipient(sender_address) if not sender_dn: exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT', 'signature', filepath)) return sender_info = auth.get_entry_attributes(None, sender_dn, ['*', 'entrydn', 'manager']) log.debug("Sender info: %r" % (sender_info), level=7) signature_rules = conf.get_raw('wallace', 'signature_rules') if signature_rules: signature_rules = json.loads(signature_rules) log.debug("Signature rules: %r" % (signature_rules), level=7) signature_html = None signature_text = None sig_html_conf = conf.get_raw('wallace', 'signature_file_html') sig_text_conf = conf.get_raw('wallace', 'signature_file_text') if sig_html_conf and sig_text_conf: _sig_html_conf = sig_html_conf % sender_info _sig_text_conf = sig_text_conf % sender_info if not os.path.exists(_sig_html_conf): _sig_html_conf = '/etc/kolab/signature.d/default.html' if not os.path.exists(_sig_text_conf): _sig_text_conf = '/etc/kolab/signature.d/default.txt' if os.path.exists(_sig_html_conf): signature_html = open(_sig_html_conf, 'r').read() if os.path.exists(_sig_text_conf): signature_text = open(_sig_text_conf, 'r').read() if not signature_html and not signature_text and signature_rules is not None: for signature_rule in signature_rules: try: for attr, regex in signature_rule.items(): if attr == "html": if not os.path.exists(signature_rule['html']): raise ValueError continue if attr == "text": if not os.path.exists(signature_rule['text']): raise ValueError continue if attr in sender_info and re.match(regex, sender_info[attr], flags=re.IGNORECASE): success = False while not success: try: signature_html = open(signature_rule['html'], 'r').read() % sender_info signature_text = open(signature_rule['text'], 'r').read() % sender_info success = True except KeyError as errmsg: sender_info[errmsg] = attr_resolve(sender_info, errmsg) except ValueError: continue if signature_html is None and signature_text is None: exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT', 'signature', filepath)) return signature_added = False try: _signature_added = message.get("X-Wallace-Signature") # pylint: disable=broad-except except Exception: pass if _signature_added == "YES": exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','signature', filepath)) return for part in message.walk(): disposition = None try: content_type = part.get_content_type() # pylint: disable=broad-except except Exception: continue try: disposition = part.get("Content-Disposition") # pylint: disable=broad-except except Exception: pass log.debug("Walking message part: %s; disposition = %r" % (content_type, disposition), level=8) if disposition is not None: continue if content_type == "text/plain": content = part.get_payload(decode=True) content += "\n\n-- \n%s" % (signature_text) signature_added = set_part_content(part, content) elif content_type == "text/html": content = part.get_payload(decode=True) append = "\n\n" + signature_html if "" in content: content = content.replace("", append + "") else: content = "" + content + append + "" signature_added = set_part_content(part, content) if signature_added: log.debug("Signature attached.", level=8) message.add_header("X-Wallace-Signature", "YES") (fp, new_filepath) = tempfile.mkstemp(dir="/var/spool/pykolab/wallace/signature/ACCEPT") os.write(fp, message.as_string()) os.close(fp) os.unlink(filepath) exec('modules.cb_action_%s(%r, %r)' % ('ACCEPT','signature', new_filepath))