diff --git a/bin/kolab_smtp_access_policy.py b/bin/kolab_smtp_access_policy.py index e03bad0..6c82165 100755 --- a/bin/kolab_smtp_access_policy.py +++ b/bin/kolab_smtp_access_policy.py @@ -1,1727 +1,1727 @@ #!/usr/bin/python # # 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 os import sqlalchemy import sys import time from optparse import OptionParser from ConfigParser import SafeConfigParser from sqlalchemy import Boolean from sqlalchemy import Column from sqlalchemy import Date from sqlalchemy import DateTime from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import String from sqlalchemy import Table from sqlalchemy import Sequence from sqlalchemy import PickleType from sqlalchemy import create_engine from sqlalchemy.orm import mapper try: from sqlalchemy.orm import sessionmaker except: from sqlalchemy.orm import create_session from sqlalchemy.schema import Index from sqlalchemy.schema import UniqueConstraint import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.constants import * from pykolab.translate import _ cache = None sys.path = ['..', '.'] + sys.path # TODO: Figure out how to make our logger do some syslogging as well. log = pykolab.getLogger('pykolab.smtp_access_policy') # TODO: Removing the stdout handler would mean one can no longer test by # means of manual execution in debug mode. log.remove_stdout_handler() conf = pykolab.getConf() auth = None mydomains = None # # Caching routines using SQLAlchemy. # # If creating the cache fails, we continue without any caching, significantly # increasing the load on LDAP. # cache_expire = 86400 try: metadata = MetaData() except: cache = False session = None policy_result_table = Table( 'policy_result', metadata, Column('id', Integer, Sequence('seq_id_result'), primary_key=True), Column('key', String(16), nullable=False), Column('value', Boolean, nullable=False), Column('sender', String(254), nullable=True), Column('recipient', String(254), nullable=False), Column('sasl_username', String(64)), Column('sasl_sender', String(64)), Column('created', Integer, nullable=False), Column('data', PickleType, nullable=True), mysql_charset='utf8', ) Index( 'fsrss', policy_result_table.c.key, policy_result_table.c.sender, policy_result_table.c.recipient, policy_result_table.c.sasl_username, policy_result_table.c.sasl_sender, unique=True ) class PolicyResult(object): def __init__( self, key=None, value=None, sender=None, recipient=None, sasl_username=None, sasl_sender=None, data=None ): self.key = key self.value = value self.sender = sender self.sasl_username = sasl_username self.sasl_sender = sasl_sender self.recipient = recipient self.created = (int)(time.time()) self.data = data mapper(PolicyResult, policy_result_table) statistic_table = Table( 'statistic', metadata, Column('id', Integer, Sequence('seq_id_statistic'), primary_key=True), Column('sender', String(254), nullable=False), Column('recipient', String(254), nullable=False), Column('date', Date, nullable=False), Column('count', Integer, nullable=False), mysql_charset='utf8', ) Index( 'srd', statistic_table.c.sender, statistic_table.c.recipient, statistic_table.c.date, unique=True ) class Statistic(object): def __init__(self, sender, recipient, date=datetime.date.today(), count=0): self.sender = sender self.recipient = recipient self.date = date self.count = count mapper(Statistic, statistic_table) class PolicyRequest(object): email_address_keys = ['sender', 'recipient'] recipients = [] auth = None sasl_domain = None sasl_user = None sender_domain = None sender_user = None sasl_user_uses_alias = False sasl_user_is_delegate = False def __init__(self, policy_request={}): """ Creates a new policy request object. Pass it a policy_request dictionary as described in the Postfix documentation on: http://www.postfix.org/SMTPD_POLICY_README.html """ - for key in policy_request.keys(): + for key in policy_request: # Normalize email addresses (they may contain recipient delimiters) if key in self.email_address_keys: policy_request[key] = normalize_address(policy_request[key]) if not key == 'recipient': if policy_request[key] == '': setattr(self, key, None) else: setattr(self, key, policy_request[key]) else: if not policy_request['recipient'].strip() == '': self.recipients = list( set(self.recipients + [policy_request['recipient']]) ) def add_request(self, policy_request={}): """ Add subsequent policy requests to the existing policy request. All data in the request should be the same as the initial policy request, but for the recipient - with destination limits set over 1, Postfix may attempt to deliver messages to more then one recipient during a single delivery attempt, and during submission, the policy will receive one policy request per recipient. """ # Check instance. Not sure what to do if the instance is not the same. if hasattr(self, 'instance'): if not policy_request['instance'] == self.instance: # TODO: We need to empty our pockets pass log.debug( _("Adding policy request to instance %s") % (self.instance), level=8 ) # Normalize email addresses (they may contain recipient delimiters) if 'recipient' in policy_request: policy_request['recipient'] = normalize_address(policy_request['recipient']) if not policy_request['recipient'].strip() == '': self.recipients = list( set(self.recipients + [policy_request['recipient']]) ) def parse_ldap_dn(self, dn): """ See if parameter 'dn' is a basestring LDAP dn, and if so, return the results we can obtain from said DN. Return a list of relevant attribute values. If not a DN, return None. """ values = [] try: import ldap.dn ldap_dn = ldap.dn.explode_dn(dn) except ldap.DECODING_ERROR: # This is not a DN. return None if len(ldap_dn) > 0: search_attrs = conf.get_list('kolab_smtp_access_policy', 'address_search_attrs') rule_subject = self.auth.get_user_attributes( self.sasl_domain, {'dn': dn}, search_attrs + ['objectclass'] ) for search_attr in search_attrs: if search_attr in rule_subject: if isinstance(rule_subject[search_attr], basestring): values.append(rule_subject[search_attr]) else: values.extend(rule_subject[search_attr]) return values else: # ldap.dn.explode_dn didn't error out, but it also didn't split # the DN properly. return None def parse_ldap_uri(self, uri): values = [] parsed_uri = utils.parse_ldap_uri(uri) if parsed_uri is None: return None (_protocol, _server, _port, _base_dn, _attrs, _scope, _filter) = parsed_uri if len(_attrs) == 0: search_attrs = conf.get_list('kolab_smtp_access_policy', 'address_search_attrs') else: search_attrs = [_attrs] self.auth._auth._bind() _users = self.auth._auth._search( _base_dn, scope=LDAP_SCOPE[_scope], filterstr=_filter, attrlist=search_attrs + ['objectclass'], override_search="_regular_search" ) for _user in _users: for search_attr in search_attrs: values.extend(_user[1][search_attr]) return values def parse_policy(self, _subject, _object, policy): """ Parse policy to apply on _subject, for object _object. The policy is a list of rules. The _subject is a sender for kolabAllowSMTPRecipient checks, and a recipient for kolabAllowSMTPSender checks. The _object is a recipient for kolabAllowSMTPRecipient checks, and a sender for kolabAllowSMTPSender checks. """ special_rule_values = { '$mydomains': expand_mydomains } rules = {'allow': [], 'deny': []} if isinstance(policy, basestring): policy = [policy] for rule in policy: # Find rules that are actually special values, simply by # mapping the rule onto a key in "special_rule_values", a # dictionary with the corresponding value set to a function to # execute. - if rule in special_rule_values.keys(): + if rule in special_rule_values: special_rules = special_rule_values[rule]() if rule.startswith("-"): rules['deny'].extend(special_rules) else: rules['allow'].extend(special_rules) continue # Lower-case the rule rule = rule.lower() # Also note the '-' cannot be passed on to the functions that # follow, so store the rule separately from the prefix that is # prepended to deny rules. if rule.startswith("-"): _prefix = '-' _rule = rule[1:] else: _prefix = '' _rule = rule # See if the value is an LDAP DN ldap_dn = self.parse_ldap_dn(_rule) if ldap_dn is not None and len(ldap_dn) > 0: if _prefix == '-': rules['deny'].extend(ldap_dn) else: rules['allow'].extend(ldap_dn) else: ldap_uri = self.parse_ldap_uri(_rule) if ldap_uri is not None and len(ldap_uri) > 0: if _prefix == '-': rules['deny'].extend(ldap_uri) else: rules['allow'].extend(ldap_uri) else: if rule.startswith("-"): rules['deny'].append(rule[1:]) else: rules['allow'].append(rule) allowed = False for rule in rules['allow']: deny_override = False if _object is not None and _object.endswith(rule): for deny_rule in rules['deny']: if deny_rule.endswith(rule): deny_override = True if not deny_override: allowed = True denied = False for rule in rules['deny']: allow_override = False if _object is not None and _object.endswith(rule): if not allowed: denied = True continue else: for allow_rule in rules['allow']: if allow_rule.endswith(rule): allow_override = True if not allow_override: denied = True if not denied: allowed = True return allowed def verify_alias(self): """ Verify whether the user authenticated for this policy request is using an alias of its primary authentication ID / attribute. John.Doe@example.org (mail) for example could be sending with envelope sender jdoe@example.org (mailAlternateAddress, alias). """ search_attrs = conf.get_list(self.sasl_domain, 'address_search_attrs') if search_attrs is None or \ (isinstance(search_attrs, list) and len(search_attrs) == 0): search_attrs = conf.get_list(self.sasl_domain, 'mail_attributes') if search_attrs is None or \ (isinstance(search_attrs, list) and len(search_attrs) == 0): search_attrs = conf.get_list('kolab_smtp_access_policy', 'address_search_attrs') if search_attrs is None or \ (isinstance(search_attrs, list) and len(search_attrs) == 0): search_attrs = conf.get_list( conf.get('kolab', 'auth_mechanism'), 'mail_attributes' ) want_attrs = [] for search_attr in search_attrs: if search_attr not in self.sasl_user: want_attrs.append(search_attr) if len(want_attrs) > 0: self.sasl_user.update( self.auth.get_user_attributes( self.sasl_domain, self.sasl_user, want_attrs ) ) # Catch a user using one of its own alias addresses. for search_attr in search_attrs: if search_attr in self.sasl_user: if isinstance(self.sasl_user[search_attr], list): if self.sender.lower() in [x.lower() for x in self.sasl_user[search_attr]]: return True elif self.sasl_user[search_attr].lower() == self.sender.lower(): return True return False def verify_authenticity(self): """ Verify that the SASL username or lack thereof corresponds with allowing or disallowing authenticated users. If an SASL username is supplied, use it to obtain the authentication database user object including all attributes we may find ourselves interested in. """ if self.sasl_username is None: if not conf.allow_unauthenticated: reject(_("Unauthorized access not allowed")) else: # If unauthenticated is allowed, I have nothing to do here. return True sasl_username = self.sasl_username # If we have an sasl_username, find the user object in the # authentication database, along with the attributes we are # interested in. if self.sasl_domain is None: if len(self.sasl_username.split('@')) > 1: self.sasl_domain = self.sasl_username.split('@')[1] else: self.sasl_domain = conf.get('kolab', 'primary_domain') sasl_username = "%s@%s" % ( self.sasl_username, self.sasl_domain ) if self.auth is None: self.auth = Auth(self.sasl_domain) elif not self.auth.domain == self.sasl_domain: self.auth = Auth(self.sasl_domain) sasl_users = self.auth.find_recipient(sasl_username, domain=self.sasl_domain) if isinstance(sasl_users, list): if len(sasl_users) == 0: log.error(_("Could not find recipient")) return False else: self.sasl_user = {'dn': sasl_users[0]} elif isinstance(sasl_users, basestring): self.sasl_user = {'dn': sasl_users} if not self.sasl_user['dn']: # Got a final answer here, do the caching thing. cache_update( function='verify_sender', sender=self.sender, recipients=self.recipients, result=(int)(False), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) reject( _("Could not find envelope sender user %s (511)") % ( self.sasl_username ) ) attrs = conf.get_list(self.sasl_domain, 'auth_attributes') if attrs is None or (isinstance(attrs, list) and len(attrs) == 0): attrs = conf.get_list( conf.get('kolab', 'auth_mechanism'), 'auth_attributes' ) mail_attrs = conf.get_list(self.sasl_domain, 'mail_attributes') if mail_attrs is None or \ (isinstance(mail_attrs, list) and len(mail_attrs) == 0): mail_attrs = conf.get_list( conf.get('kolab', 'auth_mechanism'), 'mail_attributes' ) if mail_attrs is not None: attrs.extend(mail_attrs) attrs.extend( [ 'kolabAllowSMTPRecipient', 'kolabAllowSMTPSender' ] ) attrs = list(set(attrs)) user_attrs = self.auth.get_user_attributes( self.sasl_domain, self.sasl_user, attrs ) user_attrs['dn'] = self.sasl_user['dn'] self.sasl_user = utils.normalize(user_attrs) log.debug( _("Obtained authenticated user details for %r: %r") % ( self.sasl_user['dn'], self.sasl_user.keys() ), level=8 ) def verify_delegate(self): """ Verify whether the authenticated user is a delegate of the envelope sender. """ sender_is_delegate = False if self.sender_domain is None: if len(self.sender.split('@')) > 1: self.sender_domain = self.sender.split('@')[1] else: self.sender_domain = conf.get('kolab', 'primary_domain') if self.sender == self.sasl_username: return True search_attrs = conf.get_list(self.sender_domain, 'mail_attributes') if search_attrs is None: search_attrs = conf.get_list( conf.get('kolab', 'auth_mechanism'), 'mail_attributes' ) sender_users = self.auth.find_recipient( self.sender, domain=self.sender_domain ) if isinstance(sender_users, list): if len(sender_users) > 1: # More then one sender user with this recipient address. # TODO: check each of the sender users found. self.sender_user = {'dn': sender_users[0]} elif len(sender_users) == 1: self.sender_user = {'dn': sender_users} else: self.sender_user = {'dn': False} elif isinstance(sender_users, basestring): self.sender_user = {'dn': sender_users} if not self.sender_user['dn']: cache_update( function='verify_sender', sender=self.sender, recipients=self.recipients, result=(int)(False), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) reject(_("Could not find envelope sender user %s") % (self.sender)) attrs = search_attrs attrs.extend( [ 'kolabAllowSMTPRecipient', 'kolabAllowSMTPSender', 'kolabDelegate' ] ) user_attrs = self.auth.get_user_attributes( self.sender_domain, self.sender_user, attrs ) user_attrs['dn'] = self.sender_user['dn'] self.sender_user = utils.normalize(user_attrs) if 'kolabdelegate' not in self.sender_user: # Got a final answer here, do the caching thing. if cache is not False: cache_update( function='verify_sender', sender=self.sender, recipients=self.recipients, result=(int)(False), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) reject( _("%s is unauthorized to send on behalf of %s") % ( self.sasl_user['dn'], self.sender_user['dn'] ) ) elif self.sender_user['kolabdelegate'] is None: # No delegates for this sender could be found. The user is # definitely NOT a delegate of the sender. log.warning( _("User %s attempted to use envelope sender address %s without authorization") % ( policy_request["sasl_username"], policy_request["sender"] ) ) # Got a final answer here, do the caching thing. if cache is not False: cache_update( function='verify_sender', sender=self.sender, recipients=self.recipients, result=(int)(False), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) reject( _("%s is unauthorized to send on behalf of %s") % ( self.sasl_user['dn'], self.sender_user['dn'] ) ) else: # See if we can match the value of the envelope sender delegates to # the actual sender sasl_username if self.sasl_user is None: sasl_users = self.auth.find_recipient( self.sasl_username, domain=self.sasl_domain ) if isinstance(sasl_users, list): if len(sasl_users) == 0: log.error(_("Could not find recipient")) return False else: self.sasl_user = {'dn': sasl_users[0]} elif isinstance(sasl_users, basestring): self.sasl_user = {'dn': sasl_users} # Possible values for the kolabDelegate attribute are: # a 'uid', a 'dn'. if 'uid' not in self.sasl_user: self.sasl_user['uid'] = self.auth.get_user_attribute( self.sasl_domain, self.sasl_user, 'uid' ) sender_delegates = self.sender_user['kolabdelegate'] if not type(sender_delegates) == list: sender_delegates = [sender_delegates] for sender_delegate in sender_delegates: if self.sasl_user['dn'] == sender_delegate: log.debug( _("Found user %s to be a delegate user of %s") % ( policy_request["sasl_username"], policy_request["sender"] ), level=8 ) sender_is_delegate = True elif self.sasl_user['uid'] == sender_delegate: log.debug( _("Found user %s to be a delegate user of %s") % ( policy_request["sasl_username"], policy_request["sender"] ), level=8 ) sender_is_delegate = True return sender_is_delegate def verify_recipient(self, recipient): """ Verify whether the sender is allowed send to this recipient, using the recipient's kolabAllowSMTPSender. """ self.recipient = recipient if not self.sasl_username == '' and self.sasl_username is not None: log.debug( _("Verifying authenticated sender '%(sender)s' with sasl_username '%(sasl_username)s' for recipient '%(recipient)s'") % ( self.__dict__ ), level=8 ) else: log.debug( _( "Verifying unauthenticated sender '%(sender)s' for recipient '%(recipient)s'" ) % ( self.__dict__ ), level=8 ) recipient_verified = False if cache is not False: records = cache_select( function='verify_recipient', sender=self.sender, recipient=recipient, sasl_username=self.sasl_username, sasl_sender=self.sasl_sender, ) if records is not None and len(records) == 1: log.info( _("Reproducing verify_recipient(%s, %s) from cache") % ( self.sender, recipient ) ) return records[0].value # TODO: Under some conditions, the recipient may not be fully qualified. # We'll cross that bridge when we get there, though. if len(recipient.split('@')) > 1: sasl_domain = recipient.split('@')[1] else: sasl_domain = conf.get('kolab', 'primary_domain') recipient = "%s@%s" % (recipient, sasl_domain) if not verify_domain(sasl_domain): if cache is not False: cache_update( function='verify_recipient', sender=self.sender, recipient=recipient, result=(int)(True), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) return True if self.auth is None: self.auth = Auth(sasl_domain) elif not self.auth.domain == sasl_domain: self.auth = Auth(sasl_domain) if verify_domain(sasl_domain): if self.auth.domains is not None and sasl_domain in self.auth.domains: log.debug( _("Using authentication domain %s instead of %s") % ( self.auth.domains[sasl_domain], sasl_domain ), level=8 ) sasl_domain = self.auth.domains[sasl_domain] else: log.debug( _("Domain %s is a primary domain") % (sasl_domain), level=8 ) else: log.warning( _("Checking the recipient for domain %s that is not ours.") % (sasl_domain) ) return True recipients = self.auth.find_recipient( normalize_address(recipient), domain=sasl_domain, ) if isinstance(recipients, list): if len(recipients) > 1: log.info( _("This recipient address is related to multiple object entries and the SMTP Access Policy can therefore not restrict message flow") ) cache_update( function='verify_recipient', sender=self.sender, recipient=normalize_address(recipient), result=(int)(True), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) return True elif len(recipients) == 1: _recipient = {'dn': recipients[0]} else: log.debug( _("Recipient address %r not found. Allowing since the MTA was configured to accept the recipient.") % ( normalize_address(recipient) ), level=3 ) cache_update( function='verify_recipient', sender=self.sender, recipient=normalize_address(recipient), result=(int)(True), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) return True elif isinstance(recipients, basestring): _recipient = {'dn': recipients} # We have gotten an invalid recipient. We need to catch this case, # because testing can input invalid recipients, and so can faulty # applications, or misconfigured servers. if not _recipient['dn']: if not conf.allow_unauthenticated: cache_update( function='verify_recipient', sender=self.sender, recipient=normalize_address(recipient), result=(int)(False), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) reject(_("Invalid recipient")) else: cache_update( function='verify_recipient', sender=self.sender, recipient=normalize_address(recipient), result=(int)(True), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) log.debug(_("Could not find this user, accepting"), level=8) return True if _recipient['dn'] is not False: recipient_policy = self.auth.get_entry_attribute( sasl_domain, _recipient['dn'], 'kolabAllowSMTPSender' ) # If no such attribute has been specified, allow if recipient_policy is None: cache_update( function='verify_recipient', sender=self.sender, recipient=normalize_address(recipient), result=(int)(True), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) recipient_verified = True # Otherwise, parse the policy obtained with the subject of the policy # being the recipient, and the object to apply the policy to being the # sender. else: recipient_verified = self.parse_policy( recipient, self.sender, recipient_policy ) cache_update( function='verify_recipient', sender=self.sender, recipient=normalize_address(recipient), result=(int)(recipient_verified), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender ) return recipient_verified def verify_recipients(self): """ Verify whether the sender is allowed send to the recipients in this policy request, using each recipient's kolabAllowSMTPSender. Note there may be multiple recipients in this policy request, and therefor self.recipients is a list - walk through that list. """ recipients_verified = True if cache is not False: records = cache_select( function='verify_recipient', sender=self.sender, recipients=self.recipients, sasl_username=self.sasl_username, sasl_sender=self.sasl_sender, ) if records is not None and len(records) == len(self.recipients): log.debug("Euh, what am I doing here?") for record in records: recipient_found = False for recipient in self.recipients: if recipient == record.recipient: recipient_found = True if not recipient_found: reject( _("Sender %s is not allowed to send to recipient %s") % ( self.sender, recipient ) ) for recipient in self.recipients: recipient_verified = self.verify_recipient(recipient) if not recipient_verified: recipients_verified = False return recipients_verified def verify_sender(self): """ Verify the sender's access policy. 1) Verify whether the sasl_username is allowed to send using the envelope sender address, with the kolabDelegate attribute associated with the LDAP object that has the envelope sender address. 2) Verify whether the sender is allowed to send to recipient(s) listed on the sender's object. A third potential action could be to check the recipient object to see if the sender is allowed to send to the recipient by the recipient's kolabAllowSMTPSender, but this is done in verify_recipients(). """ sender_verified = False if self.sender is None: # Trusted host? if not hasattr(self, 'client_address') or \ self.client_address == "" or \ self.client_address is None: # Nothing to compare to. return False try: import netaddr networks = conf.get_list( 'kolab_smtp_access_policy', 'empty_sender_hosts' ) for network in networks: if netaddr.IPNetwork(self.client_address) in netaddr.IPNetwork(network): return True except ImportError as errmsg: return False if cache is not False: records = cache_select( sender=self.sender, recipients=self.recipients, sasl_username=self.sasl_username, sasl_sender=self.sasl_sender, function='verify_sender' ) if records is not None and len(records) == len(self.recipients): log.info( _("Reproducing verify_sender(%r) from cache") % (self.__dict__) ) for record in records: recipient_found = False for recipient in self.recipients: if recipient == record.recipient: recipient_found = True if recipient_found: if not record.value: reject( _("Sender %s is not allowed to send to recipient %s") % ( self.sender, recipient ) ) if record.data is not None: self.__dict__.update(record.data) return True if self.verify_authenticity() is False: reject(_("Unverifiable sender.")) if self.sasl_user is not None: self.sasl_user_uses_alias = self.verify_alias() if not self.sasl_user_uses_alias: log.debug(_("Sender is not using an alias"), level=8) self.sasl_user_is_delegate = self.verify_delegate() # If the authenticated user is using delegate functionality, apply the # recipient policy attribute for the envelope sender. if self.sasl_user_is_delegate is False: if self.sasl_user_uses_alias is False: if not conf.allow_unauthenticated: reject(_("Sender uses unauthorized envelope sender address")) if self.sasl_user_is_delegate: # Apply the recipient policy for the sender using the envelope # sender user object. recipient_policy_domain = self.sender_domain recipient_policy_sender = self.sender recipient_policy_user = self.sender_user elif self.sasl_user is not None: # Apply the recipient policy from the authenticated user. recipient_policy_domain = self.sasl_domain recipient_policy_sender = self.sasl_username recipient_policy_user = self.sasl_user else: if not conf.allow_unauthenticated: reject(_("Could not verify sender")) else: recipient_policy_domain = self.sender_domain recipient_policy_sender = self.sender recipient_policy_user = self.sender_user log.debug( _("Verifying whether sender is allowed to send to recipient using sender policy"), level=8 ) if 'kolaballowsmtprecipient' in recipient_policy_user: recipient_policy = recipient_policy_user['kolaballowsmtprecipient'] else: recipient_policy = self.auth.get_user_attribute( recipient_policy_domain, recipient_policy_user, 'kolabAllowSMTPRecipient' ) log.debug(_("Result is %r") % (recipient_policy), level=8) # If no such attribute has been specified, allow if recipient_policy is None: log.debug( _("No recipient policy restrictions exist for this sender"), level=8 ) sender_verified = True # Otherwise,parse the policy obtained. else: log.debug( _("Found a recipient policy to apply for this sender."), level=8 ) recipient_allowed = None for recipient in self.recipients: recipient_allowed = self.parse_policy( recipient_policy_sender, recipient, recipient_policy ) if not recipient_allowed: reject( _("Sender %s not allowed to send to recipient %s") % ( recipient_policy_user['dn'], recipient ) ) sender_verified = True if cache is not False: data = { 'sasl_user_uses_alias': self.sasl_user_uses_alias, 'sasl_user_is_delegate': self.sasl_user_is_delegate, 'sender_domain': self.sender_domain, } cache_update( function='verify_sender', sender=self.sender, recipients=self.recipients, result=(int)(sender_verified), sasl_username=self.sasl_username, sasl_sender=self.sasl_sender, data=data ) return sender_verified def cache_cleanup(): if cache is not True: return log.debug(_("Cleaning up the cache"), level=8) session.query(PolicyResult).filter( PolicyResult.created < ((int)(time.time()) - cache_expire) ).delete() session.commit() def cache_init(): global cache, cache_expire, session if conf.has_section('kolab_smtp_access_policy'): if conf.has_option('kolab_smtp_access_policy', 'cache_uri'): cache_uri = conf.get('kolab_smtp_access_policy', 'cache_uri') cache = True if conf.has_option('kolab_smtp_access_policy', 'retention'): cache_expire = (int)( conf.get( 'kolab_smtp_access_policy', 'retention' ) ) else: return False else: return False if conf.debuglevel > 8: engine = create_engine(cache_uri, echo=True) else: engine = create_engine(cache_uri, echo=False) if conf.drop_tables: log.info(_("Dropping caching tables from database")) policy_result_table.drop(engine, checkfirst=True) statistic_table.drop(engine, checkfirst=True) return False try: metadata.create_all(engine) except sqlalchemy.exc.OperationalError as e: log.error(_("Operational Error in caching: %s" % (e))) return False Session = sessionmaker(bind=engine) session = Session() cache_cleanup() return cache def cache_select( function, sender, recipient='', recipients=[], sasl_username='', sasl_sender=''): if cache is not True: return None if not recipient == '' and recipients == []: recipients = [recipient] return session.query( PolicyResult ).filter_by( key=function, sender=sender, sasl_username=sasl_username, sasl_sender=sasl_sender ).filter( PolicyResult.recipient.in_(recipients) ).filter( PolicyResult.created >= ((int)(time.time()) - cache_expire) ).all() def cache_insert( function, sender, recipient='', recipients=[], result=None, sasl_username='', sasl_sender='', data=None): if cache is not True: return [] log.debug( _("Caching the policy result with timestamp %d") % ( (int)(time.time()) ), level=8 ) if not recipient == '' and recipients == []: recipients = [recipient] for recipient in recipients: session.add( PolicyResult( key=function, value=result, sender=sender, recipient=recipient, sasl_username=sasl_username, sasl_sender=sasl_sender, data=data ) ) session.commit() def cache_update( function, sender, recipient='', recipients=[], result=None, sasl_username='', sasl_sender='', data=None): """ Insert an updated set of rows into the cache depending on the necessity """ if cache is not True: return records = [] _records = cache_select( function, sender, recipient, recipients, sasl_username, sasl_sender ) for record in _records: if record.value == (int)(result): records.append(record) if not recipient == '' and recipients == []: recipients = [recipient] for recipient in recipients: recipient_found = False for record in records: if record.recipient == recipient: recipient_found = True if not recipient_found: cache_insert( function=function, sender=sender, recipient=recipient, result=result, sasl_username=sasl_username, sasl_sender=sasl_sender, data=data ) def defer_if_permit(message, policy_request=None): log.info(_("Returning action DEFER_IF_PERMIT: %s") % (message)) print("action=DEFER_IF_PERMIT %s\n\n" % (message)) sys.exit(0) def dunno(message, policy_request=None): log.info(_("Returning action DUNNO: %s") % (message)) print("action=DUNNO %s\n\n" % (message)) sys.exit(0) def hold(message, policy_request=None): log.info(_("Returning action HOLD: %s") % (message)) print("action=HOLD %s\n\n" % (message)) sys.exit(0) def permit(message, policy_request=None): log.info(_("Returning action PERMIT: %s") % (message)) # If we have no policy request, we have been called for a reason, # and everything relevant has been figured out already. if policy_request is None: print("action=PERMIT\n\n") sys.exit(0) # If the user is not authenticated, there's no reason to do # extra checks here -- to have been performed already. if not hasattr(policy_request, 'sasl_username'): print("action=PERMIT\n\n") sys.exit(0) # Same here. if policy_request.sasl_username is None: print("action=PERMIT\n\n") sys.exit(0) delegate_sender_header = None alias_sender_header = None # If the sender is a delegate of the envelope sender address, take into # account the preferred domain policy for appending the Sender and/or # X-Sender headers. # # Note that a delegatee by very definition is not using an alias. # if policy_request.sasl_user_is_delegate: # Domain-specific setting? if policy_request.sender_domain is not None: delegate_sender_header = conf.get(policy_request.sender_domain, 'delegate_sender_header') # Global setting? if delegate_sender_header is None: delegate_sender_header = conf.get('kolab_smtp_access_policy', 'delegate_sender_header') # Default if delegate_sender_header is None: delegate_sender_header = True # If the sender is using an alias as the envelope sender address, take # into account the preferred domain policy for appending the Sender # and/or X-Sender headers. elif policy_request.sasl_user_uses_alias: # Domain-specific setting? if policy_request.sender_domain is not None: alias_sender_header = conf.get(policy_request.sender_domain, 'alias_sender_header') # Global setting? if alias_sender_header is None: alias_sender_header = conf.get('kolab_smtp_access_policy', 'alias_sender_header') # Default if alias_sender_header is None: alias_sender_header = True # Make the values booleans delegate_sender_header = utils.true_or_false(delegate_sender_header) alias_sender_header = utils.true_or_false(alias_sender_header) # Do we use a (simple) encryption key to obscure the headers' contents? # Note that using an encryption key voids the actual use of proper Sender # and X-Sender headers such as they could be interpreted by a client # application. enc_key = None if policy_request.sender_domain is not None: enc_key = conf.get(policy_request.sender_domain, 'sender_header_enc_key') if enc_key is None: enc_key = conf.get('kolab_smtp_access_policy', 'sender_header_enc_key') sender_header = None xsender_header = None if delegate_sender_header or alias_sender_header: # Domain specific? sender_header = conf.get(policy_request.sender_domain, 'sender_header') # Global setting? if sender_header is None: sender_header = conf.get('kolab_smtp_access_policy', 'sender_header') # Default if sender_header is None: sender_header = True # Domain specific? xsender_header = conf.get(policy_request.sender_domain, 'xsender_header') # Global setting? if xsender_header is None: xsender_header = conf.get('kolab_smtp_access_policy', 'xsender_header') # Default if xsender_header is None: xsender_header = True # Note that if the user is not a delegatee, and not using an alias, the sender # address is the envelope sender address, and the defaults for sender_header # and xsender_header being None, ultimately evaluating to False seems # appropriate. # Make the values booleans sender_header = utils.true_or_false(sender_header) xsender_header = utils.true_or_false(xsender_header) if sender_header or xsender_header: # Do the encoding, if any if enc_key is not None: header = 'X-Authenticated-As' xheader = None sender = utils.encode(enc_key, policy_request.sasl_username) else: header = 'Sender' xheader = 'X-Sender' sender = policy_request.sasl_username if sender_header: print("action=PREPEND %s: %s" % (header, sender)) if xsender_header and xheader is not None: print("action=PREPEND %s: %s" % (xheader, sender)) print("action=PERMIT\n\n") sys.exit(0) def reject(message, policy_request=None): log.info(_("Returning action REJECT: %s") % (message)) print("action=REJECT %s\n\n" % (message)) sys.exit(0) def expand_mydomains(): """ Return a list of my domains. """ global auth, mydomains if mydomains is not None: return mydomains auth.connect() mydomains = auth.list_domains() return mydomains.keys() def normalize_address(email_address): """ Parse an address; Strip off anything after a recipient delimiter. """ # TODO: Recipient delimiter is configurable. if len(email_address.split("+")) > 1: # Take the first part split by recipient delimiter and the last part # split by '@'. return "%s@%s" % ( email_address.split("+")[0].lower(), # TODO: Under some conditions, the recipient may not be fully # qualified. We'll cross that bridge when we get there, though. email_address.split('@')[1].lower() ) else: return email_address.lower() def read_request_input(): """ Read a single policy request from sys.stdin, and return a dictionary containing the request. """ start_time = time.time() log.debug(_("Starting to loop for new request")) policy_request = {} end_of_request = False while not end_of_request: if (time.time() - start_time) >= conf.timeout: log.warning(_("Timeout for policy request reading exceeded")) sys.exit(0) request_line = sys.stdin.readline() if request_line.strip() == '': if 'request' in policy_request: log.debug(_("End of current request"), level=8) end_of_request = True else: request_line = request_line.strip() log.debug(_("Getting line: %s") % (request_line), level=8) policy_request[request_line.split('=')[0]] = \ '='.join(request_line.split('=')[1:]).lower() log.debug(_("Returning request")) return policy_request def verify_domain(domain): """ Verify whether the domain is internal (mine) or external. """ global auth, mydomains if mydomains is not None: - return domain in mydomains.keys() + return domain in mydomains auth.connect() domain_verified = False mydomains = auth.list_domains() if mydomains is not None and domain in mydomains: domain_verified = True else: domain_verified = False return domain_verified if __name__ == "__main__": access_policy_group = conf.add_cli_parser_option_group( _("Access Policy Options") ) access_policy_group.add_option( "--drop-tables", dest="drop_tables", action="store_true", default=False, help=_("Drop the caching tables from the database and exit.") ) access_policy_group.add_option( "--timeout", dest="timeout", action="store", default=10, help=_("SMTP Policy request timeout.") ) access_policy_group.add_option( "--verify-recipient", dest="verify_recipient", action="store_true", default=False, help=_("Verify the recipient access policy.") ) access_policy_group.add_option( "--verify-sender", dest="verify_sender", action="store_true", default=False, help=_("Verify the sender access policy.") ) access_policy_group.add_option( "--allow-unauthenticated", dest="allow_unauthenticated", action="store_true", default=False, help=_("Allow unauthenticated senders.") ) conf.finalize_conf() auth = Auth() cache = cache_init() policy_requests = {} if conf.drop_tables: sys.exit(0) # Start the work while True: policy_request = read_request_input() instance = policy_request['instance'] log.debug(_("Got request instance %s") % (instance)) if instance in policy_requests: policy_requests[instance].add_request(policy_request) else: policy_requests[instance] = PolicyRequest(policy_request) protocol_state = policy_request['protocol_state'].strip().lower() log.debug( _("Request instance %s is in state %s") % ( instance, protocol_state ) ) if not protocol_state == 'data': print("action=DUNNO\n\n") sys.stdout.flush() # We can recognize being in the DATA part by the recipient_count being # set to a non-zero value and the protocol_state being set to 'data'. # Note that the input we're getting is a string, not an integer. else: sender_allowed = False recipient_allowed = False try: if conf.verify_sender: sender_allowed = policy_requests[instance].verify_sender() else: sender_allowed = True if conf.verify_recipient: recipient_allowed = policy_requests[instance].verify_recipients() else: recipient_allowed = True except Exception as errmsg: import traceback log.error(_("Unhandled exception caught: %r") % (errmsg)) log.error(traceback.format_exc()) if not sender_allowed: reject(_("Sender access denied")) elif not recipient_allowed: reject(_("Recipient access denied")) else: permit(_("No objections"), policy_requests[instance]) diff --git a/ext/python/Tools/freeze/checkextensions.py b/ext/python/Tools/freeze/checkextensions.py index 334521f..078f5d7 100644 --- a/ext/python/Tools/freeze/checkextensions.py +++ b/ext/python/Tools/freeze/checkextensions.py @@ -1,90 +1,90 @@ # Check for a module in a set of extension directories. # An extension directory should contain a Setup file # and one or more .o files or a lib.a file. import os import parsesetup def checkextensions(unknown, extensions): files = [] modules = [] edict = {} for e in extensions: setup = os.path.join(e, 'Setup') liba = os.path.join(e, 'lib.a') if not os.path.isfile(liba): liba = None edict[e] = parsesetup.getsetupinfo(setup), liba for mod in unknown: for e in extensions: (mods, vars), liba = edict[e] if not mods.has_key(mod): continue modules.append(mod) if liba: # If we find a lib.a, use it, ignore the # .o files, and use *all* libraries for # *all* modules in the Setup file if liba in files: break files.append(liba) - for m in mods.keys(): + for m in mods: files = files + select(e, mods, vars, m, 1) break files = files + select(e, mods, vars, mod, 0) break return files, modules def select(e, mods, vars, mod, skipofiles): files = [] for w in mods[mod]: w = treatword(w) if not w: continue w = expandvars(w, vars) for w in w.split(): if skipofiles and w[-2:] == '.o': continue # Assume $var expands to absolute pathname if w[0] not in ('-', '$') and w[-2:] in ('.o', '.a'): w = os.path.join(e, w) if w[:2] in ('-L', '-R') and w[2:3] != '$': w = w[:2] + os.path.join(e, w[2:]) files.append(w) return files cc_flags = ['-I', '-D', '-U'] cc_exts = ['.c', '.C', '.cc', '.c++'] def treatword(w): if w[:2] in cc_flags: return None if w[:1] == '-': return w # Assume loader flag head, tail = os.path.split(w) base, ext = os.path.splitext(tail) if ext in cc_exts: tail = base + '.o' w = os.path.join(head, tail) return w def expandvars(str, vars): i = 0 while i < len(str): i = k = str.find('$', i) if i < 0: break i = i+1 var = str[i:i+1] i = i+1 if var == '(': j = str.find(')', i) if j < 0: break var = str[i:j] i = j+1 if vars.has_key(var): str = str[:k] + vars[var] + str[i:] i = k return str diff --git a/ext/python/Tools/freeze/freeze.py b/ext/python/Tools/freeze/freeze.py index a7906b1..6f74ca2 100755 --- a/ext/python/Tools/freeze/freeze.py +++ b/ext/python/Tools/freeze/freeze.py @@ -1,503 +1,503 @@ #! /usr/bin/env python """Freeze a Python script into a binary. usage: freeze [options...] script [module]... Options: -p prefix: This is the prefix used when you ran ``make install'' in the Python build directory. (If you never ran this, freeze won't work.) The default is whatever sys.prefix evaluates to. It can also be the top directory of the Python source tree; then -P must point to the build tree. -P exec_prefix: Like -p but this is the 'exec_prefix', used to install objects etc. The default is whatever sys.exec_prefix evaluates to, or the -p argument if given. If -p points to the Python source tree, -P must point to the build tree, if different. -e extension: A directory containing additional .o files that may be used to resolve modules. This directory should also have a Setup file describing the .o files. On Windows, the name of a .INI file describing one or more extensions is passed. More than one -e option may be given. -o dir: Directory where the output files are created; default '.'. -m: Additional arguments are module names instead of filenames. -a package=dir: Additional directories to be added to the package's __path__. Used to simulate directories added by the package at runtime (eg, by OpenGL and win32com). More than one -a option may be given for each package. -l file: Pass the file to the linker (windows only) -d: Debugging mode for the module finder. -q: Make the module finder totally quiet. -h: Print this help message. -x module Exclude the specified module. It will still be imported by the frozen binary if it exists on the host system. -X module Like -x, except the module can never be imported by the frozen binary. -E: Freeze will fail if any modules can't be found (that were not excluded using -x or -X). -i filename: Include a file with additional command line options. Used to prevent command lines growing beyond the capabilities of the shell/OS. All arguments specified in filename are read and the -i option replaced with the parsed params (note - quoting args in this file is NOT supported) -s subsystem: Specify the subsystem (For Windows only.); 'console' (default), 'windows', 'service' or 'com_dll' -w: Toggle Windows (NT or 95) behavior. (For debugging only -- on a win32 platform, win32 behavior is automatic.) -r prefix=f: Replace path prefix. Replace prefix with f in the source path references contained in the resulting binary. Arguments: script: The Python script to be executed by the resulting binary. module ...: Additional Python modules (referenced by pathname) that will be included in the resulting binary. These may be .py or .pyc files. If -m is specified, these are module names that are search in the path instead. NOTES: In order to use freeze successfully, you must have built Python and installed it ("make install"). The script should not use modules provided only as shared libraries; if it does, the resulting binary is not self-contained. """ # Import standard modules import modulefinder import getopt import os import sys # Import the freeze-private modules import checkextensions import makeconfig import makefreeze import makemakefile import parsesetup import bkfile # Main program def main(): # overridable context prefix = None # settable with -p option exec_prefix = None # settable with -P option extensions = [] exclude = [] # settable with -x option addn_link = [] # settable with -l, but only honored under Windows. path = sys.path[:] modargs = 0 debug = 1 odir = '' win = sys.platform[:3] == 'win' replace_paths = [] # settable with -r option error_if_any_missing = 0 # default the exclude list for each platform if win: exclude = exclude + [ 'dos', 'dospath', 'mac', 'macpath', 'macfs', 'MACFS', 'posix', 'os2', 'ce', 'riscos', 'riscosenviron', 'riscospath', ] fail_import = exclude[:] # output files frozen_c = 'frozen.c' config_c = 'config.c' target = 'a.out' # normally derived from script name makefile = 'Makefile' subsystem = 'console' # parse command line by first replacing any "-i" options with the # file contents. pos = 1 while pos < len(sys.argv)-1: # last option can not be "-i", so this ensures "pos+1" is in range! if sys.argv[pos] == '-i': try: options = open(sys.argv[pos+1]).read().split() except IOError as why: usage("File name '%s' specified with the -i option " "can not be read - %s" % (sys.argv[pos+1], why) ) # Replace the '-i' and the filename with the read params. sys.argv[pos:pos+2] = options pos = pos + len(options) - 1 # Skip the name and the included args. pos = pos + 1 # Now parse the command line with the extras inserted. try: opts, args = getopt.getopt(sys.argv[1:], 'r:a:dEe:hmo:p:P:b:qs:wX:x:l:') except getopt.error as msg: usage('getopt error: ' + str(msg)) # process option arguments for o, a in opts: if o == '-h': print(__doc__) return if o == '-b': binlib = a if o == '-d': debug = debug + 1 if o == '-e': extensions.append(a) if o == '-m': modargs = 1 if o == '-o': odir = a if o == '-p': prefix = a if o == '-P': exec_prefix = a if o == '-q': debug = 0 if o == '-w': win = not win if o == '-s': if not win: usage("-s subsystem option only on Windows") subsystem = a if o == '-x': exclude.append(a) if o == '-X': exclude.append(a) fail_import.append(a) if o == '-E': error_if_any_missing = 1 if o == '-l': addn_link.append(a) if o == '-a': apply(modulefinder.AddPackagePath, tuple(a.split("=", 2))) if o == '-r': f,r = a.split("=", 2) replace_paths.append( (f,r) ) # modules that are imported by the Python runtime implicits = [] for module in ('site', 'warnings',): if module not in exclude: implicits.append(module) # default prefix and exec_prefix if not exec_prefix: if prefix: exec_prefix = prefix else: exec_prefix = sys.exec_prefix if not prefix: prefix = sys.prefix # determine whether -p points to the Python source tree ishome = os.path.exists(os.path.join(prefix, 'Python', 'ceval.c')) # locations derived from options version = sys.version[:3] if win: extensions_c = 'frozen_extensions.c' if ishome: print("(Using Python source directory)") binlib = exec_prefix incldir = os.path.join(prefix, 'Include') config_h_dir = exec_prefix config_c_in = os.path.join(prefix, 'Modules', 'config.c.in') frozenmain_c = os.path.join(prefix, 'Python', 'frozenmain.c') makefile_in = os.path.join(exec_prefix, 'Makefile') if win: frozendllmain_c = os.path.join(exec_prefix, 'Pc\\frozen_dllmain.c') else: if not binlib: binlib = os.path.join(exec_prefix, 'lib', 'python%s' % version, 'config') else: binlib = os.path.join(binlib, 'python%s' % version, 'config') incldir = os.path.join(prefix, 'include', 'python%s' % version) config_h_dir = os.path.join(exec_prefix, 'include', 'python%s' % version) config_c_in = os.path.join(binlib, 'config.c.in') frozenmain_c = os.path.join(binlib, 'frozenmain.c') makefile_in = os.path.join(binlib, 'Makefile') frozendllmain_c = os.path.join(binlib, 'frozen_dllmain.c') supp_sources = [] defines = [] includes = ['-I' + incldir, '-I' + config_h_dir] # sanity check of directories and files check_dirs = [prefix, exec_prefix, binlib, incldir] if not win: # These are not directories on Windows. check_dirs = check_dirs + extensions for dir in check_dirs: if not os.path.exists(dir): usage('needed directory %s not found' % dir) if not os.path.isdir(dir): usage('%s: not a directory' % dir) if win: files = supp_sources + extensions # extensions are files on Windows. else: files = [config_c_in, makefile_in] + supp_sources for file in supp_sources: if not os.path.exists(file): usage('needed file %s not found' % file) if not os.path.isfile(file): usage('%s: not a plain file' % file) if not win: for dir in extensions: setup = os.path.join(dir, 'Setup') if not os.path.exists(setup): usage('needed file %s not found' % setup) if not os.path.isfile(setup): usage('%s: not a plain file' % setup) # check that enough arguments are passed if not args: usage('at least one filename argument required') # check that file arguments exist for arg in args: if arg == '-m': break # if user specified -m on the command line before _any_ # file names, then nothing should be checked (as the # very first file should be a module name) if modargs: break if not os.path.exists(arg): usage('argument %s not found' % arg) if not os.path.isfile(arg): usage('%s: not a plain file' % arg) # process non-option arguments scriptfile = args[0] modules = args[1:] # derive target name from script name base = os.path.basename(scriptfile) base, ext = os.path.splitext(base) if base: if base != scriptfile: target = base else: target = base + '.bin' # handle -o option base_frozen_c = frozen_c base_config_c = config_c base_target = target if odir and not os.path.isdir(odir): try: os.mkdir(odir) print("Created output directory", odir) except os.error, msg: usage('%s: mkdir failed (%s)' % (odir, str(msg))) base = '' if odir: base = os.path.join(odir, '') frozen_c = os.path.join(odir, frozen_c) config_c = os.path.join(odir, config_c) target = os.path.join(odir, target) makefile = os.path.join(odir, makefile) if win: extensions_c = os.path.join(odir, extensions_c) # Handle special entry point requirements # (on Windows, some frozen programs do not use __main__, but # import the module directly. Eg, DLLs, Services, etc custom_entry_point = None # Currently only used on Windows python_entry_is_main = 1 # Is the entry point called __main__? # handle -s option on Windows if win: import winmakemakefile try: custom_entry_point, python_entry_is_main = \ winmakemakefile.get_custom_entry_point(subsystem) except ValueError as why: usage(why) # Actual work starts here... # collect all modules of the program dir = os.path.dirname(scriptfile) path[0] = dir mf = modulefinder.ModuleFinder(path, debug, exclude, replace_paths) if win and subsystem=='service': # If a Windows service, then add the "built-in" module. mod = mf.add_module("servicemanager") mod.__file__="dummy.pyd" # really built-in to the resulting EXE for mod in implicits: mf.import_hook(mod) for mod in modules: if mod == '-m': modargs = 1 continue if modargs: if mod[-2:] == '.*': mf.import_hook(mod[:-2], None, ["*"]) else: mf.import_hook(mod) else: mf.load_file(mod) # Add the main script as either __main__, or the actual module name. if python_entry_is_main: mf.run_script(scriptfile) else: mf.load_file(scriptfile) if debug > 0: mf.report() print() dict = mf.modules if error_if_any_missing: missing = mf.any_missing() if missing: sys.exit("There are some missing modules: %r" % missing) # generate output for frozen modules files = makefreeze.makefreeze(base, dict, debug, custom_entry_point, fail_import) # look for unfrozen modules (builtin and of unknown origin) builtins = [] unknown = [] mods = dict.keys() mods.sort() for mod in mods: if dict[mod].__code__: continue if not dict[mod].__file__: builtins.append(mod) else: unknown.append(mod) # search for unknown modules in extensions directories (not on Windows) addfiles = [] frozen_extensions = [] # Windows list of modules. if unknown or (not win and builtins): if not win: addfiles, addmods = \ checkextensions.checkextensions(unknown+builtins, extensions) for mod in addmods: if mod in unknown: unknown.remove(mod) builtins.append(mod) else: # Do the windows thang... import checkextensions_win32 # Get a list of CExtension instances, each describing a module # (including its source files) frozen_extensions = checkextensions_win32.checkextensions( unknown, extensions, prefix) for mod in frozen_extensions: unknown.remove(mod.name) # report unknown modules if unknown: sys.stderr.write('Warning: unknown modules remain: %s\n' % ' '.join(unknown)) # windows gets different treatment if win: # Taking a shortcut here... import winmakemakefile, checkextensions_win32 checkextensions_win32.write_extension_table(extensions_c, frozen_extensions) # Create a module definition for the bootstrap C code. xtras = [frozenmain_c, os.path.basename(frozen_c), frozendllmain_c, os.path.basename(extensions_c)] + files maindefn = checkextensions_win32.CExtension( '__main__', xtras ) frozen_extensions.append( maindefn ) outfp = open(makefile, 'w') try: winmakemakefile.makemakefile(outfp, locals(), frozen_extensions, os.path.basename(target)) finally: outfp.close() return # generate config.c and Makefile builtins.sort() infp = open(config_c_in) outfp = bkfile.open(config_c, 'w') try: makeconfig.makeconfig(infp, outfp, builtins) finally: outfp.close() infp.close() cflags = ['$(OPT)'] cppflags = defines + includes libs = [os.path.join(binlib, 'libpython$(VERSION).so')] somevars = {} if os.path.exists(makefile_in): makevars = parsesetup.getmakevars(makefile_in) - for key in makevars.keys(): + for key in makevars: somevars[key] = makevars[key] somevars['CFLAGS'] = ' '.join(cflags) # override somevars['CPPFLAGS'] = ' '.join(cppflags) # override files = [base_config_c, base_frozen_c] + \ files + supp_sources + addfiles + libs + \ ['$(MODLIBS)', '$(LIBS)', '$(SYSLIBS)'] outfp = bkfile.open(makefile, 'w') try: makemakefile.makemakefile(outfp, somevars, files, base_target) finally: outfp.close() # Done! if odir: print('Now run "make" in', odir, end=' ') print('to build the target:', base_target) else: print('Now run "make" to build the target:', base_target) # Print usage message and exit def usage(msg): sys.stdout = sys.stderr print("Error:", msg) print("Use ``%s -h'' for help" % sys.argv[0]) sys.exit(2) main() diff --git a/kolabd/__init__.py b/kolabd/__init__.py index 491eaef..b15fccd 100644 --- a/kolabd/__init__.py +++ b/kolabd/__init__.py @@ -1,397 +1,397 @@ # 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 . # """ The Kolab daemon. """ from __future__ import print_function import grp import os import pwd import shutil import sys import time import traceback import multiprocessing import pykolab from pykolab.auth import Auth from pykolab import constants from pykolab import utils from pykolab.translate import _ as _l from .process import KolabdProcess as Process # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.daemon') conf = pykolab.getConf() class KolabDaemon: def __init__(self): """ The main Kolab Groupware daemon process. """ 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( "-p", "--pid-file", dest="pidfile", action="store", default="/var/run/kolabd/kolabd.pid", help=_l("Path to the PID file to use.") ) daemon_group.add_option( "-u", "--user", dest="process_username", action="store", default="kolab", help=_l("Run as user USERNAME"), metavar="USERNAME" ) daemon_group.add_option( "-g", "--group", dest="process_groupname", action="store", default="kolab", help=_l("Run as group GROUPNAME"), metavar="GROUPNAME" ) conf.finalize_conf() # pylint: disable=too-many-branches # pylint: disable=too-many-statements def run(self): """Run Forest, RUN!""" exitcode = 0 utils.ensure_directory( os.path.dirname(conf.pidfile), conf.process_username, conf.process_groupname ) 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: log.error( _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: log.error( _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) 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_sync() 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_sync() except SystemExit as errcode: exitcode = errcode 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"), file=sys.stderr) 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"), file=sys.stderr) sys.exit(exitcode) # pylint: disable=no-self-use # pylint: disable=too-many-branches # pylint: disable=too-many-locals def do_sync(self): domain_auth = {} primary_domain = conf.get('kolab', 'primary_domain') while 1: primary_auth = Auth(primary_domain) connected = False while not connected: try: primary_auth.connect() connected = True except Exception as errmsg: connected = False log.error(_l("Could not connect to LDAP, is it running?")) log.error(_l("Error: %r") % (errmsg)) log.error("Traceback: %r" % (traceback.format_exc())) time.sleep(5) log.debug(_l("Listing domains..."), level=5) try: domains = primary_auth.list_domains() except Exception: time.sleep(60) continue if not domains: log.error(_l("No domains. Not syncing")) time.sleep(5) continue # domains now is a list of key-valye pairs in the format of # {'secondary': 'primary'}, we want the primaries primaries = list(set(domains.values())) # Store the naming contexts for the domains as # # {'domain': 'naming context'} # # and the domain root dns as # # {'domain': 'domain root dn'} # domain_root_dns = {} naming_contexts = {} for primary in primaries: naming_context = primary_auth.domain_naming_context(primary) # pylint: disable=protected-access domain_root_dn = primary_auth._auth._kolab_domain_root_dn(primary) log.debug( _l("Domain %r naming context: %r, root dn: %r") % ( primary, naming_context, domain_root_dn ), level=8 ) domain_root_dns[primary] = domain_root_dn.lower() naming_contexts[primary] = naming_context.lower() log.debug( _l("Naming contexts to synchronize: %r") % ( list(set(naming_contexts.values())) ), level=8 ) # Find however many naming contexts we have, and what the # corresponding domain name is for them. primary_domains = [x for x, y in naming_contexts.items() if domain_root_dns[x] == y] # Now we can check if any changes happened. added_domains = [] removed_domains = [] # Combine the domains from LDAP with the domain processes # accounted for locally. all_domains = list(set(primary_domains + domain_auth.keys())) log.debug(_l("Result set of domains: %r") % (all_domains), level=8) for domain in all_domains: log.debug(_l("Checking for domain %s") % (domain), level=8) - if domain in domain_auth.keys() and domain in primary_domains: + if domain in domain_auth and domain in primary_domains: if not domain_auth[domain].is_alive(): log.debug(_l("Domain %s isn't alive anymore.") % (domain), level=8) domain_auth[domain].terminate() added_domains.append(domain) else: log.debug(_l("Domain %s already there and alive.") % (domain), level=8) continue - elif domain in domain_auth.keys(): + elif domain in domain_auth: log.debug(_l("Domain %s should not exist any longer.") % (domain), level=8) removed_domains.append(domain) else: log.debug(_l("Domain %s does not have a process yet.") % (domain), level=8) added_domains.append(domain) if not removed_domains and not added_domains: try: sleep_between_domain_operations_in_seconds = (float)( conf.get( 'kolab', 'domain_sync_interval' ) ) time.sleep(sleep_between_domain_operations_in_seconds) except ValueError: time.sleep(600) log.debug( _l("added domains: %r, removed domains: %r") % (added_domains, removed_domains), level=8 ) for domain in added_domains: domain_auth[domain] = Process(domain) domain_auth[domain].start() # Pause or hammer your LDAP server to death if len(added_domains) >= 5: time.sleep(10) for domain in removed_domains: domain_auth[domain].terminate() del domain_auth[domain] def reload_config(self, *args, **kw): pass def remove_pid(self, *args, **kw): """ Remove our PID file. Note that multiple processes can attempt to do this very same thing at the same time, and therefore we need to test if the PID file exists, and only try/except removing it. """ try: for p in multiprocessing.active_children(): p.terminate() if os.access(conf.pidfile, os.R_OK): os.remove(conf.pidfile) except Exception: pass raise SystemExit 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() fp = open(conf.pidfile, 'w') fp.write("%d\n" % (pid)) fp.close() diff --git a/pykolab/auth/ldap/__init__.py b/pykolab/auth/ldap/__init__.py index b174579..a2edf12 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 # 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.keys() if s not in self.domains.values()] + 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.keys(): + 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.keys(): + 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.keys(): + 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.keys(): + 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.keys(): +# 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 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.keys(): + 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.keys(): + 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/auth/ldap/syncrepl.py b/pykolab/auth/ldap/syncrepl.py index 72f5f37..5b69c63 100644 --- a/pykolab/auth/ldap/syncrepl.py +++ b/pykolab/auth/ldap/syncrepl.py @@ -1,119 +1,119 @@ #!/usr/bin/python import anydbm import ldap import ldap.syncrepl import ldapurl import pykolab from pykolab import utils log = pykolab.getLogger('pykolab.syncrepl') conf = pykolab.getConf() class DNSync(ldap.ldapobject.LDAPObject,ldap.syncrepl.SyncreplConsumer): callback = None def __init__(self, filename, *args, **kwargs): if kwargs.has_key('callback'): self.callback = kwargs['callback'] del kwargs['callback'] ldap.ldapobject.LDAPObject.__init__(self, *args, **kwargs) self.__db = anydbm.open(filename, 'c', 0o640) self.__presentUUIDs = {} def syncrepl_set_cookie(self,cookie): self.__db['cookie'] = cookie def syncrepl_get_cookie(self): if 'cookie' in self.__db: return self.__db['cookie'] def syncrepl_delete(self, uuids): log.debug("syncrepl_delete uuids: %r" % (uuids), level=8) # Get the unique_attribute name to issue along with our # callback (if any) unique_attr = conf.get('ldap', 'unique_attribute') if unique_attr == None: unique_attr = 'entryuuid' if unique_attr == 'nsuniqueid': log.warning( _("The name of the persistent, unique attribute " + \ "is very probably not compatible with the use of " + \ "syncrepl.") ) for uuid in uuids: dn = self.__db[uuid] log.debug("syncrepl_delete dn: %r" % (dn), level=8) if not self.callback == None: self.callback( change_type='delete', previous_dn=None, change_number=None, dn=dn, entry={ unique_attr: uuid } ) del self.__db[uuid] def syncrepl_present(self, uuids, refreshDeletes=False): if uuids is None: if refreshDeletes is False: nonpresent = [] - for uuid in self.__db.keys(): + for uuid in self.__db: if uuid == 'cookie': continue if uuid in self.__presentUUIDs: continue nonpresent.append(uuid) self.syncrepl_delete(nonpresent) self.__presentUUIDs = {} else: for uuid in uuids: self.__presentUUIDs[uuid] = True def syncrepl_entry(self, dn, attrs, uuid): attrs = utils.normalize(attrs) if uuid in self.__db: odn = self.__db[uuid] if odn != dn: if not self.callback == None: self.callback( change_type='moddn', previous_dn=odn, change_number=None, dn=dn, entry=attrs ) else: if not self.callback == None: self.callback( change_type='modify', previous_dn=None, change_number=None, dn=self.__db[uuid], entry=attrs ) else: if not self.callback == None: self.callback( change_type='add', previous_dn=None, change_number=None, dn=dn, entry=attrs ) self.__db[uuid] = dn diff --git a/pykolab/cli/cmd_acl_cleanup.py b/pykolab/cli/cmd_acl_cleanup.py index d2f8210..2d6bd6e 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 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.keys(): + 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.keys(): + #for _aci_subject in acls: # connect to auth(!) # find recipient result_attr=aci_subject - # if no entry, expire acl \ No newline at end of file + # if no entry, expire acl diff --git a/pykolab/cli/cmd_add_alias.py b/pykolab/cli/cmd_add_alias.py index e905f79..5daffe5 100644 --- a/pykolab/cli/cmd_add_alias.py +++ b/pykolab/cli/cmd_add_alias.py @@ -1,131 +1,131 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys import commands import pykolab from pykolab.auth import Auth from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('add_alias', execute, description="Add alias.") def execute(*args, **kw): try: primary_rcpt_address = conf.cli_args.pop(0) try: secondary_rcpt_address = conf.cli_args.pop(0) except: print(_("Specify the (new) alias address"), file=sys.stderr) sys.exit(1) except: print(_("Specify the existing recipient address"), file=sys.stderr) sys.exit(1) if len(primary_rcpt_address.split('@')) > 1: primary_rcpt_domain = primary_rcpt_address.split('@')[-1] else: primary_rcpt_domain = conf.get('kolab', 'primary_domain') auth = Auth(domain=primary_rcpt_domain) domains = auth.list_domains() #print domains if len(secondary_rcpt_address.split('@')) > 1: secondary_rcpt_domain = secondary_rcpt_address.split('@')[-1] else: secondary_rcpt_domain = conf.get('kolab', 'primary_domain') # Check if either is in fact a domain - if not primary_rcpt_domain.lower() in domains.keys(): + 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.keys(): + if not secondary_rcpt_domain.lower() in domains: print(_("Domain %r is not a local domain") % (secondary_rcpt_domain), file=sys.stderr) sys.exit(1) if not primary_rcpt_domain == secondary_rcpt_domain: if not domains[primary_rcpt_domain] == domains[secondary_rcpt_domain]: print(_("Primary and secondary domain do not have the same parent domain"), file=sys.stderr) sys.exit(1) primary_recipient_dn = auth.find_recipient(primary_rcpt_address) if primary_recipient_dn == [] or len(primary_recipient_dn) == 0: print(_("No such recipient %r") % (primary_rcpt_address), file=sys.stderr) sys.exit(1) secondary_recipient_dn = auth.find_recipient(secondary_rcpt_address) if not secondary_recipient_dn == [] and not len(secondary_recipient_dn) == 0: print(_("Recipient for alias %r already exists") % (secondary_rcpt_address), file=sys.stderr) sys.exit(1) rcpt_attrs = conf.get_list('ldap', 'mail_attributes') primary_rcpt_attr = rcpt_attrs[0] if len(rcpt_attrs) >= 2: secondary_rcpt_attr = rcpt_attrs[1] else: print(_("Environment is not configured for " + \ "users to hold secondary mail attributes"), file=sys.stderr) sys.exit(1) primary_recipient = auth.get_entry_attributes(primary_rcpt_domain, primary_recipient_dn, rcpt_attrs) if not primary_recipient.has_key(primary_rcpt_attr): print(_("Recipient %r is not the primary recipient for address %r") % (primary_recipient, primary_rcpt_address), file=sys.stderr) sys.exit(1) if not primary_recipient.has_key(secondary_rcpt_attr): auth.set_entry_attributes(primary_rcpt_domain, primary_recipient_dn, {secondary_rcpt_attr: [ secondary_rcpt_address ] }) else: if isinstance(primary_recipient[secondary_rcpt_attr], basestring): new_secondary_rcpt_attrs = [ primary_recipient[secondary_rcpt_attr], secondary_rcpt_address ] else: new_secondary_rcpt_attrs = \ primary_recipient[secondary_rcpt_attr] + \ [ secondary_rcpt_address ] auth.set_entry_attributes( primary_rcpt_domain, primary_recipient_dn, { secondary_rcpt_attr: new_secondary_rcpt_attrs } ) diff --git a/pykolab/cli/cmd_check_quota.py b/pykolab/cli/cmd_check_quota.py index 71af296..cae5077 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 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.keys(): + 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 2a3ea95..47bbcb1 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 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.keys(): + 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_list_domains.py b/pykolab/cli/cmd_list_domains.py index 62ee29f..e76edca 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 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'].keys(): + 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 8f14ee6..dc86e47 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 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.keys(): + 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 7121723..af96cda 100644 --- a/pykolab/cli/cmd_list_mailbox_metadata.py +++ b/pykolab/cli/cmd_list_mailbox_metadata.py @@ -1,97 +1,97 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys import commands import pykolab from pykolab.imap import IMAP from pykolab.translate import _ from pykolab import utils log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('list_mailbox_metadata', execute, aliases='lmm', description=description()) def cli_options(): my_option_group = conf.add_cli_parser_option_group(_("CLI Options")) my_option_group.add_option( '--user', dest = "user", action = "store", default = None, metavar = "USER", help = _("List annotations as user USER") ) def description(): return """Obtain a list of metadata entries on a folder.""" def execute(*args, **kw): try: folder = conf.cli_args.pop(0) except IndexError: folder = utils.ask_question(_("Folder name")) if len(folder.split('@')) > 1: domain = folder.split('@')[1] elif not conf.user == None and len(conf.user.split('@')) > 1: domain = conf.user.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') imap = IMAP() if not conf.user == None: imap.connect(domain=domain, login=False) backend = conf.get(domain, 'imap_backend') if backend == None: backend = conf.get('kolab', 'imap_backend') admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') imap.login_plain(admin_login, admin_password, conf.user) else: imap.connect(domain=domain) if not imap.has_folder(folder): print(_("No such folder %r") % (folder), file=sys.stderr) else: metadata = [] folders = imap.list_folders(folder) for folder in folders: print("Folder", folder) metadata = imap.get_metadata(folder) if metadata.has_key(folder): - for annotation in metadata[folder].keys(): + for annotation in metadata[folder]: print(" %-49s %s" % ( annotation, metadata[folder][annotation] )) diff --git a/pykolab/cli/cmd_mailbox_cleanup.py b/pykolab/cli/cmd_mailbox_cleanup.py index 30a03a8..656a978 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 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.keys(): + 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 c3e5669..10d9a19 100644 --- a/pykolab/cli/cmd_remove_mailaddress.py +++ b/pykolab/cli/cmd_remove_mailaddress.py @@ -1,95 +1,95 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import sys import commands import pykolab from pykolab.auth import Auth from pykolab 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, basestring): 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.keys(): + 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_sync_mailhost_attrs.py b/pykolab/cli/cmd_sync_mailhost_attrs.py index 7b70a77..4d9663f 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 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.keys() if domains[x] == primary] + 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/commands.py b/pykolab/cli/commands.py index 196fa93..ba15202 100644 --- a/pykolab/cli/commands.py +++ b/pykolab/cli/commands.py @@ -1,201 +1,201 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import sys import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() commands = {} command_groups = {} def __init__(): # We only want the base path commands_base_path = os.path.dirname(__file__) for commands_path, dirnames, filenames in os.walk(commands_base_path): if not commands_path == commands_base_path: continue for filename in filenames: if filename.startswith('cmd_') and filename.endswith('.py'): module_name = filename.replace('.py','') cmd_name = module_name.replace('cmd_', '') #print "exec(\"from %s import __init__ as %s_register\"" % (module_name,cmd_name) try: exec("from %s import __init__ as %s_register" % (module_name,cmd_name)) except ImportError: pass exec("%s_register()" % (cmd_name)) for dirname in dirnames: register_group(commands_path, dirname) register('help', list_commands) register('delete_user', not_yet_implemented, description="Not yet implemented") register('list_groups', not_yet_implemented, description="Not yet implemented") register('add_group', not_yet_implemented, description="Not yet implemented") register('delete_group', not_yet_implemented, description="Not yet implemented") def list_commands(*args, **kw): """ List commands """ __commands = {} - for command in commands.keys(): + for command in commands: if isinstance(command, tuple): command_group, command = command __commands[command_group] = { command: commands[(command_group,command)] } else: __commands[command] = commands[command] _commands = __commands.keys() _commands.sort() for _command in _commands: if __commands[_command].has_key('group'): continue if __commands[_command].has_key('function'): # This is a top-level command if not __commands[_command]['description'] == None: print("%-25s - %s" % (_command.replace('_','-'),__commands[_command]['description'])) else: print("%-25s" % (_command.replace('_','-'))) for _command in _commands: if not __commands[_command].has_key('function'): # This is a nested command print("\n" + _("Command Group: %s") % (_command) + "\n") ___commands = __commands[_command].keys() ___commands.sort() for __command in ___commands: if not __commands[_command][__command]['description'] == None: print("%-4s%-21s - %s" % ('',__command.replace('_','-'),__commands[_command][__command]['description'])) else: print("%-4s%-21s" % ('',__command.replace('_','-'))) def execute(cmd_name, *args, **kw): if cmd_name == "": execute("help") sys.exit(0) if not commands.has_key(cmd_name): log.error(_("No such command.")) sys.exit(1) if not commands[cmd_name].has_key('function') and \ not commands[cmd_name].has_key('group'): log.error(_("No such command.")) sys.exit(1) if commands[cmd_name].has_key('group'): group = commands[cmd_name]['group'] command_name = commands[cmd_name]['cmd_name'] try: exec("from %s.cmd_%s import cli_options as %s_%s_cli_options" % (group,command_name,group,command_name)) exec("%s_%s_cli_options()" % (group,command_name)) except ImportError: pass else: command_name = commands[cmd_name]['cmd_name'] try: exec("from cmd_%s import cli_options as %s_cli_options" % (command_name,command_name)) exec("%s_cli_options()" % (command_name)) except ImportError: pass conf.finalize_conf() commands[cmd_name]['function'](conf.cli_args, kw) def register_group(dirname, module): commands_base_path = os.path.join(os.path.dirname(__file__), module) commands[module] = {} for commands_path, dirnames, filenames in os.walk(commands_base_path): if not commands_path == commands_base_path: continue for filename in filenames: if filename.startswith('cmd_') and filename.endswith('.py'): module_name = filename.replace('.py','') cmd_name = module_name.replace('cmd_', '') #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" % (module,module_name,module,cmd_name) exec("from %s.%s import __init__ as %s_%s_register" % (module,module_name,module,cmd_name)) exec("%s_%s_register()" % (module,cmd_name)) def register(cmd_name, func, group=None, description=None, aliases=[]): if not group == None: command = "%s_%s" % (group,cmd_name) else: command = cmd_name if isinstance(aliases, basestring): aliases = [aliases] if commands.has_key(command): log.fatal(_("Command '%s' already registered") % (command)) sys.exit(1) if callable(func): if group == None: commands[cmd_name] = { 'cmd_name': cmd_name, 'function': func, 'description': description } else: commands[group][cmd_name] = { 'cmd_name': cmd_name, 'function': func, 'description': description } commands[command] = commands[group][cmd_name] commands[command]['group'] = group commands[command]['cmd_name'] = cmd_name for alias in aliases: commands[alias] = { 'cmd_name': cmd_name, 'function': func, 'description': _("Alias for %s") % (cmd_name.replace('_','-')) } ## ## Commands not yet implemented ## def not_yet_implemented(*args, **kw): print(_("Not yet implemented")) sys.exit(1) diff --git a/pykolab/cli/wap/cmd_system_capabilities.py b/pykolab/cli/wap/cmd_system_capabilities.py index 6e8a06e..7b27e40 100644 --- a/pykolab/cli/wap/cmd_system_capabilities.py +++ b/pykolab/cli/wap/cmd_system_capabilities.py @@ -1,50 +1,50 @@ # -*- 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 pykolab from pykolab.cli import commands from pykolab.translate import _ log = pykolab.getLogger('pykolab.cli') conf = pykolab.getConf() def __init__(): commands.register('system_capabilities', execute, group='wap', description="Display the system capabilities.") def execute(*args, **kw): from pykolab import wap_client # Create the authentication object. # TODO: Binds with superuser credentials! wap_client.authenticate() system_capabilities = wap_client.system_capabilities() if system_capabilities['count'] < 1: print("No system capabilities") sys.exit(1) - for domain in system_capabilities['list'].keys(): + for domain in system_capabilities['list']: print("Domain capabilities for %s" % (domain)) domain_capabilities = system_capabilities['list'][domain] - for service in domain_capabilities['actions'].keys(): + for service in domain_capabilities['actions']: print(" %-15s - %r" % (service, domain_capabilities['actions'][service]['type'])) diff --git a/pykolab/conf/__init__.py b/pykolab/conf/__init__.py index 8487521..6de3c8e 100644 --- a/pykolab/conf/__init__.py +++ b/pykolab/conf/__init__.py @@ -1,796 +1,796 @@ # -*- 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 logging import os import sys from optparse import OptionParser from ConfigParser import SafeConfigParser import pykolab from pykolab.conf.defaults import Defaults from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.conf') class Conf(object): def __init__(self): """ self.cli_args == Arguments passed on the CLI self.cli_keywords == Parser results (again, CLI) self.cli_parser == The actual Parser (from OptionParser) self.plugins == Our Kolab Plugins """ self.cli_parser = None self.cli_args = None self.cli_keywords = None self.entitlement = None self.changelog = {} try: from pykolab.conf.entitlement import Entitlement entitlements = True except Exception: entitlements = False pass if entitlements: self.entitlement = Entitlement().get() self.plugins = None # The location where our configuration parser is going to end up self.cfg_parser = None # Create the options self.create_options() def finalize_conf(self, fatal=True): self.create_options_from_plugins() self.parse_options(fatal=fatal) # The defaults can some from; # - a file we ship with the packages # - a customly supplied file (by customer) # - a file we write out # - this python class # # Look, we want defaults self.defaults = Defaults(self.plugins) # But, they should be available in our class as well - for option in self.defaults.__dict__.keys(): + for option in self.defaults.__dict__: log.debug( _("Setting %s to %r (from defaults)") % ( option, self.defaults.__dict__[option] ), level=8 ) setattr(self, option, self.defaults.__dict__[option]) # This is where we check our parser for the defaults being set there. self.set_defaults_from_cli_options() self.options_set_from_config() # Also set the cli options if hasattr(self, 'cli_keywords') and self.cli_keywords is not None: - for option in self.cli_keywords.__dict__.keys(): + for option in self.cli_keywords.__dict__: retval = False if hasattr(self, "check_setting_%s" % (option)): exec( "retval = self.check_setting_%s(%r)" % ( option, self.cli_keywords.__dict__[option] ) ) # The warning, error or confirmation dialog is in the check_setting_%s() # function if not retval: continue log.debug( _("Setting %s to %r (from CLI, verified)") % ( option, self.cli_keywords.__dict__[option] ), level=8 ) setattr(self, option, self.cli_keywords.__dict__[option]) else: log.debug( _("Setting %s to %r (from CLI, not checked)") % ( option, self.cli_keywords.__dict__[option] ), level=8 ) setattr(self, option, self.cli_keywords.__dict__[option]) def load_config(self, config): """ Given a SafeConfigParser instance, loads a configuration file and checks, then sets everything it can find. """ - for section in self.defaults.__dict__.keys(): + for section in self.defaults.__dict__: if section == 'testing': continue if not config.has_section(section): continue - for key in self.defaults.__dict__[section].keys(): + for key in self.defaults.__dict__[section]: retval = False if not config.has_option(section, key): continue if isinstance(self.defaults.__dict__[section][key], int): value = config.getint(section, key) elif isinstance(self.defaults.__dict__[section][key], bool): value = config.getboolean(section, key) elif isinstance(self.defaults.__dict__[section][key], str): value = config.get(section, key) elif isinstance(self.defaults.__dict__[section][key], list): value = eval(config.get(section, key)) elif isinstance(self.defaults.__dict__[section][key], dict): value = eval(config.get(section, key)) if hasattr(self, "check_setting_%s_%s" % (section, key)): exec("retval = self.check_setting_%s_%s(%r)" % (section, key, value)) if not retval: # We just don't set it, check_setting_%s should have # taken care of the error messages continue if not self.defaults.__dict__[section][key] == value: if key.count('password') >= 1: log.debug( _("Setting %s_%s to '****' (from configuration file)") % ( section, key ), level=8 ) else: log.debug( _("Setting %s_%s to %r (from configuration file)") % ( section, key, value ), level=8 ) setattr(self, "%s_%s" % (section, key), value) def options_set_from_config(self): """ Sets the default configuration options from a configuration file. Configuration file may be customized using the --config CLI option """ log.debug(_("Setting options from configuration file"), level=4) # Check from which configuration file we should get the defaults # Other then default? self.config_file = self.defaults.config_file if hasattr(self, 'cli_keywords') and self.cli_keywords is not None: if not self.cli_keywords.config_file == self.defaults.config_file: self.config_file = self.cli_keywords.config_file config = self.check_config() self.load_config(config) def set_options_from_testing_section(self): """ Go through the options in the [testing] section if it exists. """ config = self.check_config() if not config.has_section('testing'): return for key in config.options('testing'): retval = False if isinstance(self.defaults.__dict__['testing'][key], int): value = config.getint('testing', key) elif isinstance(self.defaults.__dict__['testing'][key], bool): value = config.getboolean('testing', key) elif isinstance(self.defaults.__dict__['testing'][key], str): value = config.get('testing', key) elif isinstance(self.defaults.__dict__['testing'][key], list): value = eval(config.get('testing', key)) elif isinstance(self.defaults.__dict__['testing'][key], dict): value = eval(config.get('testing', key)) if hasattr(self, "check_setting_%s_%s" % ('testing', key)): exec("retval = self.check_setting_%s_%s(%r)" % ('testing', key, value)) if not retval: # We just don't set it, check_setting_%s should have # taken care of the error messages continue setattr(self, "%s_%s" % ('testing', key), value) if key.count('password') >= 1: log.debug( _("Setting %s_%s to '****' (from configuration file)") % ('testing', key), level=8 ) else: log.debug( _("Setting %s_%s to %r (from configuration file)") % ('testing', key, value), level=8 ) def check_config(self, val=None): """ Checks self.config_file or the filename passed using 'val' and returns a SafeConfigParser instance if everything is OK. """ if val is not None: config_file = val else: config_file = self.config_file if not os.access(config_file, os.R_OK): log.error(_("Configuration file %s not readable") % config_file) config = SafeConfigParser() log.debug(_("Reading configuration file %s") % config_file, level=8) try: config.read(config_file) except Exception: log.error(_("Invalid configuration file %s") % config_file) if not config.has_section("kolab"): log.warning( _("No master configuration section [kolab] in configuration file %s") % config_file ) return config def add_cli_parser_option_group(self, name): return self.cli_parser.add_option_group(name) def create_options_from_plugins(self): """ Create options from plugins. This function must be called separately from Conf.__init__(), or the configuration store is not yet done initializing when the plugins class and the plugins themselves go look for it. """ import pykolab.plugins self.plugins = pykolab.plugins.KolabPlugins() self.plugins.add_options(self.cli_parser) def create_options(self, load_plugins=True): """ Create the OptionParser for the options passed to us from runtime Command Line Interface. """ # Enterprise Linux 5 does not have an "epilog" parameter to OptionParser try: self.cli_parser = OptionParser(epilog=epilog) except Exception: self.cli_parser = OptionParser() # # Runtime Options # runtime_group = self.cli_parser.add_option_group(_("Runtime Options")) runtime_group.add_option( "-c", "--config", dest="config_file", action="store", default="/etc/kolab/kolab.conf", help=_("Configuration file to use") ) runtime_group.add_option( "-d", "--debug", dest="debuglevel", type='int', default=0, help=_( "Set the debugging verbosity. Maximum is 9, tracing protocols LDAP, SQL and IMAP." ) ) runtime_group.add_option( "-e", "--default", dest="answer_default", action="store_true", default=False, help=_("Use the default answer to all questions.") ) runtime_group.add_option( "-l", dest="loglevel", type='str', default="CRITICAL", help=_("Set the logging level. One of info, warn, error, critical or debug") ) runtime_group.add_option( "--logfile", dest="logfile", action="store", default="/var/log/kolab/pykolab.log", help=_("Log file to use") ) runtime_group.add_option( "-q", "--quiet", dest="quiet", action="store_true", default=False, help=_("Be quiet.") ) runtime_group.add_option( "-y", "--yes", dest="answer_yes", action="store_true", default=False, help=_("Answer yes to all questions.") ) def parse_options(self, fatal=True): """ Parse options passed to our call. """ if fatal: (self.cli_keywords, self.cli_args) = self.cli_parser.parse_args() def run(self): """ Run Forest, RUN! """ if self.cli_args: if len(self.cli_args) >= 1: if hasattr(self, "command_%s" % self.cli_args[0].replace('-', '_')): exec( "self.command_%s(%r)" % ( self.cli_args[0].replace('-', '_'), self.cli_args[1:] ) ) else: print(_("No command supplied"), file=sys.stderr) def command_dump(self, *args, **kw): """ Dumps applicable, valid configuration that is not defaults. """ if not self.cfg_parser: self.read_config() if not self.cfg_parser.has_section('kolab'): print("No section found for kolab", file=sys.stderr) sys.exit(1) # Get the sections, and then walk through the sections in a # sensible way. items = self.cfg_parser.options('kolab') items.sort() for item in items: mode = self.cfg_parser.get('kolab', item) print("%s = %s" % (item, mode)) if not self.cfg_parser.has_section(mode): print("WARNING: No configuration section %s for item %s" % (mode, item)) continue keys = self.cfg_parser.options(mode) keys.sort() if self.cfg_parser.has_option(mode, 'leave_this_one_to_me'): print("Ignoring section %s" % (mode)) continue for key in keys: print("%s_%s = %s" % (mode, key, self.cfg_parser.get(mode, key))) def read_config(self, value=None): """ Reads the configuration file, sets a self.cfg_parser. """ if not value: value = self.defaults.config_file if hasattr(self, 'cli_keywords') and self.cli_keywords is not None: value = self.cli_keywords.config_file self.cfg_parser = SafeConfigParser() self.cfg_parser.read(value) if hasattr(self, 'cli_keywords') and hasattr(self.cli_keywords, 'config_file'): self.cli_keywords.config_file = value self.defaults.config_file = value self.config_file = value def command_get(self, *args, **kw): """ Get a configuration option. Pass me a section and key please. """ exec("args = %r" % args) print("%s/%s: %r" % (args[0], args[1], self.get(args[0], args[1]))) # if len(args) == 3: # # Return non-zero if no match # # Return zero if match # # Improvised "check" function def command_set(self, *args, **kw): """ Set a configuration option. Pass me a section, key and value please. Note that the section should already exist. TODO: Add a strict parameter TODO: Add key value checking """ if not self.cfg_parser: self.read_config() if not len(args) == 3: log.error(_("Insufficient options. Need section, key and value -in that order.")) if not self.cfg_parser.has_section(args[0]): log.error(_("No section '%s' exists.") % (args[0])) if '%' in args[2]: value = args[2].replace('%', '%%') else: value = args[2] self.cfg_parser.set(args[0], args[1], value) if hasattr(self, 'cli_keywords') and hasattr(self.cli_keywords, 'config_file'): fp = open(self.cli_keywords.config_file, "w+") self.cfg_parser.write(fp) fp.close() else: fp = open(self.config_file, "w+") self.cfg_parser.write(fp) fp.close() def create_logger(self): """ Create a logger instance using cli_options.debuglevel """ global log if self.cli_keywords.debuglevel is not None: loglevel = logging.DEBUG else: loglevel = logging.INFO self.cli_keywords.debuglevel = 0 self.debuglevel = self.cli_keywords.debuglevel # Initialize logger log = pykolab.logger.Logger( loglevel=loglevel, debuglevel=self.cli_keywords.debuglevel, logfile=self.cli_keywords.logfile ) def set_defaults_from_cli_options(self): - for long_opt in self.cli_parser.__dict__['_long_opt'].keys(): + for long_opt in self.cli_parser.__dict__['_long_opt']: if long_opt == "--help": continue setattr( self.defaults, self.cli_parser._long_opt[long_opt].dest, self.cli_parser._long_opt[long_opt].default ) # But, they should be available in our class as well - for option in self.cli_parser.defaults.keys(): + for option in self.cli_parser.defaults: log.debug( _("Setting %s to %r (from the default values for CLI options)") % ( option, self.cli_parser.defaults[option] ), level=8 ) setattr(self, option, self.cli_parser.defaults[option]) def has_section(self, section): if not self.cfg_parser: self.read_config() return self.cfg_parser.has_section(section) def has_option(self, section, option): if not self.cfg_parser: self.read_config() return self.cfg_parser.has_option(section, option) def get_list(self, section, key, default=None): """ Gets a comma and/or space separated list from the configuration file and returns a list. """ values = [] untrimmed_values = [] setting = self.get_raw(section, key) if setting is None: return default if default else [] raw_values = setting.split(',') if raw_values is None: return default if default else [] for raw_value in raw_values: untrimmed_values.extend(raw_value.split(' ')) for value in untrimmed_values: if not value.strip() == "": values.append(value.strip().lower()) return values def get_raw(self, section, key, default=None): if not self.cfg_parser: self.read_config() if self.cfg_parser.has_option(section, key): return self.cfg_parser.get(section, key, 1) return default def get(self, section, key, default=None, quiet=False): """ Get a configuration option from our store, the configuration file, or an external source if we have some sort of function for it. TODO: Include getting the value from plugins through a hook. """ retval = False if not self.cfg_parser: self.read_config() # log.debug(_("Obtaining value for section %r, key %r") % (section, key), level=8) if self.cfg_parser.has_option(section, key): try: return self.cfg_parser.get(section, key) except Exception: self.read_config() return self.cfg_parser.get(section, key) if hasattr(self, "get_%s_%s" % (section, key)): try: exec("retval = self.get_%s_%s(quiet)" % (section, key)) except Exception: log.error( _("Could not execute configuration function: %s") % ( "get_%s_%s(quiet=%r)" % ( section, key, quiet ) ) ) return default return retval if quiet: return "" else: log.debug( _("Option %s/%s does not exist in config file %s, pulling from defaults") % ( section, key, self.config_file ), level=8 ) if hasattr(self.defaults, "%s_%s" % (section, key)): return getattr(self.defaults, "%s_%s" % (section, key)) elif hasattr(self.defaults, "%s" % (section)): if key in getattr(self.defaults, "%s" % (section)): _dict = getattr(self.defaults, "%s" % (section)) return _dict[key] else: log.warning(_("Option does not exist in defaults.")) return default else: log.warning(_("Option does not exist in defaults.")) return default def check_setting_config_file(self, value): if os.path.isfile(value): if os.access(value, os.R_OK): self.read_config(value=value) self.config_file = value return True else: log.error(_("Configuration file %s not readable.") % (value)) return False else: log.error(_("Configuration file %s does not exist.") % (value)) return False def check_setting_debuglevel(self, value): if value < 0: log.info( _( "WARNING: A negative debug level value does not " + "make this program be any more silent." ) ) elif value == 0: return True elif value <= 9: return True else: log.warning(_("This program has 9 levels of verbosity. Using the maximum of 9.")) return True def check_setting_saslauth_mode(self, value): if value: # TODO: I suppose this is platform specific if os.path.isfile("/var/run/saslauthd/mux"): if os.path.isfile("/var/run/saslauthd/saslauthd.pid"): log.error(_("Cannot start SASL authentication daemon")) return False else: try: os.remove("/var/run/saslauthd/mux") except IOError: log.error(_("Cannot start SASL authentication daemon")) return False elif os.path.isfile("/var/run/sasl2/mux"): if os.path.isfile("/var/run/sasl2/saslauthd.pid"): log.error(_("Cannot start SASL authentication daemon")) return False else: try: os.remove("/var/run/sasl2/mux") except IOError: log.error(_("Cannot start SASL authentication daemon")) return False return True def check_setting_use_imap(self, value): if value: try: import imaplib self.use_imap = value return True except ImportError: log.error(_("No imaplib library found.")) return False def check_setting_use_lmtp(self, value): if value: try: from smtplib import LMTP self.use_lmtp = value return True except ImportError: log.error(_("No LMTP class found in the smtplib library.")) return False def check_setting_use_mail(self, value): if value: try: from smtplib import SMTP self.use_mail = value return True except ImportError: log.error(_("No SMTP class found in the smtplib library.")) return False def check_setting_test_suites(self, value): # Attempt to load the suite, # Get the suite's options, # Set them here. if not hasattr(self, 'test_suites'): self.test_suites = [] if "zpush" in value: selectively = False for item in ['calendar', 'contacts', 'mail']: if self.cli_keywords.__dict__[item]: log.debug( _("Found you specified a specific set of items to test: %s") % (item), level=8 ) selectively = item if not selectively: self.calendar = True self.contacts = True self.mail = True else: log.debug(_("Selectively selecting: %s") % (selectively), level=8) setattr(self, selectively, True) self.test_suites.append('zpush') def check_setting_calendar(self, value): if self.cli_parser._long_opt['--calendar'].default == value: return False else: return True def check_setting_contacts(self, value): if self.cli_parser._long_opt['--contacts'].default == value: return False else: return True def check_setting_mail(self, value): if self.cli_parser._long_opt['--mail'].default == value: return False else: return True diff --git a/pykolab/conf/entitlement.py b/pykolab/conf/entitlement.py index 4ed79ec..3dd92d6 100644 --- a/pykolab/conf/entitlement.py +++ b/pykolab/conf/entitlement.py @@ -1,266 +1,266 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from ConfigParser import ConfigParser import hashlib import OpenSSL import os import StringIO import subprocess import sys from pykolab.translate import _ import pykolab log = pykolab.getLogger('pykolab.conf') class Entitlement(object): def __init__(self, *args, **kw): self.entitlement = {} self.entitlement_files = [] ca_cert_file = '/etc/pki/tls/certs/mirror.kolabsys.com.ca.cert' customer_cert_file = '/etc/pki/tls/private/mirror.kolabsys.com.client.pem' customer_key_file = '/etc/pki/tls/private/mirror.kolabsys.com.client.pem' # Licence lock and key verification. self.entitlement_verification = [ 'f700660f456a60c92ab2f00d0f1968230920d89829d42aa27d30f678', '95783ba5521ea54aa3a32b7949f145aa5015a4c9e92d12b9e4c95c14' ] if os.access(ca_cert_file, os.R_OK): # Verify /etc/kolab/mirror_ca.crt ca_cert = OpenSSL.crypto.load_certificate( OpenSSL.SSL.FILETYPE_PEM, open(ca_cert_file).read() ) if (bool)(ca_cert.has_expired()): raise Exception(_("Invalid entitlement verification " + \ "certificate at %s" % (ca_cert_file))) # TODO: Check validity and warn ~1-2 months in advance. ca_cert_issuer = ca_cert.get_issuer() ca_cert_subject = ca_cert.get_subject() ca_cert_issuer_hash = subprocess.Popen( [ 'openssl', 'x509', '-in', ca_cert_file, '-noout', '-issuer_hash' ], stdout=subprocess.PIPE ).communicate()[0].strip() ca_cert_issuer_hash_digest = hashlib.sha224(ca_cert_issuer_hash).hexdigest() if not ca_cert_issuer_hash_digest in self.entitlement_verification: raise Exception(_("Invalid entitlement verification " + \ "certificate at %s") % (ca_cert_file)) ca_cert_subject_hash = subprocess.Popen( [ 'openssl', 'x509', '-in', ca_cert_file, '-noout', '-subject_hash' ], stdout=subprocess.PIPE ).communicate()[0].strip() ca_cert_subject_hash_digest = hashlib.sha224(ca_cert_subject_hash).hexdigest() if not ca_cert_subject_hash_digest in self.entitlement_verification: raise Exception(_("Invalid entitlement verification " + \ "certificate at %s") % (ca_cert_file)) customer_cert_issuer_hash = subprocess.Popen( [ 'openssl', 'x509', '-in', customer_cert_file, '-noout', '-issuer_hash' ], stdout=subprocess.PIPE ).communicate()[0].strip() customer_cert_issuer_hash_digest = hashlib.sha224(customer_cert_issuer_hash).hexdigest() if not customer_cert_issuer_hash_digest in self.entitlement_verification: raise Exception(_("Invalid entitlement verification " + \ "certificate at %s") % (customer_cert_file)) if not ca_cert_issuer.countryName == ca_cert_subject.countryName: raise Exception(_("Invalid entitlement certificate")) if not ca_cert_issuer.organizationName == ca_cert_subject.organizationName: raise Exception(_("Invalid entitlement certificate")) if os.path.isdir('/etc/kolab/entitlement.d/') and \ os.access('/etc/kolab/entitlement.d/', os.R_OK): for root, dirs, files in os.walk('/etc/kolab/entitlement.d/'): if not root == '/etc/kolab/entitlement.d/': continue for entitlement_file in files: log.debug(_("Parsing entitlement file %s") % (entitlement_file), level=8) if os.access(os.path.join(root, entitlement_file), os.R_OK): self.entitlement_files.append( os.path.join(root, entitlement_file) ) else: log.error( _("License file %s not readable!") % ( os.path.join(root, entitlement_file) ) ) else: log.error(_("No entitlement directory found")) for entitlement_file in self.entitlement_files: decrypt_command = [ 'openssl', 'smime', '-decrypt', '-recip', customer_cert_file, '-in', entitlement_file ] decrypt_process = subprocess.Popen( decrypt_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) verify_command = [ 'openssl', 'smime', '-verify', '-certfile', ca_cert_file, '-CAfile', ca_cert_file, '-inform', 'DER' ] verify_process = subprocess.Popen( verify_command, stdin=decrypt_process.stdout, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) (stdout, stderr) = verify_process.communicate() license = License(stdout, self.entitlement) license.verify_certificate(customer_cert_file) self.entitlement = license.get() # else: # log.error(_("Error reading entitlement certificate authority file")) def get(self): - if len(self.entitlement.keys()) == 0: + if len(self.entitlement) == 0: return None else: return self.entitlement class License(object): entitlement = {} def __init__(self, new_entitlement, existing_entitlement): self.parser = ConfigParser() fp = StringIO.StringIO(new_entitlement) self.parser.readfp(fp) self.entitlement['users'] = self.parser.get('kolab_entitlements', 'users') self.entitlement['margin'] = self.parser.get('kolab_entitlements', 'margin') def verify_certificate(self, customer_cert_file): # Verify the certificate section as well. cert_serial = self.parser.get('mirror_ca', 'serial_number') cert_issuer_hash = self.parser.get('mirror_ca', 'issuer_hash') cert_subject_hash = self.parser.get('mirror_ca', 'subject_hash') customer_cert_serial = subprocess.Popen( [ 'openssl', 'x509', '-in', customer_cert_file, '-noout', '-serial' ], stdout=subprocess.PIPE ).communicate()[0].strip().split('=')[1] if not customer_cert_serial == cert_serial: raise Exception(_("Invalid entitlement verification " + \ "certificate at %s") % (customer_cert_file)) customer_cert_issuer_hash = subprocess.Popen( [ 'openssl', 'x509', '-in', customer_cert_file, '-noout', '-issuer_hash' ], stdout=subprocess.PIPE ).communicate()[0].strip() if not customer_cert_issuer_hash == cert_issuer_hash: raise Exception(_("Invalid entitlement verification " + \ "certificate at %s") % (customer_cert_file)) customer_cert_subject_hash = subprocess.Popen( [ 'openssl', 'x509', '-in', customer_cert_file, '-noout', '-subject_hash' ], stdout=subprocess.PIPE ).communicate()[0].strip() if not customer_cert_subject_hash == cert_subject_hash: raise Exception(_("Invalid entitlement verification " + \ "certificate at %s") % (customer_cert_file)) def get(self): return self.entitlement diff --git a/pykolab/imap/__init__.py b/pykolab/imap/__init__.py index 1ac6896..58ef755 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 . # import logging import re import time import socket import sys from urlparse 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.keys(): + 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 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 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'): self.imap.disconnect() del self.imap # Empty out self._imap as well - for key in self._imap.keys(): + for key in self._imap: self._imap[key].disconnect() del self._imap[key] else: if server in self._imap: self._imap[server].disconnect() 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.keys(): + 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.keys()) > 0: + 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.keys(): + 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"].keys(): + 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"].keys(): + 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.keys(): + 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, basestring): 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/plugins/recipientpolicy/__init__.py b/pykolab/plugins/recipientpolicy/__init__.py index aab2a5e..94c707f 100644 --- a/pykolab/plugins/recipientpolicy/__init__.py +++ b/pykolab/plugins/recipientpolicy/__init__.py @@ -1,163 +1,163 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab import utils from pykolab.translate import _ conf = pykolab.getConf() log = pykolab.getLogger('pykolab.plugins.recipientpolicy') class KolabRecipientpolicy(object): """ Example plugin making quota adjustments given arbitrary conditions. """ def __init__(self): pass def add_options(self, *args, **kw): pass #def mail_domain_space_policy_check(self, kw={}, args=()): #(mail, alternative_mail, domain_name, domain_root_dn) = args ## Your actions go here. For example: #return (mail, alternative_mail) def set_primary_mail(self, *args, **kw): """ The arguments passed to the 'set_user_attrs_mail' hook: primary_mail - the policy user_attrs - the current user attributes primary_domain - the domain to use in the primary mail attribute secondary_domains - the secondary domains that are aliases Return the new primary mail address """ user_attrs = utils.normalize(kw['entry']) if not user_attrs.has_key('domain'): user_attrs['domain'] = kw['primary_domain'] elif not user_attrs['domain'] == kw['primary_domain']: user_attrs['domain'] = kw['primary_domain'] if not user_attrs.has_key('preferredlanguage'): default_locale = conf.get(user_attrs['domain'], 'default_locale') if default_locale == None: default_locale = conf.get('kolab', 'default_locale') if default_locale == None: default_locale = 'en_US' user_attrs['preferredlanguage'] = default_locale try: mail = kw['primary_mail'] % user_attrs mail = utils.translate(mail, user_attrs['preferredlanguage']) mail = mail.lower() return mail except KeyError: log.warning(_("Attribute substitution for 'mail' failed in Recipient Policy")) if user_attrs.has_key('mail'): return user_attrs['mail'] else: return None def set_secondary_mail(self, *args, **kw): """ The arguments passed to the 'set_user_attrs_alternative_mail' hook: primary_mail - the policy user_attrs - the current user attributes primary_domain - the domain to use in the primary mail attribute secondary_domains - the secondary domains that are aliases Return a list of secondary mail addresses """ user_attrs = utils.normalize(kw['entry']) if not user_attrs.has_key('domain'): user_attrs['domain'] = kw['primary_domain'] elif not user_attrs['domain'] == kw['primary_domain']: user_attrs['domain'] = kw['primary_domain'] if not user_attrs.has_key('preferredlanguage'): default_locale = conf.get(user_attrs['domain'], 'default_locale') if default_locale == None: default_locale = conf.get(user_attrs['domain'], 'default_locale') if default_locale == None: default_locale = 'en_US' user_attrs['preferredlanguage'] = default_locale try: exec("alternative_mail_routines = %s" % kw['secondary_mail']) except Exception: log.error(_("Could not parse the alternative mail routines")) alternative_mail = [] log.debug(_("Alternative mail routines: %r") % (alternative_mail_routines), level=8) _domains = [ kw['primary_domain'] ] + kw['secondary_domains'] for attr in [ 'givenname', 'sn', 'surname' ]: try: user_attrs[attr] = utils.translate(user_attrs[attr], user_attrs['preferredlanguage']) except Exception: log.error(_("An error occurred in composing the secondary mail attribute for entry %r") % (user_attrs['id'])) if conf.debuglevel > 8: import traceback traceback.print_exc() return [] - for number in alternative_mail_routines.keys(): - for routine in alternative_mail_routines[number].keys(): + for number in alternative_mail_routines: + for routine in alternative_mail_routines[number]: try: exec("retval = '%s'.%s" % (routine,alternative_mail_routines[number][routine] % user_attrs)) log.debug(_("Appending additional mail address: %s") % (retval), level=8) alternative_mail.append(retval) except Exception as errmsg: log.error(_("Policy for secondary email address failed: %r") % (errmsg)) if conf.debuglevel > 8: import traceback traceback.print_exc() return [] for _domain in kw['secondary_domains']: user_attrs['domain'] = _domain try: exec("retval = '%s'.%s" % (routine,alternative_mail_routines[number][routine] % user_attrs)) log.debug(_("Appending additional mail address: %s") % (retval), level=8) alternative_mail.append(retval) except KeyError: log.warning(_("Attribute substitution for 'alternative_mail' failed in Recipient Policy")) alternative_mail = utils.normalize(alternative_mail) alternative_mail = list(set(alternative_mail)) return alternative_mail diff --git a/pykolab/plugins/sievemgmt/__init__.py b/pykolab/plugins/sievemgmt/__init__.py index 75e247b..6588bdb 100644 --- a/pykolab/plugins/sievemgmt/__init__.py +++ b/pykolab/plugins/sievemgmt/__init__.py @@ -1,431 +1,431 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab import utils from pykolab.auth import Auth from pykolab.translate import _ conf = pykolab.getConf() log = pykolab.getLogger('pykolab.plugin_sievemgmt') import sys import time from urlparse import urlparse class KolabSievemgmt(object): """ Plugin to manage Sieve scripts according to KEP #14. """ def __init__(self): pass def add_options(self, *args, **kw): pass def sieve_mgmt_refresh(self, *args, **kw): """ The arguments passed to the 'sieve_mgmt_refresh' hook: user - the user identifier """ - if not len(kw.keys()) == 1 or not kw.has_key('user'): + if not len(kw) == 1 or not kw.has_key('user'): log.error(_("Wrong number of arguments for sieve management plugin")) return else: address = kw['user'] auth = Auth() auth.connect() user = auth.find_recipient(address) log.info("User for address %r: %r" % (address, user)) # Get the main, default backend backend = conf.get('kolab', 'imap_backend') if len(address.split('@')) > 1: domain = address.split('@')[1] else: domain = conf.get('kolab', 'primary_domain') if conf.has_section(domain) and conf.has_option(domain, 'imap_backend'): backend = conf.get(domain, 'imap_backend') if conf.has_section(domain) and conf.has_option(domain, 'imap_uri'): uri = conf.get(domain, 'imap_uri') else: uri = conf.get(backend, 'uri') hostname = None port = None result = urlparse(uri) if hasattr(result, 'hostname'): hostname = result.hostname else: scheme = uri.split(':')[0] (hostname, port) = uri.split('/')[2].split(':') port = 4190 # Get the credentials admin_login = conf.get(backend, 'admin_login') admin_password = conf.get(backend, 'admin_password') import sievelib.managesieve sieveclient = sievelib.managesieve.Client(hostname, port, conf.debuglevel > 8) sieveclient.connect(None, None, True) sieveclient._plain_authentication(admin_login, admin_password, address) sieveclient.authenticated = True result = sieveclient.listscripts() if result == None: active = None scripts = [] else: active, scripts = result log.debug(_("Found the following scripts for user %s: %s") % (address, ','.join(scripts)), level=8) log.debug(_("And the following script is active for user %s: %s") % (address, active), level=8) mgmt_required_extensions = [] mgmt_script = """# # MANAGEMENT # """ user = auth.get_entry_attributes(domain, user, ['*']) # # Vacation settings (a.k.a. Out of Office) # vacation_active = None vacation_text = None vacation_uce = None vacation_noreact_domains = None vacation_react_domains = None vacation_active_attr = conf.get('sieve', 'vacation_active_attr') vacation_text_attr = conf.get('sieve', 'vacation_text_attr') vacation_uce_attr = conf.get('sieve', 'vacation_uce_attr') vacation_noreact_domains_attr = conf.get('sieve', 'vacation_noreact_domains_attr') vacation_react_domains_attr = conf.get('sieve', 'vacation_react_domains_attr') if not vacation_text_attr == None: if user.has_key(vacation_active_attr): vacation_active = utils.true_or_false(user[vacation_active_attr]) else: vacation_active = False if user.has_key(vacation_text_attr): vacation_text = user[vacation_text_attr] else: vacation_active = False if user.has_key(vacation_uce_attr): vacation_uce = utils.true_or_false(user[vacation_uce_attr]) else: vacation_uce = False if user.has_key(vacation_react_domains_attr): if isinstance(user[vacation_react_domains_attr], list): vacation_react_domains = user[vacation_react_domains_attr] else: vacation_react_domains = [ user[vacation_react_domains_attr] ] else: if user.has_key(vacation_noreact_domains_attr): if isinstance(user[vacation_noreact_domains_attr], list): vacation_noreact_domains = user[vacation_noreact_domains_attr] else: vacation_noreact_domains = [ user[vacation_noreact_domains_attr] ] else: vacation_noreact_domains = [] # # Delivery to Folder # dtf_active_attr = conf.get('sieve', 'deliver_to_folder_active') if not dtf_active_attr == None: if user.has_key(dtf_active_attr): dtf_active = utils.true_or_false(user[dtf_active_attr]) else: dtf_active = False else: # TODO: Not necessarily de-activated, the *Active attributes are # not supposed to charge this - check the deliver_to_folder_attr # attribute value for a value. dtf_active = False if dtf_active: dtf_folder_name_attr = conf.get('sieve', 'deliver_to_folder_attr') if not dtf_folder_name_attr == None: if user.has_key(dtf_folder_name_attr): dtf_folder = user[dtf_folder_name_attr] else: log.warning(_("Delivery to folder active, but no folder name attribute available for user %r") % (user)) dtf_active = False else: log.error(_("Delivery to folder active, but no folder name attribute configured")) dtf_active = False # # Folder name to delivery spam to. # # Global or local. # sdf_filter = True sdf = conf.get('sieve', 'spam_global_folder') if sdf == None: sdf = conf.get('sieve', 'spam_personal_folder') if sdf == None: sdf_filter = False # # Mail forwarding # forward_active = None forward_addresses = [] forward_keepcopy = None forward_uce = None forward_active_attr = conf.get('sieve', 'forward_address_active') if not forward_active_attr == None: if user.has_key(forward_active_attr): forward_active = utils.true_or_false(user[forward_active_attr]) else: forward_active = False if not forward_active == False: forward_address_attr = conf.get('sieve', 'forward_address_attr') if user.has_key(forward_address_attr): if isinstance(user[forward_address_attr], basestring): forward_addresses = [ user[forward_address_attr] ] elif isinstance(user[forward_address_attr], str): forward_addresses = [ user[forward_address_attr] ] else: forward_addresses = user[forward_address_attr] if len(forward_addresses) == 0: forward_active = False forward_keepcopy_attr = conf.get('sieve', 'forward_keepcopy_active') if not forward_keepcopy_attr == None: if user.has_key(forward_keepcopy_attr): forward_keepcopy = utils.true_or_false(user[forward_keepcopy_attr]) else: forward_keepcopy = False forward_uce_attr = conf.get('sieve', 'forward_uce_active') if not forward_uce_attr == None: if user.has_key(forward_uce_attr): forward_uce = utils.true_or_false(user[forward_uce_attr]) else: forward_uce = False if vacation_active: mgmt_required_extensions.append('vacation') mgmt_required_extensions.append('envelope') if dtf_active: mgmt_required_extensions.append('fileinto') if forward_active and (len(forward_addresses) > 1 or forward_keepcopy): mgmt_required_extensions.append('copy') if sdf_filter: mgmt_required_extensions.append('fileinto') import sievelib.factory mgmt_script = sievelib.factory.FiltersSet("MANAGEMENT") for required_extension in mgmt_required_extensions: mgmt_script.require(required_extension) mgmt_script.require('fileinto') if vacation_active: if not vacation_react_domains == None and len(vacation_react_domains) > 0: mgmt_script.addfilter( 'vacation', [('envelope', ':domain', ":is", "from", vacation_react_domains)], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) elif not vacation_noreact_domains == None and len(vacation_noreact_domains) > 0: mgmt_script.addfilter( 'vacation', [('not', ('envelope', ':domain', ":is", "from", vacation_noreact_domains))], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) else: mgmt_script.addfilter( 'vacation', [('true',)], [ ( "vacation", ":days", 1, ":subject", "Out of Office", # ":handle", see http://tools.ietf.org/html/rfc5230#page-4 # ":mime", to indicate the reason is in fact MIME vacation_text ) ] ) if forward_active: forward_rules = [] # Principle can be demonstrated by: # # python -c "print ','.join(['a','b','c'][:-1])" # for forward_copy in forward_addresses[:-1]: forward_rules.append(("redirect", ":copy", forward_copy)) if forward_keepcopy: # Principle can be demonstrated by: # # python -c "print ','.join(['a','b','c'][-1])" # if forward_uce: rule_name = 'forward-uce-keepcopy' else: rule_name = 'forward-keepcopy' forward_rules.append(("redirect", ":copy", forward_addresses[-1])) else: if forward_uce: rule_name = 'forward-uce' else: rule_name = 'forward' forward_rules.append(("redirect", forward_addresses[-1])) forward_rules.append(("stop")) if forward_uce: mgmt_script.addfilter(rule_name, ['true'], forward_rules) else: # NOTE: Messages with no X-Spam-Status header need to be matched # too, and this does exactly that. mgmt_script.addfilter(rule_name, [("not", ("X-Spam-Status", ":matches", "Yes,*"))], forward_rules) if sdf_filter: mgmt_script.addfilter('spam_delivery_folder', [("X-Spam-Status", ":matches", "Yes,*")], [("fileinto", "INBOX/Spam"), ("stop")]) if dtf_active: mgmt_script.addfilter('delivery_to_folder', ['true'], [("fileinto", dtf_folder)]) mgmt_script = mgmt_script.__str__() result = sieveclient.putscript("MANAGEMENT", mgmt_script) if not result: log.error(_("Uploading script MANAGEMENT failed for user %s") % (address)) else: log.debug(_("Uploading script MANAGEMENT for user %s succeeded") % (address), level=8) user_script = """# # User # require ["include"]; """ for script in scripts: if not script in [ "MASTER", "MANAGEMENT", "USER" ]: log.debug(_("Including script %s in USER (for user %s)") % (script,address) ,level=8) user_script = """%s include :personal "%s"; """ % (user_script, script) result = sieveclient.putscript("USER", user_script) if not result: log.error(_("Uploading script USER failed for user %s") % (address)) else: log.debug(_("Uploading script USER for user %s succeeded") % (address), level=8) result = sieveclient.putscript("MASTER", """# # MASTER # # This file is authoritative for your system and MUST BE KEPT ACTIVE. # # Altering it is likely to render your account dysfunctional and may # be violating your organizational or corporate policies. # # For more information on the mechanism and the conventions behind # this script, see http://wiki.kolab.org/KEP:14 # require ["include"]; # OPTIONAL: Includes for all or a group of users # include :global "all-users"; # include :global "this-group-of-users"; # The script maintained by the general management system include :personal "MANAGEMENT"; # The script(s) maintained by one or more editors available to the user include :personal "USER"; """) if not result: log.error(_("Uploading script MASTER failed for user %s") % (address)) else: log.debug(_("Uploading script MASTER for user %s succeeded") % (address), level=8) sieveclient.setactive("MASTER") diff --git a/pykolab/setup/components.py b/pykolab/setup/components.py index 5cc7b58..f68bb48 100644 --- a/pykolab/setup/components.py +++ b/pykolab/setup/components.py @@ -1,266 +1,266 @@ # -*- coding: utf-8 -*- # # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import os import pykolab from pykolab.constants import * from pykolab.translate import _ log = pykolab.getLogger('pykolab.setup') conf = pykolab.getConf() components = {} component_groups = {} executed_components = [] components_included_in_cli = [] finalize_conf_ok = None def __init__(): # We only want the base path components_base_path = os.path.dirname(__file__) for components_path, dirnames, filenames in os.walk(components_base_path): if not components_path == components_base_path: continue for filename in filenames: if filename.startswith('setup_') and filename.endswith('.py'): module_name = filename.replace('.py','') component_name = module_name.replace('setup_', '') #print "exec(\"from %s import __init__ as %s_register\"" % (module_name,component_name) exec("from %s import __init__ as %s_register" % (module_name,component_name)) exec("%s_register()" % (component_name)) for dirname in dirnames: register_group(components_path, dirname) register('help', list_components, description=_("Display this help.")) def list_components(*args, **kw): """ List components """ __components = {} - for component in components.keys(): + for component in components: if isinstance(component, tuple): component_group, component = component __components[component_group] = { component: components[(component_group,component)] } else: __components[component] = components[component] _components = __components.keys() _components.sort() for _component in _components: if __components[_component].has_key('function'): # This is a top-level component if not __components[_component]['description'] == None: print("%-25s - %s" % (_component.replace('_','-'),__components[_component]['description'])) else: print("%-25s" % (_component.replace('_','-'))) for _component in _components: if not __components[_component].has_key('function'): # This is a nested component print("\n" + _("Command Group: %s") % (_component) + "\n") ___components = __components[_component].keys() ___components.sort() for __component in ___components: if not __components[_component][__component]['description'] == None: print("%-4s%-21s - %s" % ('',__component.replace('_','-'),__components[_component][__component]['description'])) else: print("%-4s%-21s" % ('',__component.replace('_','-'))) def _list_components(*args, **kw): """ List components and return API compatible, parseable lists and dictionaries. """ __components = {} - for component in components.keys(): + for component in components: if isinstance(component, tuple): component_group, component = component __components[component_group] = { component: components[(component_group,component)] } else: __components[component] = components[component] _components = __components.keys() _components.sort() return _components def cli_options_from_component(component_name, *args, **kw): global components_included_in_cli if component_name in components_included_in_cli: return if components[component_name].has_key('group'): group = components[component_name]['group'] component_name = components[component_name]['component_name'] try: exec("from %s.setup_%s import cli_options as %s_%s_cli_options" % (group,component_name,group,component_name)) exec("%s_%s_cli_options()" % (group,component_name)) except ImportError: pass else: try: exec("from setup_%s import cli_options as %s_cli_options" % (component_name,component_name)) exec("%s_cli_options()" % (component_name)) except ImportError: pass components_included_in_cli.append(component_name) def execute(component_name, *args, **kw): if component_name == '': log.debug( _("No component selected, continuing for all components"), level=8 ) while 1: for component in _list_components(): execute_this = True if component in executed_components: execute_this = False if component == "help": execute_this = False if execute_this: if components[component].has_key('after'): for _component in components[component]['after']: if not _component in executed_components: execute_this = False if execute_this: execute(component) executed_components.append(component) executed_all = True for component in _list_components(): if not component in executed_components and not component == "help": executed_all = False if executed_all: break return else: for component in _list_components(): cli_options_from_component(component) if not components.has_key(component_name): log.error(_("No such component.")) sys.exit(1) if not components[component_name].has_key('function') and \ not components[component_name].has_key('group'): log.error(_("No such component.")) sys.exit(1) conf.finalize_conf() if len(conf.cli_args) >= 1: _component_name = conf.cli_args.pop(0) else: _component_name = component_name components[component_name]['function'](conf.cli_args, kw) def register_group(dirname, module): components_base_path = os.path.join(os.path.dirname(__file__), module) components[module] = {} for components_path, dirnames, filenames in os.walk(components_base_path): if not components_path == components_base_path: continue for filename in filenames: if filename.startswith('setup_') and filename.endswith('.py'): module_name = filename.replace('.py','') component_name = module_name.replace('setup_', '') #print "exec(\"from %s.%s import __init__ as %s_%s_register\"" % (module,module_name,module,component_name) exec("from %s.%s import __init__ as %s_%s_register" % (module,module_name,module,component_name)) exec("%s_%s_register()" % (module,component_name)) def register(component_name, func, group=None, description=None, aliases=[], after=[], before=[]): if not group == None: component = "%s_%s" % (group,component_name) else: component = component_name if isinstance(aliases, basestring): aliases = [aliases] if components.has_key(component): log.fatal(_("Command '%s' already registered") % (component)) sys.exit(1) if callable(func): if group == None: components[component_name] = { 'function': func, 'description': description, 'after': after, 'before': before, } else: components[group][component_name] = { 'function': func, 'description': description, 'after': after, 'before': before, } components[component] = components[group][component_name] components[component]['group'] = group components[component]['component_name'] = component_name for alias in aliases: components[alias] = { 'function': func, 'description': _("Alias for %s") % (component_name) } ## ## Commands not yet implemented ## def not_yet_implemented(*args, **kw): print(_("Not yet implemented")) sys.exit(1) diff --git a/pykolab/setup/setup_freebusy.py b/pykolab/setup/setup_freebusy.py index 947e1fb..3138226 100644 --- a/pykolab/setup/setup_freebusy.py +++ b/pykolab/setup/setup_freebusy.py @@ -1,191 +1,191 @@ # -*- 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 ConfigParser import RawConfigParser import os import sys import time import socket from urlparse import urlparse 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.keys(): - if len(freebusy_settings[section].keys()) < 1: + for section in freebusy_settings: + if len(freebusy_settings[section]) < 1: cfg_parser.remove_section(section) continue - for key in freebusy_settings[section].keys(): + 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_mta.py b/pykolab/setup/setup_mta.py index 903acd7..55b2695 100644 --- a/pykolab/setup/setup_mta.py +++ b/pykolab/setup/setup_mta.py @@ -1,540 +1,540 @@ # -*- 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 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.keys(): + 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.keys(): + 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'): 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/translit.py b/pykolab/translit.py index 0ff042d..cf54bae 100644 --- a/pykolab/translit.py +++ b/pykolab/translit.py @@ -1,132 +1,132 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.translit') locale_translit_map = { 'ru_RU': 'cyrillic' } translit_map = { 'cyrillic': { u'А': 'A', u'а': 'a', u'Б': 'B', u'б': 'b', u'В': 'V', u'в': 'v', u'Г': 'G', u'г': 'g', u'Д': 'D', u'д': 'd', u'Е': 'E', u'е': 'e', u'Ё': 'Yo', u'ё': 'e', u'Ж': 'Zh', u'ж': 'zh', u'З': 'Z', u'з': 'z', u'И': 'I', u'и': 'i', u'Й': 'J', u'й': 'j', u'К': 'K', u'к': 'k', u'Л': 'L', u'л': 'l', u'М': 'M', u'м': 'm', u'Н': 'N', u'н': 'n', u'О': 'O', u'о': 'o', u'П': 'P', u'п': 'p', u'Р': 'R', u'р': 'r', u'С': 'S', u'с': 's', u'Т': 'T', u'т': 't', u'У': 'U', u'у': 'u', u'Ф': 'F', u'ф': 'f', u'Х': 'Kh', u'х': 'kh', u'Ц': 'Tc', u'ц': 'tc', u'Ч': 'Ch', u'ч': 'ch', u'Ш': 'Sh', u'ш': 'sh', u'Щ': 'Shch', u'щ': 'shch', u'Ъ': '', u'ъ': '', u'Ы': 'Y', u'ы': 'y', u'Ь': '', u'ь': '', u'Э': 'E', u'э': 'e', u'Ю': 'Yu', u'ю': 'yu', u'Я': 'Ya', u'я': 'ya', } } def transliterate(_input, lang, _output_expected=None): if locale_translit_map.has_key(lang): _translit_name = locale_translit_map[lang] else: _translit_name = lang _output = '' if not isinstance(_input, unicode): for char in _input.decode('utf-8'): if translit_map.has_key(_translit_name): if translit_map[_translit_name].has_key(char): _output += translit_map[_translit_name][char] - elif char in [repr(x) for x in translit_map[_translit_name].keys()]: - _output += translit_map[_translit_name][[char in [raw(x) for x in translit_map[_translit_name].keys()]][0]] + elif char in [repr(x) for x in translit_map[_translit_name]]: + _output += translit_map[_translit_name][[char in [raw(x) for x in translit_map[_translit_name]]][0]] else: _output += char else: _output += char else: for char in _input: if translit_map.has_key(_translit_name): if translit_map[_translit_name].has_key(char): _output += translit_map[_translit_name][char] - elif char in [repr(x) for x in translit_map[_translit_name].keys()]: - _output += translit_map[_translit_name][[char in [raw(x) for x in translit_map[_translit_name].keys()]][0]] + elif char in [repr(x) for x in translit_map[_translit_name]]: + _output += translit_map[_translit_name][[char in [raw(x) for x in translit_map[_translit_name]]][0]] else: _output += char else: _output += char return _output diff --git a/pykolab/utils.py b/pykolab/utils.py index bda7e76..e84f13b 100644 --- a/pykolab/utils.py +++ b/pykolab/utils.py @@ -1,635 +1,635 @@ # -*- 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 base64 import getpass import grp import os import pwd from six import string_types import struct import sys import pykolab from pykolab import constants from pykolab.translate import _ as _l # pylint: disable=invalid-name log = pykolab.getLogger('pykolab.utils') conf = pykolab.getConf() try: # pylint: disable=redefined-builtin input = raw_input except NameError: pass try: unicode('') except NameError: unicode = str # pylint: disable=too-many-branches def ask_question(question, default="", password=False, confirm=False): """ Ask a question on stderr. Since the answer to the question may actually be a password, cover that case with a getpass.getpass() prompt. Accepts a default value, but ignores defaults for password prompts. Usage: pykolab.utils.ask_question("What is the server?", default="localhost") """ if default != "" and default is not None and conf.cli_keywords.answer_default: if not conf.cli_keywords.quiet: print("%s [%s]: " % (question, default)) return default if password: if default == "" or default is None: answer = getpass.getpass("%s: " % (question)) else: answer = getpass.getpass("%s [%s]: " % (question, default)) else: if default == "" or default is None: answer = input("%s: " % (question)) else: answer = input("%s [%s]: " % (question, default)) # pylint: disable=too-many-nested-blocks if not answer == "": if confirm: answer_confirm = None answer_confirmed = False while not answer_confirmed: if password: answer_confirm = getpass.getpass(_l("Confirm %s: ") % (question)) else: answer_confirm = input(_l("Confirm %s: ") % (question)) if not answer_confirm == answer: print(_l("Incorrect confirmation. Please try again."), file=sys.stderr) if password: if default == "" or default is None: answer = getpass.getpass(_l("%s: ") % (question)) else: answer = getpass.getpass(_l("%s [%s]: ") % (question, default)) else: if default == "" or default is None: answer = input(_l("%s: ") % (question)) else: answer = input(_l("%s [%s]: ") % (question, default)) else: answer_confirmed = True if answer == "": return default return answer # pylint: disable=too-many-return-statements def ask_confirmation(question, default="y", all_inclusive_no=True): """ Create a confirmation dialog, including a default option (capitalized), and a "yes" or "no" parsing that can either require an explicit, full "yes" or "no", or take the default or any YyNn answer. """ default_answer = None if default in ["y", "Y"]: default_answer = True default_no = "n" default_yes = "Y" elif default in ["n", "N"]: default_answer = False default_no = "N" default_yes = "y" else: # This is a 'yes' or 'no' question the user # needs to provide the full yes or no for. default_no = "'no'" default_yes = "Please type 'yes'" if conf.cli_keywords.answer_yes \ or (conf.cli_keywords.answer_default and default_answer is not None): if not conf.cli_keywords.quiet: print("%s [%s/%s]: " % (question, default_yes, default_no)) if conf.cli_keywords.answer_yes: return True if conf.cli_keywords.answer_default: return default_answer answer = False while not answer: answer = input("%s [%s/%s]: " % (question, default_yes, default_no)) # Parse answer and set back to False if not appropriate if all_inclusive_no: if answer == "" and default_answer is not None: return default_answer if answer in ["y", "Y", "yes"]: return True if answer in ["n", "N", "no"]: return False answer = False print(_l("Please answer 'yes' or 'no'."), file=sys.stderr) if answer not in ["y", "Y", "yes"]: return False return True # pylint: disable=dangerous-default-value def ask_menu(question, options={}, default=''): if default != '' and conf.cli_keywords.answer_default: if not conf.cli_keywords.quiet: print(question + " [" + default + "]:") return default if default != '': print(question + " [" + default + "]:") else: print(question) answer_correct = False max_key_length = 0 if isinstance(options, list): _options = options options = {} for key in _options: options[key] = key keys = options.keys() keys.sort() while not answer_correct: for key in keys: key_length = len("%s" % key) if key_length > max_key_length: max_key_length = key_length str_format = "%%%ds" % max_key_length - if default == '' or default not in options.keys(): + if default == '' or default not in options: for key in keys: if options[key] == key: print(" - " + key) else: print(" - " + str_format % key + ": " + options[key]) answer = input(_l("Choice") + ": ") else: answer = input(_l("Choice (type '?' for options)") + ": ") if answer == '?': for key in keys: if options[key] == key: print(" - " + key) else: print(" - " + str_format % key + ": " + options[key]) continue - if answer == '' and default in options.keys(): + if answer == '' and default in options: answer = default - if answer in [str(x) for x in options.keys()]: + if answer in [str(x) for x in options]: answer_correct = True return answer def decode(key, enc): if key is None: return enc dec = [] enc = base64.urlsafe_b64decode(enc) for i in range(len(enc)): key_c = key[i % len(key)] dec_c = chr((256 + ord(enc[i]) - ord(key_c)) % 256) dec.append(dec_c) return "".join(dec) def encode(key, clear): if key is None: return clear enc = [] for i in range(len(clear)): key_c = key[i % len(key)] enc_c = chr((ord(clear[i]) + ord(key_c)) % 256) enc.append(enc_c) return base64.urlsafe_b64encode("".join(enc)) def ensure_directory(_dir, _user='root', _group='root'): if not os.path.isdir(_dir): os.makedirs(_dir) 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(_group) except KeyError: print(_l("Group %s does not exist") % (_group), file=sys.stderr) sys.exit(1) # Set real and effective group if not the same as current. if not group_gid == rgid: os.chown(_dir, -1, group_gid) if ruid == 0: # Means we haven't switched yet. try: (_, _, user_uid, _, _, _, _) = pwd.getpwnam(_user) except KeyError: print(_l("User %s does not exist") % (_user), file=sys.stderr) sys.exit(1) # Set real and effective user if not the same as current. if not user_uid == ruid: os.chown(_dir, user_uid, -1) except Exception: print(_l("Could not change the permissions on %s") % (_dir), file=sys.stderr) def generate_password(): import subprocess p1 = subprocess.Popen(['head', '-c', '200', '/dev/urandom'], stdout=subprocess.PIPE) p2 = subprocess.Popen(['tr', '-dc', '_A-Z-a-z-0-9'], stdin=p1.stdout, stdout=subprocess.PIPE) p3 = subprocess.Popen(['head', '-c', '15'], stdin=p2.stdout, stdout=subprocess.PIPE) p1.stdout.close() p2.stdout.close() output = p3.communicate()[0] return output def multiline_message(message): if hasattr(conf, 'cli_keywords') and hasattr(conf.cli_keywords, 'quiet'): if conf.cli_keywords.quiet: return "" column_width = 80 # First, replace all occurences of "\n" message = message.replace(" ", "") message = message.replace("\n", " ") lines = [] line = "" for word in message.split(): if (len(line) + len(word)) > column_width: lines.append(line) line = word else: if line == "": line = word else: line += " %s" % (word) lines.append(line) return "\n%s\n" % ("\n".join(lines)) def stripped_message(message): return "\n" + message.strip() + "\n" def str2unicode(s, encoding='utf-8'): if isinstance(s, unicode): return s try: return unicode(s, encoding) except Exception: pass return s def normalize(_object): if isinstance(_object, list): result = [] elif isinstance(_object, dict): result = {} else: return _object if isinstance(_object, list): for item in _object: result.append(item.lower()) result = list(set(result)) return result if isinstance(_object, dict): def _strip(value): try: return value.strip() except Exception: return value for key in _object: if isinstance(_object[key], list): if _object[key] is None: continue # Dont run strip anything from attributes which # hold byte strings if key.lower() in constants.BINARY_ATTRS: val = _object[key] else: val = map(_strip, _object[key]) if len(val) == 1: result[key.lower()] = ''.join(val) else: result[key.lower()] = val else: if _object[key] is None: continue result[key.lower()] = _strip(_object[key]) if 'objectsid' in result and not result['objectsid'][0] == "S": result['objectsid'] = sid_to_string(result['objectsid']) if 'sn' in result: result['surname'] = result['sn'].replace(' ', '') if 'mail' in result: if isinstance(result['mail'], list): result['mail'] = result['mail'][0] if result['mail']: if len(result['mail'].split('@')) > 1: result['domain'] = result['mail'].split('@')[1] if 'domain' not in result and 'standard_domain' in result: result['domain'] = result['standard_domain'] if 'objectclass' not in result: result['objectclass'] = [] if result['objectclass'] is None: result['objectclass'] = [] if not isinstance(result['objectclass'], list): result['objectclass'] = [result['objectclass']] result['objectclass'] = [x.lower() for x in result['objectclass']] return result def parse_input(_input, splitchars=[' ']): """ Split the input string using the split characters defined in splitchars, and remove the empty list items, then unique the list items. Takes a string as input, and a list of characters the string should be split with (list of delimiter characters). """ _parse_list = _input.split(splitchars.pop()) _output_list = [] for splitchar in splitchars: __parse_list = [] for item in _parse_list: __parse_list.extend(item.split(splitchar)) _parse_list = __parse_list for item in _parse_list: if not item == '': if _output_list.count(item) < 1: _output_list.append(item) return _output_list def parse_ldap_uri(uri): """ Parse an LDAP URI and return it's components. Returns a tuple containing; - protocol (ldap, ldaps), - server (address or None), - base_dn, - attrs (list of attributes length 1, or empty), - scope, - filter or None on failure """ _protocol = uri.split(':')[0] try: try: _ldap_uri, _attr, _scope, _filter = uri.split('?') _server = _ldap_uri.split('//')[1].split('/')[0] _base_dn = _ldap_uri.split('//')[1].split('/')[1] except Exception: _server = uri.split('//')[1].split('/')[0] _attr = None _scope = None _filter = None _base_dn = None if len(_server.split(':')) > 1: _port = _server.split(':')[1] _server = _server.split(':')[0] else: if _protocol == 'ldaps': _port = "636" else: _port = "389" if _server == '': _server = None if _attr == '': _attrs = [] else: _attrs = [_attr] if _scope == '': _scope = 'sub' if _filter == '': _filter = "(objectclass=*)" return (_protocol, _server, _port, _base_dn, _attrs, _scope, _filter) except Exception: return None def pop_empty_from_list(_input_list): _output_list = [] for item in _input_list: if not item == '': _output_list.append(item) def sid_to_string(sid): srl = ord(sid[0]) number_sub_id = ord(sid[1]) iav = struct.unpack('!Q', '\x00\x00' + sid[2:8])[0] sub_ids = [] for i in range(number_sub_id): sub_ids.append(struct.unpack(' 1: (locale_name, locale_charset) = locale.normalize(locale_name).split('.') else: locale_charset = 'utf-8' try: log.debug(_l("Attempting to set locale"), level=8) locale.setlocale(locale.LC_ALL, (locale_name, locale_charset)) log.debug(_l("Success setting locale"), level=8) except Exception: log.debug(_l("Failure to set locale"), level=8) command = ['/usr/bin/iconv', '-f', 'UTF-8', '-t', 'ASCII//TRANSLIT', '-s'] log.debug(_l("Executing '%s | %s'") % (r"%s" % (mystring), ' '.join(command)), level=8) process = subprocess.Popen( command, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE, env={'LANG': locale.normalize(locale_name)} ) try: print(r"%s" % (mystring), file=process.stdin) except UnicodeEncodeError: pass result = process.communicate()[0].strip() if '?' in result or (result == '' and not mystring == ''): log.warning(_l("Could not translate %s using locale %s") % (mystring, locale_name)) from pykolab import translit result = translit.transliterate(mystring, locale_name) return result def true_or_false(val): if val is None: return False if isinstance(val, bool): return val if isinstance(val, string_types): val = val.lower() if val in ["true", "yes", "y", "1"]: return True else: return False if isinstance(val, int) or isinstance(val, float): if val >= 1: return True else: return False def is_service(services): """ Checks each item in list services to see if it has a RC script in pykolab.constants.RC_DIR to see if it's a service, and returns the name of the service for the first service it can find. However, it also checks whether the other services exist and issues a warning if more then one service exists. Usage: utils.is_service(['dirsrv', 'ldap']) """ _service = None _other_services = [] for service in services: if os.path.isfile(os.path.join(constants.RC_DIR, service)): if _service == '': _service = service else: _other_services.append(service) return (_service, _other_services) diff --git a/pykolab/wap_client/__init__.py b/pykolab/wap_client/__init__.py index 956fbf5..034be05 100644 --- a/pykolab/wap_client/__init__.py +++ b/pykolab/wap_client/__init__.py @@ -1,661 +1,661 @@ import json import httplib import urllib import sys from urlparse import urlparse import pykolab from pykolab import utils from pykolab.translate import _ log = pykolab.getLogger('pykolab.wap_client') conf = pykolab.getConf() if not hasattr(conf, 'defaults'): conf.finalize_conf() API_HOSTNAME = "localhost" API_SCHEME = "http" API_PORT = 80 API_SSL = False API_BASE = "/kolab-webadmin/api/" kolab_wap_url = conf.get('kolab_wap', 'api_url') if not kolab_wap_url == None: result = urlparse(kolab_wap_url) else: result = None if hasattr(result, 'scheme') and result.scheme == 'https': API_SSL = True API_PORT = 443 if hasattr(result, 'hostname'): API_HOSTNAME = result.hostname if hasattr(result, 'port'): API_PORT = result.port if hasattr(result, 'path'): API_BASE = result.path session_id = None conn = None def authenticate(username=None, password=None, domain=None): global session_id if username == None: username = conf.get('ldap', 'bind_dn') if password == None: password = conf.get('ldap', 'bind_pw') if domain == None: domain = conf.get('kolab', 'primary_domain') post = json.dumps( { 'username': username, 'password': password, 'domain': domain } ) response = request('POST', "system.authenticate", post=post) if not response: return False if response.has_key('session_token'): session_id = response['session_token'] return True def connect(uri=None): global conn, API_SSL, API_PORT, API_HOSTNAME, API_BASE if not uri == None: result = urlparse(uri) if hasattr(result, 'scheme') and result.scheme == 'https': API_SSL = True API_PORT = 443 if hasattr(result, 'hostname'): API_HOSTNAME = result.hostname if hasattr(result, 'port'): API_PORT = result.port if hasattr(result, 'path'): API_BASE = result.path if conn == None: if API_SSL: conn = httplib.HTTPSConnection(API_HOSTNAME, API_PORT) else: conn = httplib.HTTPConnection(API_HOSTNAME, API_PORT) conn.connect() return conn def disconnect(quit=False): global conn, session_id if quit and session_id: request('GET', 'system.quit') session_id = None if conn: conn.close() conn = None def domain_add(domain, aliases=[]): dna = conf.get('ldap', 'domain_name_attribute') post = json.dumps({ dna: [ domain ] + aliases }) return request('POST', 'domain.add', post=post) def domain_delete(domain, force=False): domain_id, domain_attrs = domain_find(domain).popitem() param = {} param['id'] = domain_id if force: param['force'] = force post = json.dumps(param) return request('POST', 'domain.delete', post=post) def domain_find(domain): dna = conf.get('ldap', 'domain_name_attribute') get = { dna: domain } return request('GET', 'domain.find', get=get) def domain_info(domain): domain_id, domain_attrs = domain_find(domain) get = { 'id': domain_id } return request('GET', 'domain.info', get=get) def domains_capabilities(): return request('GET', 'domains.capabilities') def domains_list(): return request('GET', 'domains.list') def form_value_generate(params): post = json.dumps(params) return request('POST', 'form_value.generate', post=post) def form_value_generate_password(*args, **kw): return request('GET', 'form_value.generate_password') def form_value_list_options(object_type, object_type_id, attribute): post = json.dumps( { 'object_type': object_type, 'type_id': object_type_id, 'attribute': attribute } ) return request('POST', 'form_value.list_options', post=post) def form_value_select_options(object_type, object_type_id, attribute): post = json.dumps( { 'object_type': object_type, 'type_id': object_type_id, 'attributes': [ attribute ] } ) return request('POST', 'form_value.select_options', post=post) def get_group_input(): group_types = group_types_list() - if len(group_types.keys()) > 1: - for key in group_types.keys(): + if len(group_types) > 1: + for key in group_types: if not key == "status": print("%s) %s" % (key,group_types[key]['name'])) group_type_id = utils.ask_question("Please select the group type") - elif len(group_types.keys()) > 0: + elif len(group_types) > 0: print("Automatically selected the only group type available") group_type_id = group_types.keys()[0] else: print("No group types available") sys.exit(1) if group_types.has_key(group_type_id): group_type_info = group_types[group_type_id]['attributes'] else: print("No such group type") sys.exit(1) params = { 'group_type_id': group_type_id } - for attribute in group_type_info['form_fields'].keys(): + for attribute in group_type_info['form_fields']: params[attribute] = utils.ask_question(attribute) - for attribute in group_type_info['auto_form_fields'].keys(): + for attribute in group_type_info['auto_form_fields']: exec("retval = group_form_value_generate_%s(params)" % (attribute)) params[attribute] = retval[attribute] return params def get_user_input(): user_types = user_types_list() if user_types['count'] > 1: print("") - for key in user_types['list'].keys(): + for key in user_types['list']: if not key == "status": print("%s) %s" % (key,user_types['list'][key]['name'])) print("") user_type_id = utils.ask_question("Please select the user type") elif user_types['count'] > 0: print("Automatically selected the only user type available") user_type_id = user_types['list'].keys()[0] else: print("No user types available") sys.exit(1) if user_types['list'].has_key(user_type_id): user_type_info = user_types['list'][user_type_id]['attributes'] else: print("No such user type") sys.exit(1) params = { 'object_type': 'user', 'type_id': user_type_id } must_attrs = [] may_attrs = [] - for attribute in user_type_info['form_fields'].keys(): + for attribute in user_type_info['form_fields']: if isinstance(user_type_info['form_fields'][attribute], dict): if user_type_info['form_fields'][attribute].has_key('optional') and user_type_info['form_fields'][attribute]['optional']: may_attrs.append(attribute) else: must_attrs.append(attribute) else: must_attrs.append(attribute) for attribute in must_attrs: if isinstance(user_type_info['form_fields'][attribute], dict) and \ user_type_info['form_fields'][attribute].has_key('type'): if user_type_info['form_fields'][attribute]['type'] == 'select': if not user_type_info['form_fields'][attribute].has_key('values'): attribute_values = form_value_select_options('user', user_type_id, attribute) default = '' if attribute_values[attribute].has_key('default'): default = attribute_values[attribute]['default'] params[attribute] = utils.ask_menu( "Choose the %s value" % (attribute), attribute_values[attribute]['list'], default=default ) else: default = '' if user_type_info['form_fields'][attribute].has_key('default'): default = user_type_info['form_fields'][attribute]['default'] params[attribute] = utils.ask_menu( "Choose the %s value" % (attribute), user_type_info['form_fields'][attribute]['values'], default=default ) else: params[attribute] = utils.ask_question(attribute) else: params[attribute] = utils.ask_question(attribute) - for attribute in user_type_info['fields'].keys(): + for attribute in user_type_info['fields']: params[attribute] = user_type_info['fields'][attribute] exec("retval = user_form_value_generate(params)") print(retval) return params def group_add(params=None): if params == None: params = get_group_input() post = json.dumps(params) return request('POST', 'group.add', post=post) def group_delete(params=None): if params == None: params = { 'id': utils.ask_question("Name of group to delete", "group") } post = json.dumps(params) return request('POST', 'group.delete', post=post) def group_form_value_generate_mail(params=None): if params == None: params = get_user_input() params = json.dumps(params) return request('POST', 'group_form_value.generate_mail', params) def group_find(params=None): post = { 'search': { 'params': {} } } for (k,v) in params.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } return request('POST', 'group.find', post=json.dumps(post)) def group_info(group=None): if group == None: group = utils.ask_question("group DN") return request('GET', 'group.info', get={ 'id': group }) def group_members_list(group=None): if group == None: group = utils.ask_question("Group email address") group = request('GET', 'group.members_list?group=%s' % (group)) return group def group_types_list(): return request('GET', 'group_types.list') def groups_list(params={}): return request('POST', 'groups.list', post=json.dumps(params)) def ou_add(params={}): return request('POST', 'ou.add', post=json.dumps(params)) def ou_delete(params={}): return request('POST', 'ou.delete', post=json.dumps(params)) def ou_edit(params={}): return request('POST', 'ou.edit', post=json.dumps(params)) def ou_find(params=None): post = { 'search': { 'params': {} } } for (k,v) in params.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } return request('POST', 'ou.find', post=json.dumps(post)) def ou_info(ou): _params = { 'id': ou } ou = request('GET', 'ou.info', get=_params) return ou def ous_list(params={}): return request('POST', 'ous.list', post=json.dumps(params)) def request(method, api_uri, get=None, post=None, headers={}): response_data = request_raw(method, api_uri, get, post, headers) if response_data['status'] == "OK": del response_data['status'] return response_data['result'] else: print("%s: %s (code %s)" % (response_data['status'], response_data['reason'], response_data['code'])) return False def request_raw(method, api_uri, get=None, post=None, headers={}, isretry=False): global session_id if not session_id == None: headers["X-Session-Token"] = session_id reconnect = False conn = connect() if conf.debuglevel > 8: conn.set_debuglevel(9) if not get == None: _get = "?%s" % (urllib.urlencode(get)) else: _get = "" log.debug(_("Requesting %r with params %r") % ("%s/%s" % (API_BASE,api_uri), (get, post)), level=8) try: conn.request(method.upper(), "%s/%s%s" % (API_BASE, api_uri, _get), post, headers) response = conn.getresponse() data = response.read() log.debug(_("Got response: %r") % (data), level=8) except (httplib.BadStatusLine, httplib.CannotSendRequest) as e: if isretry: raise e log.info(_("Connection error: %r; re-connecting..."), e) reconnect = True # retry with a new connection if reconnect: disconnect() return request_raw(method, api_uri, get, post, headers, True) try: response_data = json.loads(data) except ValueError: # Some data is not JSON log.error(_("Response data is not JSON")) return response_data def resource_add(params=None): if params == None: params = get_user_input() return request('POST', 'resource.add', post=json.dumps(params)) def resource_delete(params=None): if params == None: params = { 'id': utils.ask_question("Resource DN to delete", "resource") } return request('POST', 'resource.delete', post=json.dumps(params)) def resource_find(params=None): post = { 'search': { 'params': {} } } for (k,v) in params.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } return request('POST', 'resource.find', post=json.dumps(post)) def resource_info(resource=None): if resource == None: resource = utils.ask_question("Resource DN") return request('GET', 'resource.info', get={ 'id': resource }) def resource_types_list(): return request('GET', 'resource_types.list') def resources_list(params={}): return request('POST', 'resources.list', post=json.dumps(params)) def role_add(params=None): if params == None: role_name = utils.ask_question("Role name") params = { 'cn': role_name } params = json.dumps(params) return request('POST', 'role.add', params) def role_capabilities(): return request('GET', 'role.capabilities') def role_delete(params=None): if params == None: role_name = utils.ask_question("Role name") role = role_find_by_attribute({'cn': role_name}) params = { 'role': role.keys()[0] } if not params.has_key('role'): role = role_find_by_attribute(params) params = { 'role': role.keys()[0] } post = json.dumps(params) return request('POST', 'role.delete', post=post) def role_find_by_attribute(params=None): if params == None: role_name = utils.ask_question("Role name") else: role_name = params['cn'] get = { 'cn': role_name } role = request('GET', 'role.find_by_attribute', get=get) return role def role_info(role_name): role = role_find_by_attribute({'cn': role_name}) get = { 'role': role['id'] } role = request('GET', 'role.info', get=get) return role def roles_list(): return request('GET', 'roles.list') def sharedfolder_add(params=None): if params == None: params = get_user_input() return request('POST', 'sharedfolder.add', post=json.dumps(params)) def sharedfolder_delete(params=None): if params == None: params = { 'id': utils.ask_question("Shared Folder DN to delete", "sharedfolder") } return request('POST', 'sharedfolder.delete', post=json.dumps(params)) def sharedfolders_list(params={}): return request('POST', 'sharedfolders.list', post=json.dumps(params)) def system_capabilities(domain=None): return request('GET', 'system.capabilities', get={'domain':domain}) def system_get_domain(): return request('GET', 'system.get_domain') def system_select_domain(domain=None): if domain == None: domain = utils.ask_question("Domain name") get = { 'domain': domain } return request('GET', 'system.select_domain', get=get) def user_add(params=None): if params == None: params = get_user_input() params = json.dumps(params) return request('POST', 'user.add', post=params) def user_delete(params=None): if params == None: params = { 'id': utils.ask_question("Username for user to delete", "user") } post = json.dumps(params) return request('POST', 'user.delete', post=post) def user_edit(user = None, attributes={}): if user == None: get = { 'id': utils.ask_question("Username for user to edit", "user") } else: get = { 'id': user } user_info = request('GET', 'user.info', get=get) - for attribute in attributes.keys(): + for attribute in attributes: user_info[attribute] = attributes[attribute] post = json.dumps(user_info) user_edit = request('POST', 'user.edit', get=get, post=post) return user_edit def user_find(attribs=None): if attribs == None: post = { 'search': { 'params': { utils.ask_question("Attribute") : { 'value': utils.ask_question("value"), 'type': 'exact' } } } } else: post = { 'search': { 'params': {} } } for (k,v) in attribs.iteritems(): post['search']['params'][k] = { 'value': v, 'type': 'exact' } post = json.dumps(post) user = request('POST', 'user.find', post=post) return user def user_form_value_generate(params=None): if params == None: params = get_user_input() post = json.dumps(params) return request('POST', 'form_value.generate', post=post) def user_form_value_generate_uid(params=None): if params == None: params = get_user_input() params = json.dumps(params) return request('POST', 'form_value.generate_uid', params) def user_form_value_generate_userpassword(*args, **kw): result = form_value_generate_password() return { 'userpassword': result['password'] } def user_info(user=None): if user == None: user = utils.ask_question("User email address") _params = { 'id': user } user = request('GET', 'user.info', get=_params) return user def users_list(params={}): return request('POST', 'users.list', post=json.dumps(params)) def user_types_list(): return request('GET', 'user_types.list') diff --git a/pykolab/xml/attendee.py b/pykolab/xml/attendee.py index 099e21a..b7be74b 100644 --- a/pykolab/xml/attendee.py +++ b/pykolab/xml/attendee.py @@ -1,280 +1,280 @@ import kolabformat from pykolab.translate import _ from pykolab.translate import N_ from contact_reference import ContactReference participant_status_labels = { "NEEDS-ACTION": N_("Needs Action"), "ACCEPTED": N_("Accepted"), "DECLINED": N_("Declined"), "TENTATIVE": N_("Tentatively Accepted"), "DELEGATED": N_("Delegated"), "IN-PROCESS": N_("Started"), "COMPLETED": N_("Completed"), "PENDING": N_("Pending"), # support integer values, too kolabformat.PartNeedsAction: N_("Needs Action"), kolabformat.PartAccepted: N_("Accepted"), kolabformat.PartDeclined: N_("Declined"), kolabformat.PartTentative: N_("Tentatively Accepted"), kolabformat.PartDelegated: N_("Delegated"), kolabformat.PartInProcess: N_("Started"), kolabformat.PartCompleted: N_("Completed"), } def participant_status_label(status): return _(participant_status_labels[status]) if participant_status_labels.has_key(status) else _(status) class Attendee(kolabformat.Attendee): cutype_map = { "INDIVIDUAL": kolabformat.CutypeIndividual, "RESOURCE": kolabformat.CutypeResource, "GROUP": kolabformat.CutypeGroup, "ROOM": kolabformat.CutypeRoom, "UNKNOWN": kolabformat.CutypeUnknown, } participant_status_map = { "NEEDS-ACTION": kolabformat.PartNeedsAction, "ACCEPTED": kolabformat.PartAccepted, "DECLINED": kolabformat.PartDeclined, "TENTATIVE": kolabformat.PartTentative, "DELEGATED": kolabformat.PartDelegated, "IN-PROCESS": kolabformat.PartInProcess, "COMPLETED": kolabformat.PartCompleted, } # See RFC 2445, 5445 role_map = { "CHAIR": kolabformat.Chair, "REQ-PARTICIPANT": kolabformat.Required, "OPT-PARTICIPANT": kolabformat.Optional, "NON-PARTICIPANT": kolabformat.NonParticipant, } rsvp_map = { "TRUE": True, "FALSE": False, } properties_map = { 'role': 'get_role', 'rsvp': 'rsvp', 'partstat': 'get_participant_status', 'cutype': 'get_cutype', 'delegated-to': 'get_delegated_to', 'delegated-from': 'get_delegated_from', } def __init__( self, email, name=None, rsvp=False, role=None, participant_status=None, cutype=None, ical_params=None ): self.email = email self.contactreference = ContactReference(email) if not name == None: self.contactreference.set_name(name) kolabformat.Attendee.__init__(self, self.contactreference) if isinstance(rsvp, bool): self.setRSVP(rsvp) else: if self.rsvp_map.has_key(rsvp): self.setRSVP(self.rsvp_map[rsvp]) if not role == None: self.set_role(role) if not cutype == None: self.set_cutype(cutype) if ical_params and ical_params.has_key('DELEGATED-FROM'): self.delegate_from(Attendee(str(ical_params['DELEGATED-FROM']), role=self.get_role(), cutype=self.get_cutype())) if ical_params and ical_params.has_key('DELEGATED-TO'): self.delegate_to(Attendee(str(ical_params['DELEGATED-TO']))) if not participant_status == None: self.set_participant_status(participant_status) def copy_from(self, obj): if isinstance(obj, kolabformat.Attendee): self.contactreference = ContactReference(obj.contact()) self.email = self.contactreference.get_email() self.setContact(self.contactreference) # manually copy all properities, copy constructor doesn't work :-( self.setRSVP(obj.rsvp()) self.setRole(obj.role()) self.setCutype(obj.cutype()) self.setPartStat(obj.partStat()) self.setDelegatedTo(obj.delegatedTo()) self.setDelegatedFrom(obj.delegatedFrom()) def delegate_from(self, delegators): crefs = [] if not isinstance(delegators, list): delegators = [delegators] for delegator in delegators: if not isinstance(delegator, Attendee): raise ValueError(_("Not a valid attendee")) else: self.set_role(delegator.get_role()) self.set_cutype(delegator.get_cutype()) crefs.append(delegator.contactreference) if len(crefs) == 0: raise ValueError(_("No valid delegator references found")) else: crefs += self.get_delegated_from() self.setDelegatedFrom(list(set(crefs))) def delegate_to(self, delegatees): self.set_participant_status("DELEGATED") crefs = [] if not isinstance(delegatees, list): delegatees = [delegatees] for delegatee in delegatees: if not isinstance(delegatee, Attendee): raise ValueError(_("Not a valid attendee")) else: crefs.append(delegatee.contactreference) if len(crefs) == 0: raise ValueError(_("No valid delegatee references found")) else: crefs += self.get_delegated_to() self.setDelegatedTo(list(set(crefs))) def get_cutype(self, translated=False): cutype = self.cutype() if translated: return self._translate_value(cutype, self.cutype_map) return cutype def get_delegated_from(self, translated=False): delegators = [] for cr in self.delegatedFrom(): delegators.append(cr.email() if translated else ContactReference(cr)) return delegators def get_delegated_to(self, translated=False): delegatees = [] for cr in self.delegatedTo(): delegatees.append(cr.email() if translated else ContactReference(cr)) return delegatees def get_email(self): return self.contactreference.get_email() def get_name(self): return self.contactreference.get_name() def get_displayname(self): name = self.contactreference.get_name() email = self.contactreference.get_email() return "%s <%s>" % (name, email) if not name == "" else email def get_participant_status(self, translated=False): partstat = self.partStat() if translated: return self._translate_value(partstat, self.participant_status_map) return partstat def get_role(self, translated=False): role = self.role() if translated: return self._translate_value(role, self.role_map) return role def get_rsvp(self): return self.rsvp() def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) return name_map[val] if name_map.has_key(val) else 'UNKNOWN' def set_cutype(self, cutype): - if cutype in self.cutype_map.keys(): + 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.keys(): + 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.keys(): + if role in self.role_map: self.setRole(self.role_map[role]) elif role in self.role_map.values(): self.setRole(role) else: raise InvalidAttendeeRoleError(_("Invalid role %r") % (role)) def set_rsvp(self, rsvp): self.setRSVP(rsvp) def to_dict(self): data = self.contactreference.to_dict() data.pop('type', None) for p, getter in self.properties_map.iteritems(): val = None args = {} if hasattr(self, getter): if getter.startswith('get_'): args = dict(translated=True) val = getattr(self, getter)(**args) if val is not None: data[p] = val return data def __str__(self): return self.email class AttendeeIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeCutypeError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeParticipantStatusError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidAttendeeRoleError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/event.py b/pykolab/xml/event.py index 3fa5cdc..d85b621 100644 --- a/pykolab/xml/event.py +++ b/pykolab/xml/event.py @@ -1,1511 +1,1511 @@ import datetime import icalendar import kolabformat import pytz import time import uuid import base64 import re import pykolab from pykolab import constants from pykolab import utils from pykolab.xml import utils as xmlutils from pykolab.xml import participant_status_label from pykolab.xml.utils import ustr from pykolab.translate import _ from os import path from attendee import Attendee from contact_reference import ContactReference from recurrence_rule import RecurrenceRule from collections import OrderedDict log = pykolab.getLogger('pykolab.xml_event') def event_from_ical(ical, string=None): return Event(from_ical=ical, from_string=string) def event_from_string(string): return Event(from_string=string) def event_from_message(message): event = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/calendar+xml": payload = part.get_payload(decode=True) event = event_from_string(payload) # append attachment parts to Event object elif event and part.has_key('Content-ID'): event._attachment_parts.append(part) return event class Event(object): type = 'event' thisandfuture = False status_map = { None: kolabformat.StatusUndefined, "TENTATIVE": kolabformat.StatusTentative, "CONFIRMED": kolabformat.StatusConfirmed, "CANCELLED": kolabformat.StatusCancelled, "COMPLETED": kolabformat.StatusCompleted, "IN-PROCESS": kolabformat.StatusInProcess, "NEEDS-ACTION": kolabformat.StatusNeedsAction, } classification_map = { "PUBLIC": kolabformat.ClassPublic, "PRIVATE": kolabformat.ClassPrivate, "CONFIDENTIAL": kolabformat.ClassConfidential, } alarm_type_map = { 'EMAIL': kolabformat.Alarm.EMailAlarm, 'DISPLAY': kolabformat.Alarm.DisplayAlarm, 'AUDIO': kolabformat.Alarm.AudioAlarm } related_map = { 'START': kolabformat.Start, 'END': kolabformat.End } properties_map = { # property: getter "uid": "get_uid", "created": "get_created", "lastmodified-date": "get_lastmodified", "sequence": "sequence", "classification": "get_classification", "categories": "categories", "start": "get_start", "end": "get_end", "duration": "get_duration", "transparency": "transparency", "rrule": "recurrenceRule", "rdate": "recurrenceDates", "exdate": "exceptionDates", "recurrence-id": "recurrenceID", "summary": "summary", "description": "description", "priority": "priority", "status": "get_ical_status", "location": "location", "organizer": "organizer", "attendee": "get_attendees", "attach": "attachments", "url": "url", "alarm": "alarms", "x-custom": "customProperties", # TODO: add to_dict() support for these # "exception": "exceptions", } def __init__(self, from_ical="", from_string=""): self._attendees = [] self._categories = [] self._exceptions = [] self._attachment_parts = [] if isinstance(from_ical, str) and from_ical == "": if from_string == "": self.event = kolabformat.Event() else: self.event = kolabformat.readEvent(from_string, False) self._load_attendees() self._load_exceptions() else: self.from_ical(from_ical, from_string) self.set_created(self.get_created()) self.uid = self.get_uid() def _load_attendees(self): for a in self.event.attendees(): att = Attendee(a.contact().email()) att.copy_from(a) self._attendees.append(att) def _load_exceptions(self): for ex in self.event.exceptions(): exception = Event() exception.uid = ex.uid() exception.event = ex exception._load_attendees() self._exceptions.append(exception) def add_attendee(self, email_or_attendee, name=None, rsvp=False, role=None, participant_status=None, cutype="INDIVIDUAL", params=None): if isinstance(email_or_attendee, Attendee): attendee = email_or_attendee else: attendee = Attendee(email_or_attendee, name, rsvp, role, participant_status, cutype, params) # apply update to self and all exceptions self.update_attendees([attendee], True) def add_category(self, category): self._categories.append(ustr(category)) self.event.setCategories(self._categories) def add_recurrence_date(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo is None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Rdate needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.addRecurrenceDate(xmlutils.to_cdatetime(_datetime, True)) def add_exception_date(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Exdate needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.addExceptionDate(xmlutils.to_cdatetime(_datetime, True)) def add_exception(self, exception): recurrence_id = exception.get_recurrence_id() if recurrence_id is None: raise EventIntegrityError("Recurrence exceptions require a Recurrence-ID property") # check if an exception with the given recurrence-id already exists append = True vexceptions = self.event.exceptions() for i, ex in enumerate(self._exceptions): if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture: # update the existing exception vexceptions[i] = exception.event self._exceptions[i] = exception append = False # check if main event matches the given recurrence-id if append and self.get_recurrence_id() == recurrence_id: self.event = exception.event self._load_attendees() self._load_exceptions() append = False if append: vexceptions.append(exception.event) self._exceptions.append(exception) self.event.setExceptions(vexceptions) def del_exception(self, exception): recurrence_id = exception.get_recurrence_id() if recurrence_id is None: raise EventIntegrityError("Recurrence exceptions require a Recurrence-ID property") updated = False vexceptions = self.event.exceptions() for i, ex in enumerate(self._exceptions): if ex.get_recurrence_id() == recurrence_id and ex.thisandfuture == exception.thisandfuture: del vexceptions[i] del self._exceptions[i] updated = True if updated: self.event.setExceptions(vexceptions) def as_string_itip(self, method="REQUEST"): cal = icalendar.Calendar() cal.add( 'prodid', '-//pykolab-%s-%s//kolab.org//' % ( constants.__version__, constants.__release__ ) ) cal.add('version', '2.0') # TODO: Really? cal.add('calscale', 'GREGORIAN') # TODO: Not always a request... cal.add('method', method) # TODO: Add timezone information using icalendar.?() # Not sure if there is a class for it. cal.add_component(self.to_ical()) # add recurrence exceptions if len(self._exceptions) > 0 and not method == 'REPLY': for exception in self._exceptions: cal.add_component(exception.to_ical()) if hasattr(cal, 'to_ical'): return cal.to_ical() elif hasattr(cal, 'as_string'): return cal.as_string() def to_ical(self): event = icalendar.Event() # Required event['uid'] = self.get_uid() # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(event.singletons)): _attr = attr.lower().replace('-', '') ical_getter = 'get_ical_%s' % (_attr) default_getter = 'get_%s' % (_attr) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() if not retval == None and not retval == "": event.add(attr.lower(), retval) elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if not retval == None and not retval == "": event.add(attr.lower(), retval, encode=0) # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(event.multiple)): _attr = attr.lower().replace('-', '') ical_getter = 'get_ical_%s' % (_attr) default_getter = 'get_%s' % (_attr) retval = None if hasattr(self, ical_getter): retval = getattr(self, ical_getter)() elif hasattr(self, default_getter): retval = getattr(self, default_getter)() if isinstance(retval, list) and not len(retval) == 0: for _retval in retval: event.add(attr.lower(), _retval, encode=0) # copy custom properties to iCal for cs in self.event.customProperties(): event.add(cs.identifier, cs.value) return event def delegate(self, delegators, delegatees, names=None): if not isinstance(delegators, list): delegators = [delegators] if not isinstance(delegatees, list): delegatees = [delegatees] if not isinstance(names, list): names = [names] _delegators = [] for delegator in delegators: _delegators.append(self.get_attendee(delegator)) _delegatees = [] for i,delegatee in enumerate(delegatees): try: _delegatees.append(self.get_attendee(delegatee)) except: # TODO: An iTip needs to be sent out to the new attendee self.add_attendee(delegatee, names[i] if i < len(names) else None) _delegatees.append(self.get_attendee(delegatee)) for delegator in _delegators: delegator.delegate_to(_delegatees) for delegatee in _delegatees: delegatee.delegate_from(_delegators) self.event.setAttendees(self._attendees) def from_ical(self, ical, raw=None): if isinstance(ical, icalendar.Event) or isinstance(ical, icalendar.Calendar): ical_event = ical elif hasattr(icalendar.Event, 'from_ical'): ical_event = icalendar.Event.from_ical(ical) elif hasattr(icalendar.Event, 'from_string'): ical_event = icalendar.Event.from_string(ical) # VCALENDAR block was given, find the first VEVENT item if isinstance(ical_event, icalendar.Calendar): for c in ical_event.walk(): if c.name == 'VEVENT': ical_event = c break # use the libkolab calendaring bindings to load the full iCal data if ical_event.has_key('RRULE') or ical_event.has_key('ATTACH') \ or [part for part in ical_event.walk() if part.name == 'VALARM']: if raw is None or raw == "": raw = ical if isinstance(ical, str) else ical.to_ical() self._xml_from_ical(raw) else: self.event = kolabformat.Event() # TODO: Clause the timestamps for zulu suffix causing datetime.datetime # to fail substitution. for attr in list(set(ical_event.required)): if ical_event.has_key(attr): self.set_from_ical(attr.lower(), ical_event[attr]) # NOTE: Make sure to list(set()) or duplicates may arise # NOTE: Keep the original order e.g. to read DTSTART before RECURRENCE-ID for attr in list(OrderedDict.fromkeys(ical_event.singletons)): if ical_event.has_key(attr): if isinstance(ical_event[attr], list): ical_event[attr] = ical_event[attr][0]; self.set_from_ical(attr.lower(), ical_event[attr]) # NOTE: Make sure to list(set()) or duplicates may arise for attr in list(set(ical_event.multiple)): if ical_event.has_key(attr): self.set_from_ical(attr.lower(), ical_event[attr]) def _xml_from_ical(self, ical): if not "BEGIN:VCALENDAR" in ical: ical = "BEGIN:VCALENDAR\nVERSION:2.0\n" + ical + "\nEND:VCALENDAR" from kolab.calendaring import EventCal self.event = EventCal() success = self.event.fromICal(ical) if success: self._load_exceptions() return success def get_attendee_participant_status(self, attendee): return attendee.get_participant_status() def get_attendee(self, attendee): if isinstance(attendee, basestring): if attendee in [x.get_email() for x in self.get_attendees()]: attendee = self.get_attendee_by_email(attendee) elif attendee in [x.get_name() for x in self.get_attendees()]: attendee = self.get_attendee_by_name(attendee) else: raise ValueError(_("No attendee with email or name %r") %(attendee)) return attendee elif isinstance(attendee, Attendee): return attendee else: raise ValueError(_("Invalid argument value attendee %r, must be basestring or Attendee") % (attendee)) def find_attendee(self, attendee): try: return self.get_attendee(attendee) except: return None def get_attendee_by_email(self, email): if email in [x.get_email() for x in self.get_attendees()]: return [x for x in self.get_attendees() if x.get_email() == email][0] raise ValueError(_("No attendee with email %r") %(email)) def get_attendee_by_name(self, name): if name in [x.get_name() for x in self.get_attendees()]: return [x for x in self.get_attendees() if x.get_name() == name][0] raise ValueError(_("No attendee with name %r") %(name)) def get_attendees(self): return self._attendees def get_categories(self): return [str(c) for c in self.event.categories()] def get_classification(self): return self.event.classification() def get_created(self): try: return xmlutils.from_cdatetime(self.event.created(), True) except ValueError: return datetime.datetime.now() def get_description(self): return self.event.description() def get_comment(self): if hasattr(self.event, 'comment'): return self.event.comment() else: return None def get_duration(self): duration = self.event.duration() if duration and duration.isValid(): dtd = datetime.timedelta( days=duration.days(), seconds=duration.seconds(), minutes=duration.minutes(), hours=duration.hours(), weeks=duration.weeks() ) return dtd return None def get_end(self): dt = xmlutils.from_cdatetime(self.event.end(), True) if not dt: duration = self.get_duration() if duration is not None: dt = self.get_start() + duration return dt def get_date_text(self, date_format=None, time_format=None): if date_format is None: date_format = _("%Y-%m-%d") if time_format is None: time_format = _("%H:%M (%Z)") start = self.get_start() end = self.get_end() all_day = not hasattr(start, 'date') start_date = start.date() if not all_day else start end_date = end.date() if not all_day else end if start_date == end_date: end_format = time_format else: end_format = date_format + " " + time_format if all_day: time_format = '' if start_date == end_date: return start.strftime(date_format) return "%s - %s" % (start.strftime(date_format + " " + time_format), end.strftime(end_format)) def get_recurrence_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.recurrenceDates()) def get_exception_dates(self): return map(lambda _: xmlutils.from_cdatetime(_, True), self.event.exceptionDates()) def get_exceptions(self): return self._exceptions def has_exceptions(self): return len(self._exceptions) > 0 def get_attachments(self): return self.event.attachments() def get_attachment_data(self, i): vattach = self.event.attachments() if i < len(vattach): attachment = vattach[i] uri = attachment.uri() if uri and uri[0:4] == 'cid:': # get data from MIME part with matching content-id cid = '<' + uri[4:] + '>' for p in self._attachment_parts: if p['Content-ID'] == cid: return p.get_payload(decode=True) else: return attachment.data() return None def get_alarms(self): return self.event.alarms() def get_ical_attendee(self): # TODO: Formatting, aye? See also the example snippet: # # ATTENDEE;RSVP=TRUE;ROLE=REQ-PARTICIPANT;CUTYPE=GROUP: # MAILTO:employee-A@host.com attendees = [] for attendee in self.get_attendees(): email = attendee.get_email() name = attendee.get_name() rsvp = attendee.get_rsvp() role = attendee.get_role() partstat = attendee.get_participant_status() cutype = attendee.get_cutype() delegators = attendee.get_delegated_from() delegatees = attendee.get_delegated_to() - if rsvp in attendee.rsvp_map.keys(): + if rsvp in attendee.rsvp_map: _rsvp = rsvp elif rsvp in attendee.rsvp_map.values(): _rsvp = [k for k, v in attendee.rsvp_map.iteritems() if v == rsvp][0] else: _rsvp = None - if role in attendee.role_map.keys(): + if role in attendee.role_map: _role = role elif role in attendee.role_map.values(): _role = [k for k, v in attendee.role_map.iteritems() if v == role][0] else: _role = None - if partstat in attendee.participant_status_map.keys(): + if partstat in attendee.participant_status_map: _partstat = partstat elif partstat in attendee.participant_status_map.values(): _partstat = [k for k, v in attendee.participant_status_map.iteritems() if v == partstat][0] else: _partstat = None - if cutype in attendee.cutype_map.keys(): + if cutype in attendee.cutype_map: _cutype = cutype elif cutype in attendee.cutype_map.values(): _cutype = [k for k, v in attendee.cutype_map.iteritems() if v == cutype][0] else: _cutype = None _attendee = icalendar.vCalAddress("MAILTO:%s" % email) if not name == None and not name == "": _attendee.params['CN'] = icalendar.vText(name) if not _rsvp == None: _attendee.params['RSVP'] = icalendar.vText(_rsvp) if not _role == None: _attendee.params['ROLE'] = icalendar.vText(_role) if not _partstat == None: _attendee.params['PARTSTAT'] = icalendar.vText(_partstat) if not _cutype == None: _attendee.params['CUTYPE'] = icalendar.vText(_cutype) if not delegators == None and len(delegators) > 0: _attendee.params['DELEGATED-FROM'] = icalendar.vText(delegators[0].email()) if not delegatees == None and len(delegatees) > 0: _attendee.params['DELEGATED-TO'] = icalendar.vText(delegatees[0].email()) attendees.append(_attendee) return attendees def get_ical_attendee_participant_status(self, attendee): attendee = self.get_attendee(attendee) - if attendee.get_participant_status() in attendee.participant_status_map.keys(): + if attendee.get_participant_status() in attendee.participant_status_map: return attendee.get_participant_status() elif attendee.get_participant_status() in attendee.participant_status_map.values(): return [k for k, v in attendee.participant_status_map.iteritems() if v == attendee.get_participant_status()][0] else: raise ValueError(_("Invalid participant status")) def get_ical_created(self): return self.get_created() def get_ical_dtend(self): dtend = self.get_end() # shift end by one day on all-day events if not hasattr(dtend, 'hour'): dtend = dtend + datetime.timedelta(days=1) return dtend def get_ical_dtstamp(self): try: retval = self.get_lastmodified() if retval == None or retval == "": return datetime.datetime.now() except: return datetime.datetime.now() def get_ical_lastmodified(self): return self.get_ical_dtstamp() def get_ical_dtstart(self): return self.get_start() def get_ical_organizer(self): contact = self.get_organizer() organizer = icalendar.vCalAddress("MAILTO:%s" % contact.email()) name = contact.name() if not name == None and not name == "": organizer.params["CN"] = icalendar.vText(name) return organizer def get_ical_status(self): status = self.event.status() - if status in self.status_map.keys(): + 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.keys(): + if classification in self.classification_map: self.event.setClassification(self.classification_map[classification]) elif classification in self.classification_map.values(): self.event.setClassification(classification) else: raise ValueError(_("Invalid classification %r") % (classification)) def set_created(self, _datetime=None): if _datetime is None or isinstance(_datetime, datetime.time): _datetime = datetime.datetime.utcnow() self.event.setCreated(xmlutils.to_cdatetime(_datetime, False, True)) def set_description(self, description): self.event.setDescription(ustr(description)) def set_comment(self, comment): if hasattr(self.event, 'setComment'): self.event.setComment(ustr(comment)) def set_dtstamp(self, _datetime): self.event.setLastModified(xmlutils.to_cdatetime(_datetime, False, True)) def set_end(self, _datetime): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, make it UTC if _datetime.tzinfo == None: _datetime = _datetime.replace(tzinfo=pytz.utc) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event end needs datetime.date or datetime.datetime instance, got %r") % (type(_datetime))) self.event.setEnd(xmlutils.to_cdatetime(_datetime, True)) def set_exception_dates(self, _datetimes): for _datetime in _datetimes: self.add_exception_date(_datetime) def set_recurrence_dates(self, _datetimes): for _datetime in _datetimes: self.add_recurrence_date(_datetime) def add_custom_property(self, name, value): if not name.upper().startswith('X-'): raise ValueError(_("Invalid custom property name %r") % (name)) props = self.event.customProperties() props.append(kolabformat.CustomProperty(name.upper(), value)) self.event.setCustomProperties(props) def set_from_ical(self, attr, value): attr = attr.replace('-', '') ical_setter = 'set_ical_' + attr default_setter = 'set_' + attr params = value.params if hasattr(value, 'params') else {} if isinstance(value, icalendar.vDDDTypes) and hasattr(value, 'dt'): value = value.dt if attr == "categories": self.add_category(value) elif attr == "class": if (value and value[:2] not in ['X-', 'x-']): self.set_classification(value) elif attr == "recurrenceid": self.set_ical_recurrenceid(value, params) elif hasattr(self, ical_setter): getattr(self, ical_setter)(value) elif hasattr(self, default_setter): getattr(self, default_setter)(value) def set_ical_attendee(self, _attendee): if isinstance(_attendee, basestring): _attendee = [_attendee] if isinstance(_attendee, list): for attendee in _attendee: address = str(attendee).split(':')[-1] if hasattr(attendee, 'params'): params = attendee.params else: params = {} if params.has_key('CN'): name = ustr(params['CN']) else: name = None if params.has_key('ROLE'): role = params['ROLE'] else: role = None if params.has_key('PARTSTAT'): partstat = params['PARTSTAT'] else: partstat = None if params.has_key('RSVP'): rsvp = params['RSVP'] else: rsvp = None if params.has_key('CUTYPE'): cutype = params['CUTYPE'] else: cutype = kolabformat.CutypeIndividual att = self.add_attendee(address, name=name, rsvp=rsvp, role=role, participant_status=partstat, cutype=cutype, params=params) def set_ical_dtend(self, dtend): # shift end by one day on all-day events if not hasattr(dtend, 'hour'): dtend = dtend - datetime.timedelta(days=1) self.set_end(dtend) def set_ical_dtstamp(self, dtstamp): self.set_dtstamp(dtstamp) def set_ical_dtstart(self, dtstart): self.set_start(dtstart) def set_ical_lastmodified(self, lastmod): self.set_lastmodified(lastmod) def set_ical_duration(self, value): if hasattr(value, 'dt'): value = value.dt duration = kolabformat.Duration(value.days, 0, 0, value.seconds, False) self.event.setDuration(duration) def set_ical_organizer(self, organizer): address = str(organizer).split(':')[-1] cn = None if hasattr(organizer, 'params'): params = organizer.params else: params = {} if params.has_key('CN'): 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.keys(): + if status in self.status_map: self.event.setStatus(self.status_map[status]) elif status in self.status_map.values(): self.event.setStatus(status) elif not status == kolabformat.StatusUndefined: raise InvalidEventStatusError(_("Invalid status set: %r") % (status)) def set_summary(self, summary): self.event.setSummary(summary) def set_uid(self, uid): self.uid = uid self.event.setUid(str(uid)) def set_recurrence_id(self, _datetime, _thisandfuture=None): valid_datetime = False if isinstance(_datetime, datetime.date): valid_datetime = True if isinstance(_datetime, datetime.datetime): # If no timezone information is passed on, use the one from event start if _datetime.tzinfo == None: _start = self.get_start() _datetime = _datetime.replace(tzinfo=_start.tzinfo) valid_datetime = True if not valid_datetime: raise InvalidEventDateError(_("Event recurrence-id needs datetime.datetime instance")) if _thisandfuture is None: _thisandfuture = self.thisandfuture self.event.setRecurrenceID(xmlutils.to_cdatetime(_datetime), _thisandfuture) def set_transparency(self, transp): return self.event.setTransparency(transp) def __str__(self): event_xml = kolabformat.writeEvent(self.event) error = kolabformat.error() if error == None or not error: return event_xml else: raise EventIntegrityError(kolabformat.errorMessage()) def to_dict(self): data = dict() for p, getter in self.properties_map.iteritems(): val = None if hasattr(self, getter): val = getattr(self, getter)() elif hasattr(self.event, getter): val = getattr(self.event, getter)() if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectordatetime): val = [xmlutils.from_cdatetime(x, True) for x in val] elif isinstance(val, kolabformat.vectors): val = [str(x) for x in val] elif isinstance(val, kolabformat.vectorcs): for x in val: data[x.identifier] = x.value val = None elif isinstance(val, kolabformat.ContactReference): val = ContactReference(val).to_dict() elif isinstance(val, kolabformat.RecurrenceRule): val = RecurrenceRule(val).to_dict() elif isinstance(val, kolabformat.vectorattachment): val = [dict(fmttype=x.mimetype(), label=x.label(), uri=x.uri()) for x in val] elif isinstance(val, kolabformat.vectoralarm): val = [self._alarm_to_dict(x) for x in val] elif isinstance(val, list): val = [x.to_dict() for x in val if hasattr(x, 'to_dict')] if val is not None: data[p] = val return data def _alarm_to_dict(self, alarm): ret = dict( action=self._translate_value(alarm.type(), self.alarm_type_map), summary=alarm.summary(), description=alarm.description(), trigger=None ) start = alarm.start() if start and start.isValid(): ret['trigger'] = xmlutils.from_cdatetime(start, True) else: ret['trigger'] = dict(related=self._translate_value(alarm.relativeTo(), self.related_map)) duration = alarm.relativeStart() if duration and duration.isValid(): prefix = '-' if duration.isNegative() else '+' value = prefix + "P%dW%dDT%dH%dM%dS" % ( duration.weeks(), duration.days(), duration.hours(), duration.minutes(), duration.seconds() ) ret['trigger']['value'] = re.sub(r"T$", '', re.sub(r"0[WDHMS]", '', value)) if alarm.type() == kolabformat.Alarm.EMailAlarm: ret['attendee'] = [ContactReference(a).to_dict() for a in alarm.attendees()] return ret def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) return name_map[val] if name_map.has_key(val) else 'UNKNOWN' def to_message(self, creator=None): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate msg = MIMEMultipart() organizer = self.get_organizer() email = organizer.email() name = organizer.name() if creator: msg['From'] = creator elif not name: msg['From'] = email else: msg['From'] = '"%s" <%s>' % (name, email) msg['To'] = ', '.join([x.__str__() for x in self.get_attendees()]) msg['Date'] = formatdate(localtime=True) msg.add_header('X-Kolab-MIME-Version', '3.0') msg.add_header('X-Kolab-Type', 'application/x-vnd.kolab.' + self.type) text = utils.multiline_message(""" This is a Kolab Groupware object. To view this object you will need an email client that understands the Kolab Groupware format. For a list of such email clients please visit http://www.kolab.org/ """) msg.attach( MIMEText(text) ) part = MIMEBase('application', "calendar+xml") part.set_charset('UTF-8') msg["Subject"] = self.get_uid() # extract attachment data into separate MIME parts vattach = self.event.attachments() i = 0 for attach in vattach: if attach.uri(): continue mimetype = attach.mimetype() (primary, seconday) = mimetype.split('/') name = attach.label() if not name: name = 'unknown.x' (basename, suffix) = path.splitext(name) t = datetime.datetime.now() cid = "%s.%s.%s%s" % (basename, time.mktime(t.timetuple()), t.microsecond + len(self._attachment_parts), suffix) p = MIMEBase(primary, seconday) p.add_header('Content-Disposition', 'attachment', filename=name) p.add_header('Content-Transfer-Encoding', 'base64') p.add_header('Content-ID', '<' + cid + '>') p.set_payload(base64.b64encode(attach.data())) self._attachment_parts.append(p) # modify attachment object attach.setData('', mimetype) attach.setUri('cid:' + cid, mimetype) vattach[i] = attach i += 1 self.event.setAttachments(vattach) part.set_payload(str(self)) part.add_header('Content-Disposition', 'attachment; filename="kolab.xml"') part.replace_header('Content-Transfer-Encoding', '8bit') msg.attach(part) # append attachment parts for p in self._attachment_parts: msg.attach(p) return msg def to_message_itip(self, from_address, method="REQUEST", participant_status="ACCEPTED", subject=None, message_text=None): from email.MIMEMultipart import MIMEMultipart from email.MIMEBase import MIMEBase from email.MIMEText import MIMEText from email.Utils import COMMASPACE, formatdate # encode unicode strings with quoted-printable from email import charset charset.add_charset('utf-8', charset.SHORTEST, charset.QP) msg = MIMEMultipart("alternative") msg_from = None attendees = None if method == "REPLY": # TODO: Make user friendly name msg['To'] = self.get_organizer().email() attendees = self.get_attendees() reply_attendees = [] # There's an exception here for delegation (partstat DELEGATED) for attendee in attendees: if attendee.get_email() == from_address: # Only the attendee is supposed to be listed in a reply attendee.set_participant_status(participant_status) attendee.set_rsvp(False) reply_attendees.append(attendee) name = attendee.get_name() email = attendee.get_email() if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) elif from_address in attendee.get_delegated_from(True): reply_attendees.append(attendee) # keep only replying (and delegated) attendee(s) self._attendees = reply_attendees self.event.setAttendees(self._attendees) if msg_from == None: organizer = self.get_organizer() email = organizer.email() name = organizer.name() if email == from_address: if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) elif method == "REQUEST": organizer = self.get_organizer() email = organizer.email() name = organizer.name() if not name: msg_from = email else: msg_from = '"%s" <%s>' % (name, email) if msg_from == None: if from_address == None: log.error(_("No sender specified")) else: msg_from = from_address msg['From'] = utils.str2unicode(msg_from) msg['Date'] = formatdate(localtime=True) if subject is None: subject = _("Invitation for %s was %s") % (self.get_summary(), participant_status_label(participant_status)) msg['Subject'] = utils.str2unicode(subject) if message_text is None: message_text = _("""This is an automated response to one of your event requests.""") msg.attach(MIMEText(utils.stripped_message(message_text), _charset='utf-8')) part = MIMEBase('text', 'calendar', charset='UTF-8', method=method) del part['MIME-Version'] # mime parts don't need this part.set_payload(self.as_string_itip(method=method)) part.add_header('Content-Transfer-Encoding', '8bit') msg.attach(part) # restore the original list of attendees # attendees being reduced to the replying attendee above if attendees is not None: self._attendees = attendees self.event.setAttendees(self._attendees) return msg def is_recurring(self): return self.event.recurrenceRule().isValid() or len(self.get_recurrence_dates()) > 0 def to_event_cal(self): from kolab.calendaring import EventCal return EventCal(self.event) def get_next_occurence(self, _datetime): if not hasattr(self, 'eventcal'): self.eventcal = self.to_event_cal() next_cdatetime = self.eventcal.getNextOccurence(xmlutils.to_cdatetime(_datetime, True)) next_datetime = xmlutils.from_cdatetime(next_cdatetime, True) if next_cdatetime is not None else None # cut infinite recurrence at a reasonable point if next_datetime and not self.get_last_occurrence() and next_datetime > xmlutils.to_dt(self._recurrence_end()): next_datetime = None # next_datetime is always a cdatetime, convert to date if necessary if next_datetime and not isinstance(self.get_start(), datetime.datetime): next_datetime = datetime.date(next_datetime.year, next_datetime.month, next_datetime.day) return next_datetime def get_occurence_end_date(self, datetime): if not datetime: return None if not hasattr(self, 'eventcal'): return None end_cdatetime = self.eventcal.getOccurenceEndDate(xmlutils.to_cdatetime(datetime, True)) return xmlutils.from_cdatetime(end_cdatetime, True) if end_cdatetime is not None else None def get_last_occurrence(self, force=False): if not hasattr(self, 'eventcal'): self.eventcal = self.to_event_cal() last = self.eventcal.getLastOccurrence() last_datetime = xmlutils.from_cdatetime(last, True) if last is not None else None # we're forced to return some date if last_datetime is None and force: last_datetime = self._recurrence_end() return last_datetime def get_next_instance(self, datetime): next_start = self.get_next_occurence(datetime) if next_start: instance = Event(from_string=str(self)) instance.set_start(next_start) instance.event.setRecurrenceID(xmlutils.to_cdatetime(next_start), False) next_end = self.get_occurence_end_date(next_start) if next_end: instance.set_end(next_end) # unset recurrence rule and exceptions instance.set_recurrence(kolabformat.RecurrenceRule()) instance.event.setExceptions(kolabformat.vectorevent()) instance.event.setExceptionDates(kolabformat.vectordatetime()) instance._exceptions = [] instance._isexception = False # unset attachments list (only stored in main event) instance.event.setAttachments(kolabformat.vectorattachment()) # copy data from matching exception # (give precedence to single occurrence exceptions over thisandfuture) for exception in self._exceptions: recurrence_id = exception.get_recurrence_id() if recurrence_id == next_start and (not exception.thisandfuture or not instance._isexception): instance = exception instance._isexception = True if not exception.thisandfuture: break elif exception.thisandfuture and next_start > recurrence_id: # TODO: merge exception properties over this instance + adjust start/end with the according offset pass return instance return None def get_instance(self, _datetime): # If no timezone information is given, use the one from event start if isinstance(_datetime, datetime.datetime) and _datetime.tzinfo == None: _start = self.get_start() if hasattr(_start, 'tzinfo'): _datetime = _datetime.replace(tzinfo=_start.tzinfo) if self.is_recurring(): instance = self.get_next_instance(_datetime - datetime.timedelta(days=1)) while instance: recurrence_id = instance.get_recurrence_id() if type(recurrence_id) == type(_datetime) and recurrence_id <= _datetime: if xmlutils.dates_equal(recurrence_id, _datetime): return instance instance = self.get_next_instance(instance.get_start()) else: break elif self.has_exceptions(): for exception in self._exceptions: recurrence_id = exception.get_recurrence_id() if type(recurrence_id) == type(_datetime) and xmlutils.dates_equal(recurrence_id, _datetime): return exception if self.get_recurrence_id(): recurrence_id = self.get_recurrence_id() if type(recurrence_id) == type(_datetime) and xmlutils.dates_equal(recurrence_id, _datetime): return self return None def _recurrence_end(self): """ Determine a reasonable end date for infinitely recurring events """ rrule = self.event.recurrenceRule() if rrule.isValid() and rrule.count() < 0 and not rrule.end().isValid(): now = datetime.datetime.now() switch = { kolabformat.RecurrenceRule.Yearly: 100, kolabformat.RecurrenceRule.Monthly: 20 } intvl = switch[rrule.frequency()] if rrule.frequency() in switch else 10 return self.get_start().replace(year=now.year + intvl) return xmlutils.from_cdatetime(rrule.end(), True) class EventIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidEventDateError(Exception): def __init__(self, message): Exception.__init__(self, message) class InvalidEventStatusError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/note.py b/pykolab/xml/note.py index 5784ebb..030b1df 100644 --- a/pykolab/xml/note.py +++ b/pykolab/xml/note.py @@ -1,138 +1,138 @@ import pytz import datetime import kolabformat from pykolab.translate import _ from pykolab.xml import utils as xmlutils from pykolab.xml.utils import ustr def note_from_string(string): _xml = kolabformat.readNote(string, False) return Note(_xml) def note_from_message(message): note = None if message.is_multipart(): for part in message.walk(): if part.get_content_type() == "application/vnd.kolab+xml": payload = part.get_payload(decode=True) note = note_from_string(payload) # append attachment parts to Note object elif note and part.has_key('Content-ID'): note._attachment_parts.append(part) return note class Note(kolabformat.Note): type = 'note' classification_map = { 'PUBLIC': kolabformat.ClassPublic, 'PRIVATE': kolabformat.ClassPrivate, 'CONFIDENTIAL': kolabformat.ClassConfidential, } properties_map = { 'uid': 'get_uid', 'summary': 'summary', 'description': 'description', 'created': 'get_created', 'lastmodified-date': 'get_lastmodified', 'classification': 'get_classification', 'categories': 'categories', 'color': 'color', } def __init__(self, *args, **kw): self._attachment_parts = [] kolabformat.Note.__init__(self, *args, **kw) def get_uid(self): uid = self.uid() if not uid == '': return uid else: self.__str__() return kolabformat.getSerializedUID() def get_created(self): try: return xmlutils.from_cdatetime(self.created(), True) except ValueError: return datetime.datetime.now() def get_lastmodified(self): try: _datetime = self.lastModified() if _datetime == None or not _datetime.isValid(): self.__str__() except: return datetime.datetime.now(pytz.utc) return xmlutils.from_cdatetime(self.lastModified(), True) def set_summary(self, summary): self.setSummary(ustr(summary)) def set_description(self, description): self.setDescription(ustr(description)) def get_classification(self, translated=True): _class = self.classification() if translated: return self._translate_value(_class, self.classification_map) return _class def set_classification(self, classification): - if classification in self.classification_map.keys(): + if classification in self.classification_map: self.setClassification(self.classification_map[classification]) elif classification in self.classification_map.values(): self.setClassification(status) else: raise ValueError(_("Invalid classification %r") % (classification)) def add_category(self, category): _categories = self.categories() _categories.append(ustr(category)) self.setCategories(_categories) def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) return name_map[val] if name_map.has_key(val) else 'UNKNOWN' def to_dict(self): if not self.isValid(): return None data = dict() for p, getter in self.properties_map.iteritems(): val = None if hasattr(self, getter): val = getattr(self, getter)() if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectori): val = [int(x) for x in val] elif isinstance(val, kolabformat.vectors): val = [str(x) for x in val] if val is not None: data[p] = val return data def __str__(self): xml = kolabformat.writeNote(self) error = kolabformat.error() if error == None or not error: return xml else: raise NoteIntegrityError(kolabformat.errorMessage()) class NoteIntegrityError(Exception): def __init__(self, message): Exception.__init__(self, message) diff --git a/pykolab/xml/recurrence_rule.py b/pykolab/xml/recurrence_rule.py index 6f6c7d8..05d651c 100644 --- a/pykolab/xml/recurrence_rule.py +++ b/pykolab/xml/recurrence_rule.py @@ -1,219 +1,219 @@ import pytz import icalendar import datetime import kolabformat from pykolab.xml import utils as xmlutils from pykolab.translate import _ from pykolab.translate import N_ """ def setFrequency(self, *args): return _kolabformat.RecurrenceRule_setFrequency(self, *args) def frequency(self): return _kolabformat.RecurrenceRule_frequency(self) def setWeekStart(self, *args): return _kolabformat.RecurrenceRule_setWeekStart(self, *args) def weekStart(self): return _kolabformat.RecurrenceRule_weekStart(self) def setEnd(self, *args): return _kolabformat.RecurrenceRule_setEnd(self, *args) def end(self): return _kolabformat.RecurrenceRule_end(self) def setCount(self, *args): return _kolabformat.RecurrenceRule_setCount(self, *args) def count(self): return _kolabformat.RecurrenceRule_count(self) def setInterval(self, *args): return _kolabformat.RecurrenceRule_setInterval(self, *args) def interval(self): return _kolabformat.RecurrenceRule_interval(self) def setBysecond(self, *args): return _kolabformat.RecurrenceRule_setBysecond(self, *args) def bysecond(self): return _kolabformat.RecurrenceRule_bysecond(self) def setByminute(self, *args): return _kolabformat.RecurrenceRule_setByminute(self, *args) def byminute(self): return _kolabformat.RecurrenceRule_byminute(self) def setByhour(self, *args): return _kolabformat.RecurrenceRule_setByhour(self, *args) def byhour(self): return _kolabformat.RecurrenceRule_byhour(self) def setByday(self, *args): return _kolabformat.RecurrenceRule_setByday(self, *args) def byday(self): return _kolabformat.RecurrenceRule_byday(self) def setBymonthday(self, *args): return _kolabformat.RecurrenceRule_setBymonthday(self, *args) def bymonthday(self): return _kolabformat.RecurrenceRule_bymonthday(self) def setByyearday(self, *args): return _kolabformat.RecurrenceRule_setByyearday(self, *args) def byyearday(self): return _kolabformat.RecurrenceRule_byyearday(self) def setByweekno(self, *args): return _kolabformat.RecurrenceRule_setByweekno(self, *args) def byweekno(self): return _kolabformat.RecurrenceRule_byweekno(self) def setBymonth(self, *args): return _kolabformat.RecurrenceRule_setBymonth(self, *args) def bymonth(self): return _kolabformat.RecurrenceRule_bymonth(self) def isValid(self): return _kolabformat.RecurrenceRule_isValid(self) """ frequency_labels = { "YEARLY": N_("Every %d year(s)"), "MONTHLY": N_("Every %d month(s)"), "WEEKLY": N_("Every %d week(s)"), "DAILY": N_("Every %d day(s)"), "HOURLY": N_("Every %d hours"), "MINUTELY": N_("Every %d minutes"), "SECONDLY": N_("Every %d seconds") } def frequency_label(freq): return _(frequency_labels[freq]) if frequency_labels.has_key(freq) else _(freq) class RecurrenceRule(kolabformat.RecurrenceRule): frequency_map = { None: kolabformat.RecurrenceRule.FreqNone, "YEARLY": kolabformat.RecurrenceRule.Yearly, "MONTHLY": kolabformat.RecurrenceRule.Monthly, "WEEKLY": kolabformat.RecurrenceRule.Weekly, "DAILY": kolabformat.RecurrenceRule.Daily, "HOURLY": kolabformat.RecurrenceRule.Hourly, "MINUTELY": kolabformat.RecurrenceRule.Minutely, "SECONDLY": kolabformat.RecurrenceRule.Secondly } weekday_map = { "MO": kolabformat.Monday, "TU": kolabformat.Tuesday, "WE": kolabformat.Wednesday, "TH": kolabformat.Thursday, "FR": kolabformat.Friday, "SA": kolabformat.Saturday, "SU": kolabformat.Sunday } properties_map = { 'freq': 'get_frequency', 'interval': 'interval', 'count': 'count', 'until': 'end', 'bymonth': 'bymonth', 'byday': 'byday', 'bymonthday':'bymonthday', 'byyearday': 'byyearday', 'byweekno': 'byweekno', 'byhour': 'byhour', 'byminute': 'byminute', 'wkst': 'get_weekstart' } def __init__(self, rrule=None): if rrule == None: kolabformat.RecurrenceRule.__init__(self) else: kolabformat.RecurrenceRule.__init__(self, rrule) def from_ical(self, vrecur): vectorimap = { 'BYSECOND': 'setBysecond', 'BYMINUTE': 'setByminute', 'BYHOUR': 'setByhour', 'BYMONTHDAY': 'setBymonthday', 'BYYEARDAY': 'setByyearday', 'BYMONTH': 'setBymonth', } settermap = { 'FREQ': 'set_frequency', 'INTERVAL': 'set_interval', 'COUNT': 'set_count', 'UNTIL': 'set_until', 'WKST': 'set_weekstart', 'BYDAY': 'set_byday', } for prop,setter in vectorimap.items(): if vrecur.has_key(prop): getattr(self, setter)([int(v) for v in vrecur[prop]]) for prop,setter in settermap.items(): if vrecur.has_key(prop): getattr(self, setter)(vrecur[prop]) def set_count(self, count): if isinstance(count, list): count = count[0] self.setCount(int(count)) def set_interval(self, val): if isinstance(val, list): val = val[0] self.setInterval(int(val)) def set_frequency(self, freq): self._set_map_value(freq, self.frequency_map, 'setFrequency') def get_frequency(self, translated=False): freq = self.frequency() if translated: return self._translate_value(freq, self.frequency_map) return freq def set_byday(self, bdays): daypos = kolabformat.vectordaypos() for wday in bdays: if isinstance(wday, str): wday = icalendar.vWeekday(wday) weekday = str(wday)[-2:] occurrence = int(wday.relative) if str(wday)[0] == '-': occurrence = occurrence * -1 if self.weekday_map.has_key(weekday): daypos.append(kolabformat.DayPos(occurrence, self.weekday_map[weekday])) self.setByday(daypos) def set_weekstart(self, wkst): self._set_map_value(wkst, self.weekday_map, 'setWeekStart') def get_weekstart(self, translated=False): wkst = self.weekStart() if translated: return self._translate_value(wkst, self.weekday_map) return wkst def set_until(self, until): if isinstance(until, list): until = until[0] if isinstance(until, datetime.datetime) or isinstance(until, datetime.date): # move into UTC timezone according to RFC 5545 if isinstance(until, datetime.datetime): until = until.astimezone(pytz.utc) self.setEnd(xmlutils.to_cdatetime(until, True)) def _set_map_value(self, val, pmap, setter): if isinstance(val, list): val = val[0] - if val in pmap.keys(): + if val in pmap: getattr(self, setter)(pmap[val]) elif val in pmap.values(): getattr(self, setter)(val) def _translate_value(self, val, map): name_map = dict([(v, k) for (k, v) in map.iteritems()]) return name_map[val] if name_map.has_key(val) else 'UNKNOWN' def to_ical(self): rrule = icalendar.vRecur(dict((k,v) for k,v in self.to_dict(True).items() if not (type(v) == str and v == '' or type(v) == list and len(v) == 0))) return rrule def to_dict(self, raw=False): if not self.isValid() or self.frequency() == kolabformat.RecurrenceRule.FreqNone: return None data = dict() for p, getter in self.properties_map.iteritems(): val = None args = {} if hasattr(self, getter): if getter.startswith('get_'): args = dict(translated=True) if hasattr(self, getter): val = getattr(self, getter)(**args) if isinstance(val, kolabformat.cDateTime): val = xmlutils.from_cdatetime(val, True) elif isinstance(val, kolabformat.vectori): val = [int(x) for x in val] elif isinstance(val, kolabformat.vectordaypos): val = ["%s%s" % (str(x.occurence()) if x.occurence() != 0 else '', self._translate_value(x.weekday(), self.weekday_map)) for x in val] if not raw and isinstance(val, list): val = ",".join(val) if val is not None: data[p] = val return data diff --git a/pykolab/xml/utils.py b/pykolab/xml/utils.py index 535e95c..5a58bf5 100644 --- a/pykolab/xml/utils.py +++ b/pykolab/xml/utils.py @@ -1,369 +1,369 @@ import datetime import pytz import kolabformat from dateutil.tz import tzlocal from collections import OrderedDict from pykolab.translate import _ from pykolab.translate import N_ def to_dt(dt): """ Convert a naive date or datetime to a tz-aware datetime. """ if isinstance(dt, datetime.date) and not isinstance(dt, datetime.datetime) or dt is not None and not hasattr(dt, 'hour'): dt = datetime.datetime(dt.year, dt.month, dt.day, 0, 0, 0, 0, tzinfo=pytz.utc) elif isinstance(dt, datetime.datetime): if dt.tzinfo == None: return dt.replace(tzinfo=pytz.utc) return dt def from_cdatetime(_cdatetime, with_timezone=True): """ Convert from kolabformat.cDateTime to datetime.date(time) """ if not _cdatetime.isValid(): return None ( year, month, day, ) = ( _cdatetime.year(), _cdatetime.month(), _cdatetime.day(), ) if _cdatetime.hour() == None or _cdatetime.hour() < 0: return datetime.date(year, month, day) ( hour, minute, second ) = ( _cdatetime.hour(), _cdatetime.minute(), _cdatetime.second() ) if with_timezone: _timezone = _cdatetime.timezone() if _timezone == '' or _timezone == None: _dt = datetime.datetime(year, month, day, hour, minute, second, tzinfo=pytz.utc) else: try: # use pytz.timezone.localize() to correctly set DST in tzinfo according to the given date _tz = pytz.timezone(_timezone) _dt = _tz.localize(datetime.datetime(year, month, day, hour, minute, second)) except: # fall back to local time _dt = datetime.datetime(year, month, day, hour, minute, second) return _dt else: return datetime.datetime(year, month, day, hour, minute, second) def to_cdatetime(_datetime, with_timezone=True, as_utc=False): """ Convert a datetime.dateime object into a kolabformat.cDateTime instance """ # convert date into UTC timezone if as_utc and hasattr(_datetime, "tzinfo"): if _datetime.tzinfo is not None: _datetime = _datetime.astimezone(pytz.utc) else: datetime = _datetime.replace(tzinfo=pytz.utc) with_timezone = False # Sometimes we deal with dummy 00000000T000000 values from iCalendar # in such cases we end up with datetime.time objects if not hasattr(_datetime, 'year'): (year, month, day) = (1970, 1, 1) else: (year, month, day) = (_datetime.year, _datetime.month, _datetime.day) if hasattr(_datetime, 'hour'): (hour, minute, second) = (_datetime.hour, _datetime.minute, _datetime.second) _cdatetime = kolabformat.cDateTime(year, month, day, hour, minute, second) else: _cdatetime = kolabformat.cDateTime(year, month, day) if with_timezone and hasattr(_datetime, "tzinfo"): if _datetime.tzinfo.__str__() in ['UTC','GMT']: _cdatetime.setUTC(True) else: _cdatetime.setTimezone(_datetime.tzinfo.__str__()) if as_utc: _cdatetime.setUTC(True) return _cdatetime def dates_equal(a, b): date_format = '%Y%m%d' if isinstance(a, datetime.date) and isinstance(b, datetime.date) else '%Y%m%dT%H%M%S' return type(a) == type(b) and a.strftime(date_format) == b.strftime(date_format) def ustr(s): """ Force the given (unicode) string into UTF-8 encoding """ if not isinstance(s, unicode): for cs in ['utf-8','latin-1']: try: s = unicode(s, cs) break except: pass if isinstance(s, unicode): return s.encode('utf-8') return s property_labels = { "name": N_("Name"), "summary": N_("Summary"), "location": N_("Location"), "description": N_("Description"), "url": N_("URL"), "status": N_("Status"), "priority": N_("Priority"), "attendee": N_("Attendee"), "start": N_("Start"), "end": N_("End"), "due": N_("Due"), "rrule": N_("Repeat"), "exdate": N_("Repeat Exception"), "organizer": N_("Organizer"), "attach": N_("Attachment"), "alarm": N_("Alarm"), "classification": N_("Classification"), "percent-complete": N_("Progress") } def property_label(propname): """ Return a localized name for the given object property """ return _(property_labels[propname]) if property_labels.has_key(propname) else _(propname) def property_to_string(propname, value): """ Render a human readable string for the given object property """ date_format = _("%Y-%m-%d") time_format = _("%H:%M (%Z)") date_time_format = date_format + " " + time_format maxlen = 50 if isinstance(value, datetime.datetime): return value.strftime(date_time_format) elif isinstance(value, datetime.date): return value.strftime(date_format) elif isinstance(value, int): return str(value) elif isinstance(value, str): if len(value) > maxlen: return value[:maxlen].rsplit(' ', 1)[0] + '...' return value elif isinstance(value, object) and hasattr(value, 'to_dict'): value = value.to_dict() if isinstance(value, dict): if propname == 'attendee': from . import attendee name = value['name'] if value.has_key('name') and not value['name'] == '' else value['email'] return "%s, %s" % (name, attendee.participant_status_label(value['partstat'])) elif propname == 'organizer': return value['name'] if value.has_key('name') and not value['name'] == '' else value['email'] elif propname == 'rrule': from . import recurrence_rule rrule = recurrence_rule.frequency_label(value['freq']) % (value['interval']) if value.has_key('count') and value['count'] > 0: rrule += " " + _("for %d times") % (value['count']) elif value.has_key('until') and (isinstance(value['until'], datetime.datetime) or isinstance(value['until'], datetime.date)): rrule += " " + _("until %s") % (value['until'].strftime(date_format)) return rrule elif propname == 'alarm': alarm_type_labels = { 'DISPLAY': _("Display message"), 'EMAIL': _("Send email"), 'AUDIO': _("Play sound") } alarm = alarm_type_labels.get(value['action'], "") if isinstance(value['trigger'], datetime.datetime): alarm += " @ " + property_to_string('trigger', value['trigger']) else: rel = _("%s after") if value['trigger']['related'] == 'END' else _("%s before") offsets = [] try: from icalendar import vDuration duration = vDuration.from_ical(value['trigger']['value'].strip('-')) except: return None if duration.days: offsets.append(_("%d day(s)") % (duration.days)) if duration.seconds: hours = duration.seconds // 3600 minutes = duration.seconds % 3600 // 60 seconds = duration.seconds % 60 if hours: offsets.append(_("%d hour(s)") % (hours)) if minutes or (hours and seconds): offsets.append(_("%d minute(s)") % (minutes)) if len(offsets): alarm += " " + rel % (", ".join(offsets)) return alarm elif propname == 'attach': return value['label'] if value.has_key('label') else value['fmttype'] return None def compute_diff(a, b, reduced=False): """ List the differences between two given dicts """ diff = [] properties = a.keys() - properties.extend([x for x in b.keys() if x not in properties]) + properties.extend([x for x in b if x not in properties]) for prop in properties: aa = a[prop] if a.has_key(prop) else None bb = b[prop] if b.has_key(prop) else None # compare two lists if isinstance(aa, list) or isinstance(bb, list): if not isinstance(aa, list): aa = [aa] if not isinstance(bb, list): bb = [bb] (aa, bb) = order_proplists(aa, bb) index = 0 length = max(len(aa), len(bb)) while index < length: aai = aa[index] if index < len(aa) else None bbi = bb[index] if index < len(bb) else None if not compare_values(aai, bbi): (old, new) = reduce_properties(aai, bbi) if reduced else (aai, bbi) diff.append(OrderedDict([('property', prop), ('index', index), ('old', old), ('new', new)])) index += 1 # the two properties differ elif not compare_values(aa, bb): if reduced: (old, new) = reduce_properties(aa, bb) else: (old, new) = (aa, bb) diff.append(OrderedDict([('property', prop), ('old', old), ('new', new)])) return diff def order_proplists(a, b): """ Orders two lists so that equal entries have the same position """ # nothing to be done here if len(a) == 0 and len(b) == 0: return (a, b) base = a comp = b flip = False if len(a) > len(b): flip = True base = b comp = a indices = [] top = len(comp) + 1 for bb in comp: index = None # find a matching entry in base for j, aa in enumerate(base): if compare_values(aa, bb, True): index = j break # move non-matching items to the end of the list if index is None: index = top top += 1 indices.append(index) # do sort by indices indices, comp = zip(*sorted(zip(indices, comp), key=lambda x: x[0])) return (comp, base) if flip else (base, comp) def compare_values(aa, bb, partial=False): ignore_keys = ['rsvp'] if not aa.__class__ == bb.__class__: return False if isinstance(aa, dict) and isinstance(bb, dict): aa = dict(aa) bb = dict(bb) # ignore some properties for comparison for k in ignore_keys: aa.pop(k, None) bb.pop(k, None) # accept partial match if partial: for k,v in aa.iteritems(): if bb.has_key(k) and bb[k] == v: return True return False return aa == bb def reduce_properties(aa, bb): """ Compares two given structs and removes equal values in bb """ if not isinstance(aa, dict) or not isinstance(bb, dict): return (aa, bb) properties = aa.keys() - properties.extend([x for x in bb.keys() if x not in properties]) + properties.extend([x for x in bb if x not in properties]) for prop in properties: if not aa.has_key(prop) or not bb.has_key(prop): continue if isinstance(aa[prop], dict) and isinstance(bb[prop], dict): (aa[prop], bb[prop]) = reduce_properties(aa[prop], bb[prop]) if aa[prop] == bb[prop]: # del aa[prop] del bb[prop] return (aa, bb) diff --git a/tests/functional/resource_func.py b/tests/functional/resource_func.py index 36bd1bd..771d698 100644 --- a/tests/functional/resource_func.py +++ b/tests/functional/resource_func.py @@ -1,75 +1,75 @@ import pykolab from pykolab import wap_client conf = pykolab.getConf() def resource_add(type, cn, members=None, owner=None, **kw): if type is None or type == '': raise Exception if cn is None or cn == '': raise Exception resource_details = { 'cn': cn, 'kolabtargetfolder': "shared/Resources/" + cn + "@example.org", 'uniquemember': members, 'owner': owner, 'ou': 'ou=resources,dc=example,dc=org' } resource_details.update(kw) bind_dn = conf.get('ldap', 'bind_dn') bind_pw = conf.get('ldap', 'bind_pw') domain = conf.get('kolab', 'primary_domain') result = wap_client.authenticate(bind_dn, bind_pw, domain) type_id = 0 resource_types = wap_client.resource_types_list() - for key in resource_types['list'].keys(): + for key in resource_types['list']: if resource_types['list'][key]['key'] == type: type_id = key if type_id == 0: raise Exception resource_type_info = resource_types['list'][type_id]['attributes'] params = {} - for attribute in resource_type_info['form_fields'].keys(): + for attribute in resource_type_info['form_fields']: attr_details = resource_type_info['form_fields'][attribute] if isinstance(attr_details, dict): if 'optional' not in attr_details or attr_details['optional'] is False or attribute in resource_details: params[attribute] = resource_details[attribute] elif isinstance(attr_details, list): params[attribute] = resource_details[attribute] fvg_params = params fvg_params['object_type'] = 'resource' fvg_params['type_id'] = type_id - fvg_params['attributes'] = [attr for attr in resource_type_info['auto_form_fields'].keys() if attr not in params] + fvg_params['attributes'] = [attr for attr in resource_type_info['auto_form_fields'] if attr not in params] result = wap_client.resource_add(params) result['dn'] = "cn=" + result['cn'] + ",ou=Resources,dc=example,dc=org" return result def purge_resources(): bind_dn = conf.get('ldap', 'bind_dn') bind_pw = conf.get('ldap', 'bind_pw') domain = conf.get('kolab', 'primary_domain') result = wap_client.authenticate(bind_dn, bind_pw, domain) resources = wap_client.resources_list() for resource in resources['list']: wap_client.resource_delete({'id': resource}) # from tests.functional.purge_imap import purge_imap # purge_imap() diff --git a/tests/functional/test_kolabd/test_001_user_sync.py b/tests/functional/test_kolabd/test_001_user_sync.py index 8b61893..cf8d443 100644 --- a/tests/functional/test_kolabd/test_001_user_sync.py +++ b/tests/functional/test_kolabd/test_001_user_sync.py @@ -1,140 +1,140 @@ import time import unittest import pykolab from pykolab import wap_client from pykolab.auth import Auth from pykolab.imap import IMAP conf = pykolab.getConf() class TestKolabDaemon(unittest.TestCase): @classmethod def setup_class(self, *args, **kw): from tests.functional.purge_users import purge_users purge_users() self.user = { 'local': 'john.doe', 'domain': 'example.org' } from tests.functional.user_add import user_add user_add("John", "Doe") @classmethod def teardown_class(self, *args, **kw): from tests.functional.purge_users import purge_users purge_users() def test_001_user_recipient_policy(self): auth = Auth() auth.connect() recipient = auth.find_recipient("%(local)s@%(domain)s" % (self.user)) if hasattr(self, 'assertIsInstance'): self.assertIsInstance(recipient, str) self.assertEqual(recipient, "uid=doe,ou=People,dc=example,dc=org") result = wap_client.user_info(recipient) self.assertEqual(result['mail'], 'john.doe@example.org') self.assertEqual(result['alias'], ['doe@example.org', 'j.doe@example.org']) def test_002_user_recipient_policy_duplicate(self): from tests.functional.user_add import user_add user = { 'local': 'jane.doe', 'domain': 'example.org' } user_add("Jane", "Doe") time.sleep(3) auth = Auth() auth.connect() recipient = auth.find_recipient("%(local)s@%(domain)s" % (user)) if hasattr(self, 'assertIsInstance'): self.assertIsInstance(recipient, str) self.assertEqual(recipient, "uid=doe2,ou=People,dc=example,dc=org") result = wap_client.user_info(recipient) if 'mailhost' not in result: from tests.functional.synchronize import synchronize_once synchronize_once() result = wap_client.user_info(recipient) self.assertEqual(result['mail'], 'jane.doe@example.org') self.assertEqual(result['alias'], ['doe2@example.org', 'j.doe2@example.org']) def test_003_user_mailbox_created(self): time.sleep(2) imap = IMAP() imap.connect() folders = imap.lm('user/%(local)s@%(domain)s' % (self.user)) self.assertEqual(len(folders), 1) def test_004_user_additional_folders_created(self): time.sleep(2) imap = IMAP() imap.connect() ac_folders = conf.get_raw('kolab', 'autocreate_folders') exec("ac_folders = %s" % (ac_folders)) folders = imap.lm('user/%(local)s/*@%(domain)s' % (self.user)) - self.assertEqual(len(folders), len(ac_folders.keys())) + self.assertEqual(len(folders), len(ac_folders)) def test_005_user_folders_metadata_set(self): imap = IMAP() imap.connect() ac_folders = conf.get_raw('kolab', 'autocreate_folders') exec("ac_folders = %s" % (ac_folders)) folders = [] folders.extend(imap.lm('user/%(local)s@%(domain)s' % (self.user))) folders.extend(imap.lm('user/%(local)s/*@%(domain)s' % (self.user))) for folder in folders: metadata = imap.get_metadata(folder) print(metadata) folder_name = '/'.join(folder.split('/')[2:]).split('@')[0] if folder_name in ac_folders: if 'annotations' in ac_folders[folder_name]: - for _annotation in ac_folders[folder_name]['annotations'].keys(): + for _annotation in ac_folders[folder_name]['annotations']: if _annotation.startswith('/private'): continue _annotation_value = ac_folders[folder_name]['annotations'][_annotation] self.assertTrue(_annotation in metadata[metadata.keys().pop()]) self.assertEqual(_annotation_value, metadata[metadata.keys().pop()][_annotation]) def test_006_user_subscriptions(self): imap = IMAP() imap.connect(login=False) login = conf.get('cyrus-imap', 'admin_login') password = conf.get('cyrus-imap', 'admin_password') imap.login_plain(login, password, 'john.doe@example.org') folders = imap.lm() self.assertTrue("INBOX" in folders) folders = imap.imap.lsub() self.assertTrue("Calendar" in folders) def test_011_resource_add(self): pass def test_012_resource_mailbox_created(self): pass def test_013_resource_mailbox_annotation(self): pass diff --git a/tests/functional/test_wap_client/test_002_user_add.py b/tests/functional/test_wap_client/test_002_user_add.py index b4d4752..a48b92b 100644 --- a/tests/functional/test_wap_client/test_002_user_add.py +++ b/tests/functional/test_wap_client/test_002_user_add.py @@ -1,78 +1,78 @@ import time import unittest import pykolab from pykolab import wap_client from pykolab.auth import Auth from pykolab.imap import IMAP conf = pykolab.getConf() class TestUserAdd(unittest.TestCase): @classmethod def setup_class(self, *args, **kw): from tests.functional.purge_users import purge_users purge_users() self.user = { 'local': 'john.doe', 'domain': 'example.org' } from tests.functional.user_add import user_add user_add("John", "Doe") from tests.functional.synchronize import synchronize_once synchronize_once() @classmethod def teardown_class(self, *args, **kw): from tests.functional.purge_users import purge_users purge_users() def test_001_inbox_created(self): time.sleep(2) imap = IMAP() imap.connect() folders = imap.lm('user/%(local)s@%(domain)s' % (self.user)) self.assertEqual(len(folders), 1) def test_002_autocreate_folders_created(self): time.sleep(2) imap = IMAP() imap.connect() exec("ac_folders = %s" % (conf.get_raw(conf.get('kolab', 'primary_domain'), 'autocreate_folders'))) folders = imap.lm('user/%(local)s/*@%(domain)s' % (self.user)) print(folders) print(ac_folders.keys()) - self.assertEqual(len(folders), len(ac_folders.keys())) + self.assertEqual(len(folders), len(ac_folders)) def test_003_folders_metadata_set(self): imap = IMAP() imap.connect() exec("ac_folders = %s" % (conf.get_raw(conf.get('kolab', 'primary_domain'), 'autocreate_folders'))) folders = [] folders.extend(imap.lm('user/%(local)s@%(domain)s' % (self.user))) folders.extend(imap.lm('user/%(local)s/*@%(domain)s' % (self.user))) for folder in folders: metadata = imap.get_metadata(folder) folder_name = '/'.join(folder.split('/')[2:]).split('@')[0] if folder_name in ac_folders: if 'annotations' in ac_folders[folder_name]: for _annotation in ac_folders[folder_name]['annotations']: if _annotation.startswith('/private/'): continue _annotation_value = ac_folders[folder_name]['annotations'][_annotation] self.assertTrue(_annotation in metadata[metadata.keys().pop()]) self.assertEqual(_annotation_value, metadata[metadata.keys().pop()][_annotation]) diff --git a/tests/functional/user_add.py b/tests/functional/user_add.py index 35e2b6b..e891053 100644 --- a/tests/functional/user_add.py +++ b/tests/functional/user_add.py @@ -1,59 +1,59 @@ import pykolab from pykolab import wap_client conf = pykolab.getConf() def user_add(givenname, sn, preferredlanguage='en_US', **kw): if givenname is None or givenname == '': raise Exception if sn is None or sn == '': raise Exception user_details = { 'givenname': givenname, 'sn': sn, 'preferredlanguage': preferredlanguage, 'ou': 'ou=People,dc=example,dc=org', 'userpassword': 'Welcome2KolabSystems' } user_details.update(kw) login = conf.get('ldap', 'bind_dn') password = conf.get('ldap', 'bind_pw') domain = conf.get('kolab', 'primary_domain') user_type_id = 0 result = wap_client.authenticate(login, password, domain) user_types = wap_client.user_types_list() - for key in user_types['list'].keys(): + for key in user_types['list']: if user_types['list'][key]['key'] == 'kolab': user_type_id = key user_type_info = user_types['list'][user_type_id]['attributes'] params = { 'user_type_id': user_type_id, } - for attribute in user_type_info['form_fields'].keys(): + for attribute in user_type_info['form_fields']: attr_details = user_type_info['form_fields'][attribute] if isinstance(attr_details, dict): if 'optional' not in attr_details or attr_details['optional'] is False or attribute in user_details: params[attribute] = user_details[attribute] elif isinstance(attr_details, list): params[attribute] = user_details[attribute] fvg_params = params fvg_params['object_type'] = 'user' fvg_params['type_id'] = user_type_id - fvg_params['attributes'] = [attr for attr in user_type_info['auto_form_fields'].keys() if attr not in params] + fvg_params['attributes'] = [attr for attr in user_type_info['auto_form_fields'] if attr not in params] result = wap_client.user_add(params) diff --git a/tests/unit/test-002-attendee.py b/tests/unit/test-002-attendee.py index 65eb820..9e7511c 100644 --- a/tests/unit/test-002-attendee.py +++ b/tests/unit/test-002-attendee.py @@ -1,145 +1,145 @@ import datetime import unittest import kolabformat from pykolab.xml import Attendee from pykolab.xml import participant_status_label from pykolab.xml.attendee import InvalidAttendeeCutypeError class TestEventXML(unittest.TestCase): attendee = Attendee("jane@doe.org") def assertIsInstance(self, _value, _type): if hasattr(unittest.TestCase, 'assertIsInstance'): return unittest.TestCase.assertIsInstance(self, _value, _type) else: if (type(_value)) == _type: return True else: raise AssertionError("%s != %s" % (type(_value), _type)) def test_001_minimal(self): self.assertIsInstance(self.attendee.__str__(), str) def test_002_empty_name(self): self.assertEqual(self.attendee.get_name(), "") def test_003_set_name(self): name = "Doe, Jane" self.attendee.set_name(name) self.assertEqual(self.attendee.get_name(), name) def test_004_default_participant_status(self): self.assertEqual(self.attendee.get_participant_status(), 0) def test_005_participant_status_map_length(self): - self.assertEqual(len(self.attendee.participant_status_map.keys()), 7) + self.assertEqual(len(self.attendee.participant_status_map), 7) def test_006_participant_status_map_forward_lookup(self): # Forward lookups self.assertEqual(self.attendee.participant_status_map["NEEDS-ACTION"], 0) self.assertEqual(self.attendee.participant_status_map["ACCEPTED"], 1) self.assertEqual(self.attendee.participant_status_map["DECLINED"], 2) self.assertEqual(self.attendee.participant_status_map["TENTATIVE"], 3) self.assertEqual(self.attendee.participant_status_map["DELEGATED"], 4) self.assertEqual(self.attendee.participant_status_map["IN-PROCESS"], 5) self.assertEqual(self.attendee.participant_status_map["COMPLETED"], 6) def test_007_participant_status_map_reverse_lookup(self): # Reverse lookups self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 0][0], "NEEDS-ACTION") self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 1][0], "ACCEPTED") self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 2][0], "DECLINED") self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 3][0], "TENTATIVE") self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 4][0], "DELEGATED") self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 5][0], "IN-PROCESS") self.assertEqual([k for k, v in self.attendee.participant_status_map.iteritems() if v == 6][0], "COMPLETED") def test_008_default_rsvp(self): self.assertEqual(self.attendee.get_rsvp(), 0) def test_009_rsvp_map_length(self): - self.assertEqual(len(self.attendee.rsvp_map.keys()), 2) + self.assertEqual(len(self.attendee.rsvp_map), 2) def test_010_rsvp_map_forward_lookup_boolean(self): self.assertEqual(self.attendee.rsvp_map["TRUE"], True) self.assertEqual(self.attendee.rsvp_map["FALSE"], False) def test_011_rsvp_map_forward_lookup_integer(self): self.assertEqual(self.attendee.rsvp_map["TRUE"], 1) self.assertEqual(self.attendee.rsvp_map["FALSE"], 0) def test_012_rsvp_map_reverse_lookup_boolean(self): self.assertEqual([k for k, v in self.attendee.rsvp_map.iteritems() if v is True][0], "TRUE") self.assertEqual([k for k, v in self.attendee.rsvp_map.iteritems() if v is False][0], "FALSE") def test_013_rsvp_map_reverse_lookup_integer(self): self.assertEqual([k for k, v in self.attendee.rsvp_map.iteritems() if v == 1][0], "TRUE") self.assertEqual([k for k, v in self.attendee.rsvp_map.iteritems() if v == 0][0], "FALSE") def test_014_default_role(self): self.assertEqual(self.attendee.get_role(), 0) def test_015_role_map_length(self): - self.assertEqual(len(self.attendee.role_map.keys()), 4) + self.assertEqual(len(self.attendee.role_map), 4) def test_016_role_map_forward_lookup(self): self.assertEqual(self.attendee.role_map["REQ-PARTICIPANT"], 0) self.assertEqual(self.attendee.role_map["CHAIR"], 1) self.assertEqual(self.attendee.role_map["OPT-PARTICIPANT"], 2) self.assertEqual(self.attendee.role_map["NON-PARTICIPANT"], 3) def test_017_role_map_reverse_lookup(self): self.assertEqual([k for k, v in self.attendee.role_map.iteritems() if v == 0][0], "REQ-PARTICIPANT") self.assertEqual([k for k, v in self.attendee.role_map.iteritems() if v == 1][0], "CHAIR") self.assertEqual([k for k, v in self.attendee.role_map.iteritems() if v == 2][0], "OPT-PARTICIPANT") self.assertEqual([k for k, v in self.attendee.role_map.iteritems() if v == 3][0], "NON-PARTICIPANT") def test_015_cutype_map_length(self): - self.assertEqual(len(self.attendee.cutype_map.keys()), 5) + self.assertEqual(len(self.attendee.cutype_map), 5) def test_016_cutype_map_forward_lookup(self): self.assertEqual(self.attendee.cutype_map["GROUP"], kolabformat.CutypeGroup) self.assertEqual(self.attendee.cutype_map["INDIVIDUAL"], kolabformat.CutypeIndividual) self.assertEqual(self.attendee.cutype_map["RESOURCE"], kolabformat.CutypeResource) self.assertEqual(self.attendee.cutype_map["ROOM"], kolabformat.CutypeRoom) self.assertEqual(self.attendee.cutype_map["UNKNOWN"], kolabformat.CutypeUnknown) def test_017_cutype_map_reverse_lookup(self): self.assertEqual([k for k, v in self.attendee.cutype_map.iteritems() if v == kolabformat.CutypeGroup][0], "GROUP") self.assertEqual([k for k, v in self.attendee.cutype_map.iteritems() if v == kolabformat.CutypeIndividual][0], "INDIVIDUAL") self.assertEqual([k for k, v in self.attendee.cutype_map.iteritems() if v == kolabformat.CutypeResource][0], "RESOURCE") self.assertEqual([k for k, v in self.attendee.cutype_map.iteritems() if v == kolabformat.CutypeRoom][0], "ROOM") self.assertEqual([k for k, v in self.attendee.cutype_map.iteritems() if v == kolabformat.CutypeUnknown][0], "UNKNOWN") def test_018_partstat_label(self): self.assertEqual(participant_status_label('NEEDS-ACTION'), "Needs Action") self.assertEqual(participant_status_label(kolabformat.PartTentative), "Tentatively Accepted") self.assertEqual(participant_status_label('UNKNOWN'), "UNKNOWN") def test_020_to_dict(self): name = "Doe, Jane" role = 'OPT-PARTICIPANT' cutype = 'RESOURCE' partstat = 'ACCEPTED' self.attendee.set_name(name) self.attendee.set_rsvp(True) self.attendee.set_role(role) self.attendee.set_cutype(cutype) self.attendee.set_participant_status(partstat) data = self.attendee.to_dict() self.assertIsInstance(data, dict) self.assertEqual(data['role'], role) self.assertEqual(data['cutype'], cutype) self.assertEqual(data['partstat'], partstat) self.assertEqual(data['name'], name) self.assertEqual(data['email'], 'jane@doe.org') self.assertTrue(data['rsvp']) def test_030_to_cutype_exception(self): self.assertRaises(InvalidAttendeeCutypeError, self.attendee.set_cutype, "DUMMY") if __name__ == '__main__': unittest.main() diff --git a/ucs/kolab_sieve.py b/ucs/kolab_sieve.py index d7125a7..70ae4b8 100755 --- a/ucs/kolab_sieve.py +++ b/ucs/kolab_sieve.py @@ -1,155 +1,155 @@ #!/usr/bin/python # # 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 . # # workaround for PEP 366 __package__ = '' import listener import logging import os import sys import univention_baseconfig import univention.debug as ulog sys.path = [ os.path.abspath( os.path.join( os.path.dirname( os.path.realpath(os.path.abspath(__file__)) ), '..' ) )] + sys.path sys.stderr = open('/dev/null', 'a') name = 'kolab_sieve' description = "Sieve Script Management for Kolab Groupware on UCS" # The filter has to be composed to make sure only Kolab Groupware # related objects are passed along to this listener module. filter = '(objectClass=kolabInetOrgPerson)' #attributes = [ '*' ] import pykolab from pykolab import constants from pykolab import utils log = pykolab.getLogger('pykolab.listener') log.remove_stdout_handler() log.setLevel(logging.DEBUG) log.debuglevel = 9 conf = pykolab.getConf() conf.finalize_conf(fatal=False) conf.debuglevel = 9 from pykolab.auth import Auth def handler(*args, **kw): auth = Auth() auth.connect() if len(args) == 4: # moddn, not relevant for Sieve Script Management pass elif len(args) == 3: dn = args[0] new = utils.normalize(args[1]) old = utils.normalize(args[2]) - if isinstance(old, dict) and len(old.keys()) > 0: + if isinstance(old, dict) and len(old) > 0: # Either the entry changed or was deleted - if isinstance(new, dict) and len(new.keys()) > 0: + if isinstance(new, dict) and len(new) > 0: # The entry was modified. result_attr = conf.get('cyrus-sasl', 'result_attribute') if result_attr not in new: log.error( "Entry %r does not have attribute %r" % ( dn, result_attr ) ) return # See if the mailserver_attribute exists mailserver_attribute = conf.get('ldap', 'mailserver_attribute').lower() if mailserver_attribute is None: log.error("Mail server attribute is not set") # TODO: Perhaps, query for IMAP servers. If there is only one, # we know what to do. return if mailserver_attribute in new: if not new[mailserver_attribute] == constants.fqdn: log.info( "The mail server for user %r is set, and it is not me (%r)" % ( dn, new[mailserver_attribute] ) ) return else: log.error("Entry %r does not have a mail server set" % (dn)) return conf.plugins.exec_hook( 'sieve_mgmt_refresh', kw={'user': new[result_attr]} ) else: # The entry was deleted. This is irrelevant for # Sieve Script Management return - elif isinstance(new, dict) and len(new.keys()) > 0: + elif isinstance(new, dict) and len(new) > 0: # Old is not a dict (or empty), so the entry is just created # See if the mailserver_attribute exists mailserver_attribute = conf.get('ldap', 'mailserver_attribute').lower() result_attr = conf.get('cyrus-sasl', 'result_attribute').lower() if mailserver_attribute is None: log.error("Mail server attribute is not set") # TODO: Perhaps, query for IMAP servers. If there is only one, # we know what to do. return if mailserver_attribute in new: if not new[mailserver_attribute] == constants.fqdn: log.info("The mail server for user %r is set, and it is not me (%r)" % (dn, new[mailserver_attribute])) return conf.plugins.exec_hook( 'sieve_mgmt_refresh', kw={'user': new[result_attr]} ) else: log.info("entry %r changed, but no new or old attributes" % (dn)) diff --git a/ucs/listener.py b/ucs/listener.py index 67c3dca..a36075f 100755 --- a/ucs/listener.py +++ b/ucs/listener.py @@ -1,200 +1,200 @@ #!/usr/bin/python # # 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 . # # workaround for PEP 366 __package__ = '' import listener import logging import os import sys from univention.config_registry import ConfigRegistry import univention.debug as ulog sys.path = [ os.path.abspath( os.path.join( os.path.dirname( os.path.realpath(os.path.abspath(__file__)) ), '..' ) )] + sys.path #sys.stderr = open('/dev/null', 'a') name = 'kolab' description = "Kolab Groupware Listener for UCS" # The filter has to be composed to make sure only Kolab Groupware # related objects are passed along to this listener module. filter = '(|(objectClass=kolabInetOrgPerson)(objectClass=univentionMailSharedFolder))' # attributes = ['*'] import pykolab from pykolab import constants from pykolab import utils log = pykolab.getLogger('pykolab.listener') # log.remove_stdout_handler() log.setLevel(logging.DEBUG) log.debuglevel = 9 conf = pykolab.getConf() conf.finalize_conf(fatal=False) conf.debuglevel = 9 from pykolab.auth import Auth def handler(*args, **kw): log.info("kolab.handler(args(%d): %r, kw: %r)" % (len(args), args, kw)) auth = Auth() auth.connect() if len(args) == 4: # moddn dn = args[0] new = utils.normalize(args[1]) old = utils.normalize(args[2]) command = args[4] pass elif len(args) == 3: dn = args[0] new = utils.normalize(args[1]) old = utils.normalize(args[2]) - if isinstance(old, dict) and len(old.keys()) > 0: + if isinstance(old, dict) and len(old) > 0: # Two options: # - entry changed # - entry deleted log.info("user %r, old is dict" % (dn)) - if isinstance(new, dict) and len(new.keys()) > 0: + if isinstance(new, dict) and len(new) > 0: log.info("Modify entry %r" % (dn)) mailserver_attribute = conf.get('ldap', 'mailserver_attribute').lower() if mailserver_attribute is None: log.error("Mail server attribute is not set") return if mailserver_attribute in old: log.info("Modified entry %r has mail server attribute %s: %r" % (dn, mailserver_attribute, new[mailserver_attribute])) if not old[mailserver_attribute] == constants.fqdn: # Even though the new mailserver can be us, it is the # *current* mail server that needs to push for the XFER. log.info("The mail server for user %r is set, and it is not me (%r)" % (dn, old[mailserver_attribute])) return else: # If old has no mailserver attribute, but new does, we need to create # the user locally. if mailserver_attribute in new: if not new[mailserver_attribute] == constants.fqdn: log.info("The mail server for user %r is set (in new, not old), but it is not me (%r)" % (dn, new[mailserver_attribute])) return else: log.info("Entry %r does not have a mail server attribute." % (dn)) return auth._auth._synchronize_callback( change_type='modify', previous_dn=None, change_number=None, dn=dn, entry=new ) else: log.info("Delete entry %r" % (dn)) # See if the mailserver_attribute exists mailserver_attribute = conf.get('ldap', 'mailserver_attribute').lower() if mailserver_attribute is None: log.error("Mail server attribute is not set") # TODO: Perhaps, query for IMAP servers. If there is only one, # we know what to do. return if mailserver_attribute in old: log.info("Deleted entry %r has mail server attribute %s: %r" % (dn, mailserver_attribute, old[mailserver_attribute])) if not old[mailserver_attribute] == constants.fqdn: log.info("The mail server for user %r is set, and it is not me (%r)" % (dn, old[mailserver_attribute])) return else: log.info("Entry deletion notification for %r does not have a mail server attribute specified." % (dn)) cfg = ConfigRegistry() cfg.load() if cfg.is_true('mail/cyrus/mailbox/delete', True): auth._auth._synchronize_callback( change_type='delete', previous_dn=None, change_number=None, dn=dn, entry=old ) - elif isinstance(new, dict) and len(new.keys()) > 0: + elif isinstance(new, dict) and len(new) > 0: # Old is not a dict (or empty), so the entry is just created log.info("Add entry %r" % (dn)) # See if the mailserver_attribute exists mailserver_attribute = conf.get('ldap', 'mailserver_attribute').lower() if mailserver_attribute is None: log.error("Mail server attribute is not set") # TODO: Perhaps, query for IMAP servers. If there is only one, # we know what to do. return if mailserver_attribute in new: log.info("Added entry %r has mail server attribute %s: %r" % (dn, mailserver_attribute, new[mailserver_attribute])) if not new[mailserver_attribute] == constants.fqdn: log.info("The mail server for user %r is set, and it is not me (%r)" % (dn, new[mailserver_attribute])) return else: log.info("Added entry %r does not have a mail server attribute set." % (dn)) return auth._auth._synchronize_callback( change_type='add', previous_dn=None, change_number=None, dn=dn, entry=new ) else: log.info("entry %r changed, but no new or old attributes" % (dn)) def initialize(): log.info("kolab.initialize()") diff --git a/wallace/module_optout.py b/wallace/module_optout.py index 4fd199f..ed7fe0e 100644 --- a/wallace/module_optout.py +++ b/wallace/module_optout.py @@ -1,192 +1,192 @@ # -*- coding: utf-8 -*- # Copyright 2010-2013 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # import json import os import random import tempfile import time from urlparse import urlparse import urllib from email import message_from_file from email.utils import formataddr from email.utils import getaddresses import modules import pykolab from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/optout') conf = pykolab.getConf() mybasepath = '/var/spool/pykolab/wallace/optout/' def __init__(): modules.register('optout', execute, description=description()) def description(): return """Consult the opt-out service.""" def execute(*args, **kw): if not os.path.isdir(mybasepath): os.makedirs(mybasepath) for stage in ['incoming', 'ACCEPT', 'REJECT', 'HOLD', 'DEFER' ]: if not os.path.isdir(os.path.join(mybasepath, stage)): os.makedirs(os.path.join(mybasepath, stage)) # TODO: Test for correct call. filepath = args[0] if kw.has_key('stage'): 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.keys(): + 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].keys(): + 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 44b1db5..e29f193 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 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.keys(): + 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/modules.py b/wallace/modules.py index 3f27bbd..1eb3ebe 100644 --- a/wallace/modules.py +++ b/wallace/modules.py @@ -1,456 +1,456 @@ # -*- coding: utf-8 -*- # Copyright 2010-2019 Kolab Systems AG (http://www.kolabsys.com) # # Jeroen van Meeuwen (Kolab Systems) # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # from __future__ import print_function import os import sys import time from email import message_from_string from email.message import Message from email.mime.base import MIMEBase from email.mime.message import MIMEMessage from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.parser import Parser from email.utils import COMMASPACE from email.utils import formatdate from email.utils import formataddr from email.utils import getaddresses from email.utils import parsedate_tz import smtplib import pykolab from pykolab import constants from pykolab.translate import _ log = pykolab.getLogger('pykolab.wallace/modules') extra_log_params = {'qid': '-'} log = pykolab.logger.LoggerAdapter(log, extra_log_params) conf = pykolab.getConf() modules = {} def initialize(): # We only want the base path modules_base_path = os.path.dirname(__file__) for modules_path, dirnames, filenames in os.walk(modules_base_path): if not modules_path == modules_base_path: continue for filename in filenames: if filename.startswith('module_') and filename.endswith('.py'): module_name = filename.replace('.py', '') name = module_name.replace('module_', '') # print("exec(\"from %s import __init__ as %s_register\")" % (module_name,name)) exec("from %s import __init__ as %s_register" % (module_name, name)) exec("%s_register()" % (name)) for dirname in dirnames: register_group(modules_path, dirname) def list_modules(*args, **kw): """ List modules """ __modules = {} - for module in modules.keys(): + for module in modules: if isinstance(module, tuple): module_group, module = module __modules[module_group] = { module: modules[(module_group, module)] } else: __modules[module] = modules[module] _modules = __modules.keys() _modules.sort() for _module in _modules: if 'function' in __modules[_module]: # This is a top-level module if __modules[_module]['description'] is not None: print("%-25s - %s" % (_module.replace('_', '-'), __modules[_module]['description'])) else: print("%-25s" % (_module.replace('_', '-'))) for _module in _modules: if 'function' not in __modules[_module]: # This is a nested module print("\n" + _("Module Group: %s") % (_module) + "\n") ___modules = __modules[_module].keys() ___modules.sort() for __module in ___modules: if __modules[_module][__module]['description'] is not None: print( "%-4s%-21s - %s" % ( '', _module.replace('_', '-'), __modules[_module][__module]['description'] ) ) else: print("%-4s%-21s" % ('', __module.replace('_', '-'))) def execute(name, *args, **kw): if name not in modules: log.error(_("No such module %r in modules %r (1).") % (name, modules)) sys.exit(1) if 'function' not in modules[name] and 'group' not in modules[name]: log.error(_("No such module %r in modules %r (2).") % (name, modules)) sys.exit(1) try: return modules[name]['function'](*args, **kw) except Exception as errmsg: log.exception(_("Module %r - Unknown error occurred; %r") % (name, errmsg)) def heartbeat(name, *args, **kw): if not modules.has_key(name): log.warning(_("No such module %r in modules %r (1).") % (name, modules)) if modules[name].has_key('heartbeat'): return modules[name]['heartbeat'](*args, **kw) def _sendmail(sender, recipients, msg): # NOTE: Use "127.0.0.1" here for IPv6 (see also the service # definition in master.cf). sl = pykolab.logger.StderrToLogger(log) smtplib.stderr = sl smtp = smtplib.SMTP(timeout=15) if conf.debuglevel > 8: smtp.set_debuglevel(1) success = False attempt = 1 while not success and attempt <= 5: try: log.debug(_("Sending email via smtplib from %r, to %r (Attempt %r)") % (sender, recipients, attempt), level=8) smtp.connect("127.0.0.1", 10027) _response = smtp.sendmail(sender, recipients, msg) if len(_response) == 0: log.debug(_("SMTP sendmail OK"), level=8) else: log.debug(_("SMTP sendmail returned: %r") % (_response), level=8) smtp.quit() success = True break except smtplib.SMTPServerDisconnected as errmsg: log.error("SMTP Server Disconnected Error, %r" % (errmsg)) except smtplib.SMTPConnectError as errmsg: # DEFER log.error("SMTP Connect Error, %r" % (errmsg)) except smtplib.SMTPDataError as errmsg: # DEFER log.error("SMTP Data Error, %r" % (errmsg)) except smtplib.SMTPHeloError as errmsg: # DEFER log.error("SMTP HELO Error, %r" % (errmsg)) except smtplib.SMTPRecipientsRefused as errmsg: # REJECT, send NDR log.error("SMTP Recipient(s) Refused, %r" % (errmsg)) except smtplib.SMTPSenderRefused as errmsg: # REJECT, send NDR log.error("SMTP Sender Refused, %r" % (errmsg)) except Exception as errmsg: log.exception(_("smtplib - Unknown error occurred: %r") % (errmsg)) try: smtp.quit() except Exception as errmsg: log.error("smtplib quit() error - %r" % errmsg) time.sleep(10) attempt += 1 return success def cb_action_HOLD(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Holding message in queue for manual review (%s by %s)") % (filepath, module)) def cb_action_DEFER(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Deferring message in %s (by module %s)") % (filepath, module)) # parse message headers message = Parser().parse(open(filepath, 'r'), True) internal_time = parsedate_tz(message.__getitem__('Date')) internal_time = time.mktime(internal_time[:9]) + internal_time[9] now_time = time.time() delta = now_time - internal_time log.debug(_("The time when the message was sent: %r") % (internal_time), level=8) log.debug(_("The time now: %r") % (now_time), level=8) log.debug(_("The time delta: %r") % (delta), level=8) if delta > 432000: # TODO: Send NDR back to user log.debug(_("Message in file %s older then 5 days, deleting") % (filepath), level=8) os.unlink(filepath) # Alternative method is file age. #Date sent(/var/spool/pykolab/wallace/optout/DEFER/tmpIv7pDl): 'Thu, 08 Mar 2012 11:51:03 +0000' #(2012, 3, 8, 11, 51, 3, 0, 1, -1) # YYYY M D H m s weekday, yearday #log.debug(datetime.datetime(*), level=8) #import os #stat = os.stat(filepath) #fileage = datetime.datetime.fromtimestamp(stat.st_mtime) #now = datetime.datetime.now() #delta = now - fileage #print("file:", filepath, "fileage:", fileage, "now:", now, "delta(seconds):", delta.seconds) #if delta.seconds > 1800: ## TODO: Send NDR back to user #log.debug(_("Message in file %s older then 1800 seconds, deleting") % (filepath), level=8) #os.unlink(filepath) def cb_action_REJECT(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Rejecting message in %s (by module %s)") % (filepath, module)) log.debug(_("Rejecting message in: %r") %(filepath), level=8) # parse message headers message = Parser().parse(open(filepath, 'r'), True) envelope_sender = getaddresses(message.get_all('From', [])) recipients = getaddresses(message.get_all('To', [])) + \ getaddresses(message.get_all('Cc', [])) + \ getaddresses(message.get_all('X-Kolab-To', [])) _recipients = [] for recipient in recipients: if not recipient[0] == '': _recipients.append('%s <%s>' % (recipient[0], recipient[1])) else: _recipients.append('%s' % (recipient[1])) # TODO: Find the preferredLanguage for the envelope_sender user. ndr_message_subject = "Undelivered Mail Returned to Sender" ndr_message_text = _("""This is the email system Wallace at %s. I'm sorry to inform you we could not deliver the attached message to the following recipients: - %s Your message is being delivered to any other recipients you may have sent your message to. There is no need to resend the message to those recipients. """) % ( constants.fqdn, "\n- ".join(_recipients) ) diagnostics = _("""X-Wallace-Module: %s X-Wallace-Result: REJECT """) % ( module ) msg = MIMEMultipart("report") msg['From'] = "MAILER-DAEMON@%s" % (constants.fqdn) msg['To'] = formataddr(envelope_sender[0]) msg['Date'] = formatdate(localtime=True) msg['Subject'] = ndr_message_subject msg.preamble = "This is a MIME-encapsulated message." part = MIMEText(ndr_message_text) part.add_header("Content-Description", "Notification") msg.attach(part) _diag_message = Message() _diag_message.set_payload(diagnostics) part = MIMEMessage(_diag_message, "delivery-status") part.add_header("Content-Description", "Delivery Report") msg.attach(part) # @TODO: here I'm not sure message will contain the whole body # when we used headersonly argument of Parser().parse() above # delete X-Kolab-* headers del message['X-Kolab-From'] del message['X-Kolab-To'] part = MIMEMessage(message) part.add_header("Content-Description", "Undelivered Message") msg.attach(part) result = _sendmail( "MAILER-DAEMON@%s" % (constants.fqdn), [formataddr(envelope_sender[0])], msg.as_string() ) log.debug(_("Rejection message was sent successfully: %r") % result) if result: os.unlink(filepath) else: log.debug(_("Message %r was not removed from spool") % filepath) def cb_action_ACCEPT(module, filepath): global extra_log_params extra_log_params['qid'] = os.path.basename(filepath) log.info(_("Accepting message in %s (by module %s)") % (filepath, module)) log.debug(_("Accepting message in: %r") %(filepath), level=8) # parse message headers message = Parser().parse(open(filepath, 'r'), True) messageid = message['message-id'] if 'message-id' in message else None sender = [formataddr(x) for x in getaddresses(message.get_all('X-Kolab-From', []))] recipients = [formataddr(x) for x in getaddresses(message.get_all('X-Kolab-To', []))] log.debug( _("Message-ID: %s, sender: %r, recipients: %r") % (messageid, sender, recipients), level=6 ) # delete X-Kolab-* headers del message['X-Kolab-From'] del message['X-Kolab-To'] log.debug(_("Removed X-Kolab- headers"), level=8) result = _sendmail( sender, recipients, # - Make sure we do not send this as binary. # - Second, strip NUL characters - I don't know where they # come from (TODO) # - Third, a character return is inserted somewhere. It # divides the body from the headers - and we don't like (TODO) # @TODO: check if we need Parser().parse() to load the whole message message.as_string() ) log.debug(_("Message was sent successfully: %r") % result) if result: os.unlink(filepath) else: log.debug(_("Message %r was not removed from spool") % filepath) def register_group(dirname, module): modules_base_path = os.path.join(os.path.dirname(__file__), module) modules[module] = {} for modules_path, dirnames, filenames in os.walk(modules_base_path): if not modules_path == modules_base_path: continue for filename in filenames: if filename.startswith('module_') and filename.endswith('.py'): module_name = filename.replace('.py','') name = module_name.replace('module_', '') # TODO: Error recovery from incomplete / incorrect modules. exec( "from %s.%s import __init__ as %s_%s_register" % ( module, module_name, module, name ) ) exec("%s_%s_register()" % (module,name)) def register(name, func, group=None, description=None, aliases=[], heartbeat=None): if not group == None: module = "%s_%s" % (group,name) else: module = name if isinstance(aliases, basestring): aliases = [aliases] if modules.has_key(module): log.fatal(_("Module '%s' already registered") % (module)) sys.exit(1) if callable(func): if group == None: modules[name] = { 'function': func, 'description': description } else: modules[group][name] = { 'function': func, 'description': description } modules[module] = modules[group][name] modules[module]['group'] = group modules[module]['name'] = name for alias in aliases: modules[alias] = { 'function': func, 'description': _("Alias for %s") % (name) } if callable(heartbeat): modules[module]['heartbeat'] = heartbeat